Token导航 LogoToken导航TokenDH.com
研究检索敏感数据github未标认证来源可访问许可证需确认审计通过

dotnet-http-clientdotnet http 客户端

Agent Skill

dotnet-http-client 用于查找、检索和筛选相关信息,适合在 Codex、Claude、Cursor、Gemini CLI 中需要根据关键词、任务场景或来源线索快速定位候选结果时使用。可结合来源仓库、安装命令和原始 README 继续核验具体用法。安装前建议确认权限范围、维护状态,以及是否会触发联网、命令执行或文件读写。

总安装

245

周安装

17

GitHub Stars

15

下载量

140
CodexClaudeCursorGemini CLI

安装说明

本站只整理中文说明和来源信息,不托管安装包,也不代用户安装。

GitHub

来源数

2

许可证

unknown

最后核验

2026-05-01

来源状态

来源可访问

安装方式

通过对话安装

复制提示词发给支持本地命令或 Skills 的 AI 助手,先确认命令和权限,再让它执行。

请帮我安装这个 Agent Skill:dotnet-http-client(dotnet http 客户端)
来源仓库:https://github.com/wshaddix/dotnet-skills
仓库路径:skills/dotnet-http-client
安装命令:
npx skills add https://github.com/wshaddix/dotnet-skills --skill dotnet-http-client
安装前请先检查当前环境是否支持对应 CLI,并向我确认将要执行的命令、安装目录、联网范围和文件读写权限;确认后再执行。

命令行安装

复制命令到本机终端执行。该命令会通过 npx skills 从第三方来源获取 Skill;本站只展示命令,不托管安装包,也不自动执行。

skills.shnpx skills
npx skills add https://github.com/wshaddix/dotnet-skills --skill dotnet-http-client

简介

用于查找、检索和筛选相关信息。dotnet-http-client 属于研究检索类 Skill,可作为该场景下的辅助能力补充。

  • 适合根据关键词或场景快速定位候选结果。
  • 可结合来源仓库进一步核验具体用法。适用宿主包括 Codex、Claude、Cursor、Gemini CLI,接入前应确认版本、权限和运行环境要求。
  • 安装前建议确认权限范围与维护状态。
  • 注意:不涉及 DI 容器机制或异步模式等边界内容。

SKILL.md

dotnet-http-client

Best practices for consuming HTTP APIs in.NET applications using IHttpClientFactory. Covers named and typed clients, resilience pipeline integration, DelegatingHandler chains for cross-cutting concerns, and testing strategies.

Out of scope: DI container mechanics and service lifetimes -- see [skill:dotnet-csharp-dependency-injection]. Async/await patterns and cancellation token propagation -- see [skill:dotnet-csharp-async-patterns]. Resilience pipeline configuration (Polly v8, retry, circuit breaker, timeout strategies) is owned by [skill:dotnet-resilience]. Integration testing frameworks -- see [skill:dotnet-integration-testing] for WebApplicationFactory and HTTP client testing patterns.

Cross-references: [skill:dotnet-resilience] for resilience pipeline configuration, [skill:dotnet-csharp-dependency-injection] for service registration, [skill:dotnet-csharp-async-patterns] for async HTTP patterns.


Why IHttpClientFactory

Creating HttpClient instances directly causes two problems:

  1. Socket exhaustion -- each HttpClient instance holds its own connection pool. Creating and disposing many instances exhausts available sockets (SocketException: Address already in use).
  2. DNS staleness -- a long-lived singleton HttpClient caches DNS lookups indefinitely, missing DNS changes during blue-green deployments or failovers.

IHttpClientFactory solves both by managing HttpMessageHandler lifetimes with automatic pooling and rotation (default: 2-minute handler lifetime).

// Do not do this
var client = new HttpClient(); // Socket exhaustion risk

// Do not do this either
static readonly HttpClient _client = new(); // DNS staleness risk

// Do this -- use IHttpClientFactory
builder.Services.AddHttpClient();

Named Clients

Register clients by name for scenarios where you consume multiple APIs with different configurations:

