Instalar com Codex ou Claude Copie este prompt, cole no Codex, Claude ou outro assistente e deixe que ele revise a página da skill e instale para você.
Um comando direto ignora o prompt de revisão. Verifique a origem antes de executá-lo.
{"short-description":".NET skill guidance for architecture tasks"}
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.
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. ** ** -- ``
.
---
##
- [ .](:
- [ ](:
- [ ](:
- [ ](:
- [](:
Code Navigation (Serena MCP)
Primary approach: Use Serena symbol operations for efficient code navigation:
Find definitions: serena_find_symbol instead of text search
Understand structure: serena_get_symbols_overview for file organization
Track references: serena_find_referencing_symbols for impact analysis
Precise edits: serena_replace_symbol_body for clean modifications
When to use Serena vs traditional tools:
Use Serena: Navigation, refactoring, dependency analysis, precise edits
Use Read/Grep: Reading full files, pattern matching, simple text operations
Fallback: If Serena unavailable, traditional tools work fine
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")