Skip to main content

httpclient-factory

IHttpClientFactory and typed HTTP clients for .NET 10 applications. Covers named/typed/keyed clients, DelegatingHandlers, resilience with Microsoft.Extensions.Http.Resilience, and testing patterns. Load this skill when configuring HTTP clients, adding retry/circuit breaker policies, or when the user mentions "HttpClient", "IHttpClientFactory", "AddHttpClient", "typed client", "named client", "DelegatingHandler", "resilience", "retry", "circuit breaker", "hedging", "Polly", "AddStandardResilienceHandler", "socket exhaustion", or "Refit".

Source facts

Repository
codewithmukesh/dotnet-claude-kit
Last source activity
February 21, 2026 at 18:10
Detected SKILL.md language
English
Stars
811
Forks
181

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
httpclient-factory
description
IHttpClientFactory and typed HTTP clients for .NET 10 applications. Covers named/typed/keyed clients, DelegatingHandlers, resilience with Microsoft.Extensions.Http.Resilience, and testing patterns. Load this skill when configuring HTTP clients, adding retry/circuit breaker policies, or when the user mentions "HttpClient", "IHttpClientFactory", "AddHttpClient", "typed client", "named client", "DelegatingHandler", "resilience", "retry", "circuit breaker", "hedging", "Polly", "AddStandardResilienceHandler", "socket exhaustion", or "Refit".
# HttpClient Factory ## Core Principles 1. **Never `new HttpClient()` per request** — Raw `HttpClient` creation causes socket exhaustion under load and ignores DNS changes. Use `IHttpClientFactory` to manage handler lifetimes. 2. **Keyed clients over typed clients** — Keyed DI (`.AddAsKeyed()`) is the recommended pattern in .NET 10. Typed clients captured in singletons silently break handler rotation. 3. **Resilience is not optional** — Every external HTTP call needs retry, circuit breaker, and timeout. `AddStandardResilienceHandler()` provides sensible defaults in one line. 4. **DelegatingHandlers for cross-cutting concerns** — Auth tokens, correlation IDs, and logging belong in the handler pipeline, not scattered across service methods. ## Patterns ### Named Client with Resilience ```csharp builder.Services.AddHttpClient("github", client => { client.BaseAddress = new Uri("https://api.github.com/"); client.DefaultRequestHeaders.UserAgent.ParseAdd("MyApp/1.0"); client.DefaultRequestHeaders.Accept.Add( new MediaTypeWithQualityHeaderValue("application/json")); }) .AddStandardResilienceHandler(); // Usage via factory public sealed class GitHubService(IHttpClientFactory factory) { public async Task<Repo?> GetRepoAsync(string owner, string name, CancellationToken ct) { var client = factory.CreateClient("github"); return await client.GetFromJsonAsync<Repo>($"repos/{owner}/{name}", ct); } } ``` ### Keyed Client (Recommended in .NET 10) Combines named client configurability with direct injection. No string lookups. ```csharp builder.Services.AddHttpClient("payments", client => { client.BaseAddress = new Uri("https://api.payments.example.com/"); }) .AddStandardResilienceHandler() .AddAsKeyed(); // Register as keyed scoped service // Inject directly — no IHttpClientFactory needed app.MapPost("/charge", async ( [FromKeyedServices("payments")] HttpClient httpClient, ChargeRequest request, CancellationToken ct) => { var response = await httpClient.PostAsJsonAsync("charges", request, ct); return response.IsSuccessStatusCode ? TypedResults.Ok() : TypedResults.Problem("Payment failed"); }); ``` Global opt-in: `builder.Services.ConfigureHttpClientDefaults(b => b.AddAsKeyed());` ### Standard Resilience Handler `AddStandardResilienceHandler()` chains 5 strategies: | Strategy | Default | |----------|---------| | Rate limiter | 1000 concurrent requests | | Total timeout | 30 seconds | | Retry | 3 retries, exponential backoff with jitter | | Circuit breaker | Opens at 10% failure rate | | Attempt timeout | 10 seconds per attempt | ```csharp builder.Services.AddHttpClient("api") .AddStandardResilienceHandler(options => { options.Retry.MaxRetryAttempts = 5; options.Retry.Delay = TimeSpan.FromSeconds(1); options.TotalRequestTimeout.Timeout = TimeSpan.FromSeconds(60); options.AttemptTimeout.Timeout = TimeSpan.FromSeconds(15); // Disable retries for non-idempotent methods options.Retry.DisableForUnsafeHttpMethods(); }); ``` ### DelegatingHandler for Auth Token Injection ```csharp public sealed class AuthenticationHandler(ITokenService tokenService) : DelegatingHandler { protected override async Task<HttpResponseMessage> SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { var token = await tokenService.GetAccessTokenAsync(cancellationToken); request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); return await base.SendAsync(request, cancellationToken); } } // Registration builder.Services.AddTransient<AuthenticationHandler>(); builder.Services.AddHttpClient("api") .AddHttpMessageHandler<AuthenticationHandler>() .AddStandardResilienceHandler(); ``` ### DelegatingHandler for Correlation ID Propagation ```csharp public sealed class CorrelationIdHandler(IHttpContextAccessor httpContextAccessor) : DelegatingHandler { protected override Task<HttpResponseMessage> SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { if (httpContextAccessor.HttpContext?.Request.Headers .TryGetValue("X-Correlation-Id", out var correlationId) is true) { request.Headers.Add("X-Correlation-Id", correlationId.ToString()); } return base.SendAsync(request, cancellationToken); } } ``` ### SocketsHttpHandler Configuration ```csharp builder.Services.AddHttpClient("advanced") .UseSocketsHttpHandler((handler, _) => { handler.PooledConnectionLifetime = TimeSpan.FromMinutes(2); handler.PooledConnectionIdleTimeout = TimeSpan.FromMinutes(1); handler.MaxConnectionsPerServer = 100; handler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Brotli; }); ``` ### Testing with Mock Handler ```csharp public sealed class MockHttpHandler( HttpStatusCode statusCode, string content) : HttpMessageHandler { protected override Task<HttpResponseMessage> SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { return Task.FromResult(new HttpResponseMessage(statusCode) { Content = new StringContent(content, Encoding.UTF8, "application/json") }); } } // In test var handler = new MockHttpHandler(HttpStatusCode.OK, """{"id":1}"""); var client = new HttpClient(handler) { BaseAddress = new Uri("https://api.test/") }; var service = new MyService(client); ``` ## Anti-patterns ### Don't Create HttpClient Per Request ```csharp // BAD — socket exhaustion under load, ignores DNS changes public async Task<string> GetDataAsync() { using var client = new HttpClient(); return await client.GetStringAsync("https://api.example.com/data"); } // GOOD — factory-managed public async Task<string> GetDataAsync(CancellationToken ct) { var client = factory.CreateClient("api"); return await client.GetStringAsync("https://api.example.com/data", ct); } ``` ### Don't Capture Typed Clients in Singletons ```csharp // BAD — transient HttpClient captured by singleton defeats handler rotation services.AddSingleton<MySingletonService>(); services.AddHttpClient<MySingletonService>(); // GOOD — use keyed client or IHttpClientFactory in singletons services.AddSingleton<MySingletonService>(); services.AddHttpClient("myservice").AddAsKeyed(ServiceLifetime.Singleton); ``` ### Don't Mutate DefaultRequestHeaders on Shared Clients ```csharp // BAD — not thread-safe httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); // GOOD — use DelegatingHandler or per-request HttpRequestMessage using var request = new HttpRequestMessage(HttpMethod.Get, "/api/data"); request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); await httpClient.SendAsync(request, ct); ``` ### Don't Forget CancellationToken ```csharp // BAD — no cancellation support var result = await httpClient.GetFromJsonAsync<Order>("/orders/1"); // GOOD — always pass CancellationToken var result = await httpClient.GetFromJsonAsync<Order>("/orders/1", cancellationToken); ``` ### Don't Stack Multiple Resilience Handlers ```csharp // BAD — conflicting resilience strategies builder.AddStandardResilienceHandler(); builder.AddStandardHedgingHandler(); // GOOD — one standard handler, or a custom pipeline builder.AddStandardResilienceHandler(); ``` ## Decision Guide | Scenario | Recommendation | |----------|---------------| | New .NET 10 project | Keyed clients with `AddAsKeyed()` | | Singleton service needs HttpClient | Named client via `IHttpClientFactory` or keyed singleton | | External API calls | `AddStandardResilienceHandler()` on every client | | Auth token injection | `DelegatingHandler` registered with `AddHttpMessageHandler` | | Hedging (parallel requests) | `AddStandardHedgingHandler()` for latency-sensitive calls | | Non-idempotent methods | `DisableForUnsafeHttpMethods()` on retry options | | Custom retry logic | `AddResilienceHandler("name", builder => ...)` | | Connection pooling control | `UseSocketsHttpHandler` with `PooledConnectionLifetime` | | API client generation | Refit with `AddRefitClient<T>()` | | Integration testing | Custom `HttpMessageHandler` or `MockHttpMessageHandler` |
View on GitHub