// Registration
builder.Services.AddHttpClient("catalog-api", client =>
{
    client.BaseAddress = new Uri("https://catalog.internal");
    client.DefaultRequestHeaders.Add("Accept", "application/json");
    client.Timeout = TimeSpan.FromSeconds(30);
});

builder.Services.AddHttpClient("payment-api", client =>
{
    client.BaseAddress = new Uri("https://payments.internal");
    client.DefaultRequestHeaders.Add("X-Api-Version", "2");
});

// Usage
public sealed class OrderService(IHttpClientFactory clientFactory)
{
    public async Task<Product?> GetProductAsync(
        string productId, CancellationToken ct)
    {
        var client = clientFactory.CreateClient("catalog-api");
        var response = await client.GetAsync($"/products/{productId}", ct);

        if (response.StatusCode == HttpStatusCode.NotFound)
        {
            return null;
        }

        response.EnsureSuccessStatusCode();
        return await response.Content
            .ReadFromJsonAsync<Product>(ct);
    }
}

Typed Clients

Typed clients encapsulate HTTP logic behind a strongly-typed interface. Prefer typed clients when a service consumes a single API with multiple operations:

// Typed client class
public sealed class CatalogApiClient(HttpClient httpClient)
{
    public async Task<Product?> GetProductAsync(
        string productId, CancellationToken ct = default)
    {
        var response = await httpClient.GetAsync(
            $"/products/{productId}", ct);

        if (response.StatusCode == HttpStatusCode.NotFound)
        {
            return null;
        }

        response.EnsureSuccessStatusCode();
        return await response.Content
            .ReadFromJsonAsync<Product>(ct);
    }

    public async Task<PagedResult<Product>> ListProductsAsync(
        int page = 1,
        int pageSize = 20,
        CancellationToken ct = default)
    {
        var response = await httpClient.GetAsync(
            $"/products?page={page}&pageSize={pageSize}", ct);

        response.EnsureSuccessStatusCode();
        return (await response.Content
            .ReadFromJsonAsync<PagedResult<Product>>(ct))!;
    }

    public async Task<Product> CreateProductAsync(
        CreateProductRequest request,
        CancellationToken ct = default)
    {
        var response = await httpClient.PostAsJsonAsync(
            "/products", request, ct);

        response.EnsureSuccessStatusCode();
        return (await response.Content
            .ReadFromJsonAsync<Product>(ct))!;
    }
}

// Registration
builder.Services.AddHttpClient<CatalogApiClient>(client =>
{
    client.BaseAddress = new Uri("https://catalog.internal");
    client.DefaultRequestHeaders.Add("Accept", "application/json");
});

Typed Client with Interface

For testability, define an interface:

public interface ICatalogApiClient
{
    Task<Product?> GetProductAsync(string productId, CancellationToken ct = default);
    Task<PagedResult<Product>> ListProductsAsync(int page = 1, int pageSize = 20, CancellationToken ct = default);
}

public sealed class CatalogApiClient(HttpClient httpClient) : ICatalogApiClient
{
    // Implementation as above
}

// Registration with interface
builder.Services.AddHttpClient<ICatalogApiClient, CatalogApiClient>(client =>
{
    client.BaseAddress = new Uri("https://catalog.internal");
});

Resilience Pipelines

Apply resilience to HTTP clients using Microsoft.Extensions.Http.Resilience. See [skill:dotnet-resilience] for detailed pipeline configuration, strategy options, and migration guidance.

Standard Resilience Handler (Recommended)

The standard handler applies the full pipeline (rate limiter, total timeout, retry, circuit breaker, attempt timeout) with sensible defaults:

builder.Services
    .AddHttpClient<CatalogApiClient>(client =>
    {
        client.BaseAddress = new Uri("https://catalog.internal");
    })
    .AddStandardResilienceHandler();

Standard Handler with Custom Options

