用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/rudironsoni/Synaxis --skill dotnet-http-client命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
AI-powered wiki generation for code repositories with commands, agents, and skills
Routes .NET/C# work to domain skills. Loads coding-standards for code paths.
Skill manifest management for dotnet-agent-harness. Tracks skill dependencies, conflicts, version compatibility, and provides validation and resolution tools. Triggers on: skill manifest, dependency resolution, skill compatibility, version conflicts, build manifest, validate dependencies.
基于 SOC 职业分类
正在显示 SKILL.md
| name | dotnet-http-client |
| description | Consumes HTTP APIs. IHttpClientFactory, typed/named clients, resilience, DelegatingHandlers. |
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.
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.
Creating HttpClient instances directly causes two problems:
HttpClient instance holds its own connection pool. Creating and disposing many instances exhausts available sockets (SocketException: Address already in use).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();
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 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();
( response.Content
.ReadFromJsonAsync<Product>(ct))!;
}
}
builder.Services.AddHttpClient<CatalogApiClient>(client =>
{
client.BaseAddress = Uri();
client.DefaultRequestHeaders.Add(, );
});
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");
});
Apply resilience to HTTP clients using Microsoft.Extensions.Http.Resilience. See [skill:dotnet-resilience] for detailed pipeline configuration, strategy options, and migration guidance.
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();
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);
});
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.
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.
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
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>();
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>();
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);
}
}
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);
}
}
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.
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"
}
}
}
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.
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);
result = client.GetProductAsync();
Assert.Null(result);
}
}
{
HttpRequestMessage? _lastRequest;
HttpRequestMessage? LastRequest => _lastRequest;
{
_lastRequest = request;
Task.FromResult(response);
}
}
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());
}
}
Test the full HTTP client pipeline including DI registration:
// See [skill:dotnet-integration-testing] for WebApplicationFactory patterns
| Factor | Named Client | Typed Client |
|---|---|---|
| API surface | Simple (1-2 calls) | Rich (multiple operations) |
| Type safety | Requires string name | Strongly typed |
| Encapsulation | HTTP logic in consuming class | HTTP logic in client class |
| Testability | Mock IHttpClientFactory | Mock the client interface |
| Multiple APIs | One name per API | One class per API |
| Recommendation | Ad-hoc or simple calls | Primary 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.
new HttpClient() in application codeAddStandardResilienceHandler() (see [skill:dotnet-resilience]) rather than manual retry loopsDelegatingHandler should do one thing (auth, logging, correlation)StringContentnew -- always inject IHttpClientFactory or a typed client. Direct instantiation causes socket exhaustion.HttpClient instance is harmless (it does not close pooled connections), but wrapping it in using is misleading.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.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.HttpMessageHandler pipeline and must be Transient.