Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
A direct command skips the review prompt. Inspect the source before running it.
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.
Scope
IHttpClientFactory patterns (named and typed clients)
DelegatingHandler chains for cross-cutting concerns
Resilience pipeline integration with HTTP clients
Testing strategies for HTTP client code
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]
Integration testing frameworks -- see [skill:dotnet-integration-testing]
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:
Socket exhaustion -- each HttpClient instance holds its own connection pool. Creating and disposing many
instances exhausts available sockets (SocketException: Address already in use).
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 thisvar client = new HttpClient(); // Socket exhaustion risk// Do not do this eitherstaticreadonly HttpClient _client = new(); // DNS staleness risk// Do this -- use IHttpClientFactory
builder.Services.AddHttpClient();
```text
---
## Named Clients
Register clients by name for scenarios where you consume multiple APIs with different configurations:
```csharp
// 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");
});
// UsagepublicsealedclassOrderService(IHttpClientFactory clientFactory)
{
publicasync Task<Product?> GetProductAsync(
string productId, CancellationToken ct)
{
var client = clientFactory.CreateClient("catalog-api");
var response = await client.GetAsync($"/products/{productId}", ct);
(response.StatusCode == HttpStatusCode.NotFound)
{
;
}
response.EnsureSuccessStatusCode();
response.Content
.ReadFromJsonAsync<Product>(ct);
}
}
```json
---
Typed clients encapsulate HTTP logic behind a strongly-typed . Prefer typed clients a service consumes a
single API multiple operations:
```csharp
{
Task<Product?> GetProductAsync(
productId, CancellationToken ct = )
{
response = httpClient.GetAsync(
, ct);
(response.StatusCode == HttpStatusCode.NotFound)
{
;
}
response.EnsureSuccessStatusCode();
response.Content
.ReadFromJsonAsync<Product>(ct);
}
Task<PagedResult<Product>> ListProductsAsync(
page = ,
pageSize = ,
CancellationToken ct = )
{
response = httpClient.GetAsync(
, ct);
response.EnsureSuccessStatusCode();
( response.Content
.ReadFromJsonAsync<PagedResult<Product>>(ct))!;
}
{
response = httpClient.PostAsJsonAsync(
, request, ct);
response.EnsureSuccessStatusCode();
( response.Content
.ReadFromJsonAsync<Product>(ct))!;
}
}
builder.Services.AddHttpClient<CatalogApiClient>(client =>
{
client.BaseAddress = Uri();
client.DefaultRequestHeaders.Add(, );
});
```json
For testability, define an :
```
{
Task<Product?> GetProductAsync( productId, CancellationToken ct = );
Task<PagedResult<Product>> ListProductsAsync( page = , pageSize = , CancellationToken ct = );
}
{
}
builder.Services.AddHttpClient<ICatalogApiClient, CatalogApiClient>(client =>
{
client.BaseAddress = Uri();
});
```text
---
Apply resilience to HTTP clients `Microsoft.Extensions.Http.Resilience`. See [skill:dotnet-resilience]
detailed pipeline configuration, strategy options, migration guidance.
;
})
.AddStandardResilienceHandler();
```text
```csharp
builder.Services
.AddHttpClient<CatalogApiClient>(client =>
{
client.BaseAddress = Uri();
})
.AddStandardResilienceHandler(options =>
{
options.Retry.MaxRetryAttempts = ;
options.Retry.Delay = TimeSpan.FromSeconds();
options.CircuitBreaker.BreakDuration = TimeSpan.FromSeconds();
options.AttemptTimeout.Timeout = TimeSpan.FromSeconds();
options.TotalRequestTimeout.Timeout = TimeSpan.FromSeconds();
});
```text
For idempotent read operations tail latency matters:
```csharp
builder.Services
.AddHttpClient()
.AddStandardHedgingHandler(options =>
{
options.Hedging.MaxHedgedAttempts = ;
options.Hedging.Delay = TimeSpan.FromMilliseconds();
});
```text
See [skill:dotnet-resilience] to use hedging vs standard retry.
---
`DelegatingHandler` provides a pipeline of message handlers that process outgoing requests incoming responses. Use
them cross-cutting concerns that apply to HTTP traffic.
{
{
stopwatch = Stopwatch.StartNew();
logger.LogInformation(
,
request.Method,
request.RequestUri);
response = .SendAsync(request, cancellationToken);
stopwatch.Stop();
logger.LogInformation(
,
request.Method,
request.RequestUri,
()response.StatusCode,
stopwatch.ElapsedMilliseconds);
response;
}
}
builder.Services.AddTransient<RequestLoggingHandler>();
builder.Services
.AddHttpClient<CatalogApiClient>()
.AddHttpMessageHandler<RequestLoggingHandler>();
```text
```
{
{
apiKey = config[]
?? InvalidOperationException();
request.Headers.Add(, apiKey);
.SendAsync(request, cancellationToken);
}
}
builder.Services.AddTransient<ApiKeyHandler>();
builder.Services
.AddHttpClient<CatalogApiClient>()
.AddHttpMessageHandler<ApiKeyHandler>();
```text
```
{
{
token = httpContextAccessor.HttpContext?
.Request.Headers.Authorization
.ToString()
.Replace(, );
(!.IsNullOrEmpty(token))
{
request.Headers.Authorization =
AuthenticationHeaderValue(, token);
}
.SendAsync(request, cancellationToken);
}
}
```text
```csharp
:
{
HeaderName = ;
{
(!request.Headers.Contains(HeaderName))
{
correlationId = Activity.Current?.Id
?? Guid.NewGuid().ToString();
request.Headers.Add(HeaderName, correlationId);
}
.SendAsync(request, cancellationToken);
}
}
```text
Handlers are added execution order:
```csharp
builder.Services.AddTransient<CorrelationIdHandler>();
builder.Services.AddTransient<BearerTokenHandler>();
builder.Services.AddTransient<RequestLoggingHandler>();
builder.Services
.AddHttpClient<CatalogApiClient>(client =>
{
client.BaseAddress = Uri();
})
.AddHttpMessageHandler<CorrelationIdHandler>()
.AddHttpMessageHandler<BearerTokenHandler>()
.AddHttpMessageHandler<RequestLoggingHandler>()
.AddStandardResilienceHandler();
```text
**Note:** In `IHttpClientFactory`, handlers registered first are outermost. `.AddStandardResilienceHandler()` added last
innermost -- it wraps the actual HTTP call directly. This means retries happen inside the resilience handler without
re-executing the outer DelegatingHandlers. This typically correct: correlation IDs auth tokens are once
the outer handlers, the resilience layer retries the raw HTTP call. If you need per-;
client.BaseAddress = Uri(baseUrl);
});
```text
```json
{
: {
: {
:
}
}
}
```text
The handler lifetime minutes. Adjust services different DNS characteristics:
```csharp
builder.Services
.AddHttpClient<CatalogApiClient>()
.SetHandlerLifetime(TimeSpan.FromMinutes());
```csharp
**Shorter lifetime** ( min): services behind load balancers frequent DNS changes. **Longer lifetime** (
min): stable services connection reuse improves performance.
---
Test typed clients providing a mock handler that returns controlled responses:
```csharp
{
[]
{
expectedProduct = Product { Id = , Name = };
handler = MockHttpMessageHandler(
HttpResponseMessage(HttpStatusCode.OK)
{
Content = JsonContent.Create(expectedProduct)
});
httpClient = HttpClient(handler)
{
BaseAddress = Uri()
};
client = CatalogApiClient(httpClient);
result = client.GetProductAsync();
Assert.NotNull(result);
Assert.Equal(, result.Name);
}
[]
{
handler = MockHttpMessageHandler(
HttpResponseMessage(HttpStatusCode.NotFound));
httpClient = HttpClient(handler)
{
BaseAddress = Uri()
};
client = CatalogApiClient(httpClient);
result = client.GetProductAsync();
Assert.Null(result);
}
}
{
HttpRequestMessage? _lastRequest;
HttpRequestMessage? LastRequest => _lastRequest;
{
_lastRequest = request;
Task.FromResult(response);
}
}
```text
Test handlers isolation providing an inner handler:
```csharp
{
[]
{
config = ConfigurationBuilder()
.AddInMemoryCollection( Dictionary<, ?>
{
[] =
})
.Build();
innerHandler = MockHttpMessageHandler(
HttpResponseMessage(HttpStatusCode.OK));
handler = ApiKeyHandler(config)
{
InnerHandler = innerHandler
};
client = HttpClient(handler)
{
BaseAddress = Uri()
};
client.GetAsync();
Assert.NotNull(innerHandler.LastRequest);
Assert.Equal(
,
innerHandler.LastRequest.Headers
.GetValues().Single());
}
}
```text
Test the full HTTP client pipeline including DI registration:
```csharp
```csharp
---
| Factor | Named Client | Typed Client |
| -------------- | ----------------------------- | ----------------------------------- |
| API surface | Simple ( calls) | Rich (multiple operations) |
| Type safety | Requires name | Strongly typed |
| Encapsulation | HTTP logic consuming | |
| | `` | |
| | | |
| | - | |
** .** , -
.
---
##
- ** ** -- ` ()`
- ** ** -- -
- ** ** -- `()` ( [:-])
- ** ** -- `` (, , )
- ** ** -- --
- ** ** --
- ** / ** -- ``
---
##
1. ** ``** -- `` .
.
2. ** ** -- . ``
( ), `` .
3. ** `` ** -- ` (":
. ` (":
.
4. ** ** -- `()`
`` . - .
(/ ). - ,
.
5. ** ** -- ``
.
---
##
- [ .](:
- [ ](:
- [ ](:
- [ ](:
- [](:
The standard handler applies the full pipeline (rate limiter, total timeout, retry, circuit breaker, attempt timeout)
with sensible defaults:
```csharp
builder.Services
.AddHttpClient<CatalogApiClient>(client =>
{
client.BaseAddress = new Uri("https://catalog.internal")
### Standard Handler with Custom Options
new
"https://catalog.internal"
5
1
15
5
60
### Hedging Handler (for Read-Only APIs)
where
"search-api"
2
500
for
when
## DelegatingHandlers
and
for
### Handler Pipeline Order
Handlers execute in registration order forrequests (outermost to innermost) and reverse order for responses:
```text
Request --> Handler A --> Handler B --> Handler C --> HttpClientHandler --> Server
Response <-- Handler A <-- Handler B <-- Handler C <-- HttpClientHandler <-- Server
```text
### Common Handlers
#### Request Logging
```csharp
publicsealedclassRequestLoggingHandler(
ILogger<RequestLoggingHandler> logger) : DelegatingHandler
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
```csharp
builder.Services.AddHttpClient<CatalogApiClient>(client =>
{
var baseUrl = builder.Configuration["Services:CatalogApi:BaseUrl"]
?? thrownew InvalidOperationException(
"CatalogApi base URL not configured")