builder.Services
    .AddHttpClient<CatalogApiClient>(client =>
    {
        client.BaseAddress = new Uri("https://catalog.internal");
    })
    .AddStandardResilienceHandler(options =>
    {
        options.Retry.MaxRetryAttempts = 5;
        options.Retry.Delay = TimeSpan.FromSeconds(1);
        options.CircuitBreaker.BreakDuration = TimeSpan.FromSeconds(15);
        options.AttemptTimeout.Timeout = TimeSpan.FromSeconds(5);
        options.TotalRequestTimeout.Timeout = TimeSpan.FromSeconds(60);
    });

Hedging Handler (for Read-Only APIs)

For idempotent read operations where tail latency matters:

builder.Services
    .AddHttpClient("search-api")
    .AddStandardHedgingHandler(options =>
    {
        options.Hedging.MaxHedgedAttempts = 2;
        options.Hedging.Delay = TimeSpan.FromMilliseconds(500);
    });

See [skill:dotnet-resilience] for when to use hedging vs standard retry.


DelegatingHandlers

DelegatingHandler provides a pipeline of message handlers that process outgoing requests and incoming responses. Use them for cross-cutting concerns that apply to HTTP traffic.

Handler Pipeline Order

Handlers execute in registration order for requests (outermost to innermost) and reverse order for responses:

Request  --> Handler A --> Handler B --> Handler C --> HttpClientHandler --> Server
Response <-- Handler A <-- Handler B <-- Handler C <-- HttpClientHandler <-- Server

Common Handlers

Request Logging

public sealed class RequestLoggingHandler(
    ILogger<RequestLoggingHandler> logger) : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        var stopwatch = Stopwatch.StartNew();

        logger.LogInformation(
            "HTTP {Method} {Uri}",
            request.Method,
            request.RequestUri);

        var response = await base.SendAsync(request, cancellationToken);

        stopwatch.Stop();
        logger.LogInformation(
            "HTTP {Method} {Uri} responded {StatusCode} in {ElapsedMs}ms",
            request.Method,
            request.RequestUri,
            (int)response.StatusCode,
            stopwatch.ElapsedMilliseconds);

        return response;
    }
}

// Registration
builder.Services.AddTransient<RequestLoggingHandler>();
builder.Services
    .AddHttpClient<CatalogApiClient>(/* ... */)
    .AddHttpMessageHandler<RequestLoggingHandler>();

API Key Authentication

public sealed class ApiKeyHandler(IConfiguration config) : DelegatingHandler
{
    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        var apiKey = config["ExternalApi:ApiKey"]
            ?? throw new InvalidOperationException("API key not configured");

        request.Headers.Add("X-Api-Key", apiKey);

        return base.SendAsync(request, cancellationToken);
    }
}

// Registration
builder.Services.AddTransient<ApiKeyHandler>();
builder.Services
    .AddHttpClient<CatalogApiClient>(/* ... */)
    .AddHttpMessageHandler<ApiKeyHandler>();

Bearer Token (from Downstream Auth)

public sealed class BearerTokenHandler(
    IHttpContextAccessor httpContextAccessor) : DelegatingHandler
{
    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        var token = httpContextAccessor.HttpContext?
            .Request.Headers.Authorization
            .ToString()
            .Replace("Bearer ", "");

        if (!string.IsNullOrEmpty(token))
        {
            request.Headers.Authorization =
                new AuthenticationHeaderValue("Bearer", token);
        }

        return base.SendAsync(request, cancellationToken);
    }
}

Correlation ID Propagation

public sealed class CorrelationIdHandler : DelegatingHandler
{
    private const string HeaderName = "X-Correlation-Id";

    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        if (!request.Headers.Contains(HeaderName))
        {
            var correlationId = Activity.Current?.Id
                ?? Guid.NewGuid().ToString();
            request.Headers.Add(HeaderName, correlationId);
        }

        return base.SendAsync(request, cancellationToken);
    }
}

Chaining Multiple Handlers

Handlers are added in execution order:

builder.Services.AddTransient<CorrelationIdHandler>();
builder.Services.AddTransient<BearerTokenHandler>();
builder.Services.AddTransient<RequestLoggingHandler>();

builder.Services
    .AddHttpClient<CatalogApiClient>(client =>
    {
        client.BaseAddress = new Uri("https://catalog.internal");
    })
    .AddHttpMessageHandler<CorrelationIdHandler>()   // 1st (outermost): add correlation ID
    .AddHttpMessageHandler<BearerTokenHandler>()     // 2nd: add auth token
    .AddHttpMessageHandler<RequestLoggingHandler>()  // 3rd: log request/response
    .AddStandardResilienceHandler();                 // 4th (innermost): resilience pipeline

Note: In IHttpClientFactory, handlers registered first are outermost. .AddStandardResilienceHandler() added last is innermost -- it wraps the actual HTTP call directly. This means retries happen inside the resilience handler without re-executing the outer DelegatingHandlers. This is typically correct: correlation IDs and auth tokens are set once by the outer handlers, and the resilience layer retries the raw HTTP call. If you need per-retry token refresh (e.g., expired bearer tokens), move the token handler inside the resilience boundary or use a custom ResiliencePipelineBuilder callback.


Configuration Patterns

Base Address from Configuration

builder.Services.AddHttpClient<CatalogApiClient>(client =>
{
    var baseUrl = builder.Configuration["Services:CatalogApi:BaseUrl"]
        ?? throw new InvalidOperationException(
            "CatalogApi base URL not configured");
    client.BaseAddress = new Uri(baseUrl);
});
{
  "Services": {
    "CatalogApi": {
      "BaseUrl": "https://catalog.internal"
    }
  }
}

Handler Lifetime

The default handler lifetime is 2 minutes. Adjust for services with different DNS characteristics:

builder.Services
    .AddHttpClient<CatalogApiClient>(/* ... */)
    .SetHandlerLifetime(TimeSpan.FromMinutes(5));

Shorter lifetime (1 min): for services behind load balancers with frequent DNS changes. Longer lifetime (5-10 min): for stable internal services where connection reuse improves performance.


Testing HTTP Clients

Unit Testing with MockHttpMessageHandler

Test typed clients by providing a mock handler that returns controlled responses:

public sealed class CatalogApiClientTests
{
    [Fact]
    public async Task GetProductAsync_ReturnsProduct_WhenFound()
    {
        // Arrange
        var expectedProduct = new Product { Id = "p1", Name = "Widget" };
        var handler = new MockHttpMessageHandler(
            new HttpResponseMessage(HttpStatusCode.OK)
            {
                Content = JsonContent.Create(expectedProduct)
            });

        var httpClient = new HttpClient(handler)
        {
            BaseAddress = new Uri("https://test.local")
        };

        var client = new CatalogApiClient(httpClient);

        // Act
        var result = await client.GetProductAsync("p1");

        // Assert
        Assert.NotNull(result);
        Assert.Equal("Widget", result.Name);
    }

    [Fact]
    public async Task GetProductAsync_ReturnsNull_WhenNotFound()
    {
        var handler = new MockHttpMessageHandler(
            new HttpResponseMessage(HttpStatusCode.NotFound));

        var httpClient = new HttpClient(handler)
        {
            BaseAddress = new Uri("https://test.local")
        };

        var client = new CatalogApiClient(httpClient);

        var result = await client.GetProductAsync("missing");

        Assert.Null(result);
    }
}

// Reusable mock handler
public sealed class MockHttpMessageHandler(
    HttpResponseMessage response) : HttpMessageHandler
{
    private HttpRequestMessage? _lastRequest;

    public HttpRequestMessage? LastRequest => _lastRequest;

    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        _lastRequest = request;
        return Task.FromResult(response);
    }
}

Testing DelegatingHandlers

Test handlers in isolation by providing an inner handler:

public sealed class ApiKeyHandlerTests
{
    [Fact]
    public async Task AddsApiKeyHeader()
    {
        // Arrange
        var config = new ConfigurationBuilder()
            .AddInMemoryCollection(new Dictionary<string, string?>
            {
                ["ExternalApi:ApiKey"] = "test-key-123"
            })
            .Build();

        var innerHandler = new MockHttpMessageHandler(
            new HttpResponseMessage(HttpStatusCode.OK));

        var handler = new ApiKeyHandler(config)
        {
            InnerHandler = innerHandler
        };

        var client = new HttpClient(handler)
        {
            BaseAddress = new Uri("https://test.local")
        };

        // Act
        await client.GetAsync("/test");

        // Assert
        Assert.NotNull(innerHandler.LastRequest);
        Assert.Equal(
            "test-key-123",
            innerHandler.LastRequest.Headers
                .GetValues("X-Api-Key").Single());
    }
}

Integration Testing with WebApplicationFactory

Test the full HTTP client pipeline including DI registration:

// See [skill:dotnet-integration-testing] for WebApplicationFactory patterns

Named vs Typed Clients -- Decision Guide

FactorNamed ClientTyped Client
API surfaceSimple (1-2 calls)Rich (multiple operations)
Type safetyRequires string nameStrongly typed
EncapsulationHTTP logic in consuming classHTTP logic in client class
TestabilityMock IHttpClientFactoryMock the client interface
Multiple APIsOne name per APIOne class per API
RecommendationAd-hoc or simple callsPrimary pattern for API consumption

Default to typed clients. Use named clients only for simple, one-off HTTP calls where a full typed client class adds unnecessary ceremony.


Key Principles

  • Always use IHttpClientFactory -- never new HttpClient() in application code
  • Prefer typed clients -- encapsulate HTTP logic behind a strongly-typed interface
  • Apply resilience via pipeline -- use AddStandardResilienceHandler() (see [skill:dotnet-resilience]) rather than manual retry loops
  • Keep handlers focused -- each DelegatingHandler should do one thing (auth, logging, correlation)
  • Register handlers as Transient -- DelegatingHandlers are created per-client-instance and should not hold state across requests
  • Pass CancellationToken everywhere -- from endpoint to typed client to HTTP call
  • Use ReadFromJsonAsync / PostAsJsonAsync -- avoid manual serialization with StringContent

Agent Gotchas

  1. Do not create HttpClient with new -- always inject IHttpClientFactory or a typed client. Direct instantiation causes socket exhaustion.
  2. Do not dispose typed clients -- the factory manages handler lifetimes. Disposing the HttpClient instance is harmless (it does not close pooled connections), but wrapping it in using is misleading.
  3. Do not set BaseAddress with a trailing path -- new Uri("https://api.example.com/v2") will drop /v2 when combining with relative URIs. Use new Uri("https://api.example.com/v2/") (trailing slash) or use absolute URIs in calls.
  4. Understand that resilience added last is innermost -- AddStandardResilienceHandler() registered after AddHttpMessageHandler calls wraps the HTTP call directly. Retries do not re-execute outer DelegatingHandlers. This is correct for most cases (tokens/correlation IDs set once). If you need per-retry token refresh, place the token handler after the resilience handler or use a custom pipeline callback.
  5. Do not register DelegatingHandlers as Singleton -- they are pooled with the HttpMessageHandler pipeline and must be Transient.

References

适合场景

01

用户想查找某类 Agent Skill 时

02

需要根据任务场景推荐可安装能力包时

03

需要对比不同来源的安装命令和来源信息时

能力概览

能力 1

按任务关键词查找相关 Skills

能力 2

展示可复制的安装命令

能力 3

保留来源站点、仓库和原始说明,方便继续核验

能力 4

展示第三方安全扫描或审计结果

安装后应在对应宿主中按原始 README 的触发条件使用;具体调用方式请以来源页面和 README 为准。

平台分布

Codex

35.24%
按下载量换算49

Claude

28.5%
按下载量换算40

Cursor

20.51%
按下载量换算29

Gemini CLI

9.74%
按下载量换算14

安全审计

Gen Agent Trust Hub

通过

Socket

通过

Snyk

通过

权限和风险

敏感数据

该 Skill 可能接触密钥、Token、环境变量或敏感配置,应进入高风险复核队列,默认不自动发布。

安装前确认

本站仅展示第三方公开信息,不托管安装包,不提供自动安装或运行环境。安装前应自行审查源码、依赖和命令行为。当前只有一个来源,正式发布前建议补源仓库或其他目录站核验。

来源信息

继续浏览同类 Skills