Skip to main content 홈 크리에이터 rudironsoni synaxis dotnet-http-client
dotnet-http-client Consumes HTTP APIs. IHttpClientFactory, typed/named clients, resilience, DelegatingHandlers.
설치로 이동 Skills Marketplace 커뮤니티가 만든 AI 스킬을 발견하고 탐색하세요.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
직접 명령은 검토 Prompt를 거치지 않습니다. 실행하기 전에 소스를 확인하세요.
npx skills add https://github.com/rudironsoni/Synaxis --skill dotnet-http-client명령은 한 줄로 유지됩니다. 복사하기 전에 가로로 스크롤해 전체 내용을 확인하세요.
로컬 사본을 원하시나요? SkillsMP에서 현재 제공할 수 있는 파일을 다운로드하세요.
Zip 다운로드 다운로드 중... 이 저장소의 다른 Skills 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.
dotnet-agent-harness-manifest 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.
name dotnet-http-client category web subcategory minimal-apis description Consumes HTTP APIs. IHttpClientFactory, typed/named clients, resilience, DelegatingHandlers. license MIT targets ["*"] tags ["architecture","dotnet","skill"] version 0.0.1 author dotnet-agent-harness invocable true claudecode {"allowed-tools":["Read","Grep","Glob","Bash","Write","Edit"]} codexcli {"short-description":".NET skill guidance for architecture tasks"} opencode {"allowed-tools":["Read","Grep","Glob","Bash","Write","Edit"]} copilot {} geminicli {} antigravity {}
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]
Resilience pipeline configuration (Polly v8, retry, circuit breaker) -- see [skill:dotnet-resilience]
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).
var client = new HttpClient();
static readonly HttpClient _client = new ();
builder.Services.AddHttpClient();
```text
---
## Named Clients
Register clients name scenarios you consume multiple APIs different configurations:
```csharp
builder.Services.AddHttpClient( , client =>
{
client.BaseAddress = Uri( );
client.DefaultRequestHeaders.Add( , );
client.Timeout = TimeSpan.FromSeconds( );
});
builder.Services.AddHttpClient( , client =>
{
client.BaseAddress = Uri( );
client.DefaultRequestHeaders.Add( , );
});
{
Task<Product?> GetProductAsync(
productId, CancellationToken ct)
{
client = clientFactory.CreateClient( );
response = client.GetAsync( , 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. ** ** -- ` `
.
---
##
- [ . ]( :
- [ ]( :
- [ ]( :
- [ ]( :
- [ ]( :
by
for
where
with
"catalog-api"
new
"https://catalog.internal"
"Accept"
"application/json"
30
"payment-api"
new
"https://payments.internal"
"X-Api-Version"
"2"
public sealed class OrderService (IHttpClientFactory clientFactory )
public
async
string
var
"catalog-api"
var
await
$"/products/{productId} "
if
return
null
return
await
## Typed Clients
interface
when
with
public sealed class CatalogApiClient (HttpClient httpClient )
public
async
string
default
var
await
$"/products/{productId} "
if
return
null
return
await
public
async
int
1
int
20
default
var
await
$"/products?page={page} &pageSize={pageSize} "
return
await
public async Task<Product> CreateProductAsync (
CreateProductRequest request,
CancellationToken ct = default )
var
await
"/products"
return
await
new
"https://catalog.internal"
"Accept"
"application/json"
### Typed Client with Interface
interface
csharp
public
interface
ICatalogApiClient
string
default
int
1
int
20
default
public sealed class CatalogApiClient (HttpClient httpClient ) : ICatalogApiClient
new
"https://catalog.internal"
## Resilience Pipelines
using
for
and
### Standard Resilience Handler (Recommended)
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 for requests (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
public sealed class RequestLoggingHandler (
ILogger<RequestLoggingHandler> logger ) : DelegatingHandler
protected override async Task<HttpResponseMessage> SendAsync (
HttpRequestMessage request,
CancellationToken cancellationToken )
var
"HTTP {Method} {Uri}"
var
await
base
"HTTP {Method} {Uri} responded {StatusCode} in {ElapsedMs}ms"
int
return
#### API Key Authentication
csharp
public sealed class ApiKeyHandler (IConfiguration config ) : DelegatingHandler
protected override Task<HttpResponseMessage> SendAsync (
HttpRequestMessage request,
CancellationToken cancellationToken )
var
"ExternalApi:ApiKey"
throw
new
"API key not configured"
"X-Api-Key"
return
base
#### Bearer Token (from Downstream Auth)
csharp
public sealed class BearerTokenHandler (
IHttpContextAccessor httpContextAccessor ) : DelegatingHandler
protected override Task<HttpResponseMessage> SendAsync (
HttpRequestMessage request,
CancellationToken cancellationToken )
var
"Bearer "
""
if
string
new
"Bearer"
return
base
#### Correlation ID Propagation
public
sealed
class
CorrelationIdHandler
DelegatingHandler
private
const
string
"X-Correlation-Id"
protected override Task<HttpResponseMessage> SendAsync (
HttpRequestMessage request,
CancellationToken cancellationToken )
if
var
return
base
### Chaining Multiple Handlers
in
new
"https://catalog.internal"
is
is
and
set
by
and
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" ]
?? throw new InvalidOperationException(
"CatalogApi base URL not configured" )
new
"Services"
"CatalogApi"
"BaseUrl"
"https://catalog.internal"
### Handler Lifetime
default
is
2
for
with
5
1
for
with
5
-10
for
internal
where
## Testing HTTP Clients
### Unit Testing with MockHttpMessageHandler
by
public
sealed
class
CatalogApiClientTests
Fact
public async Task GetProductAsync_ReturnsProduct_WhenFound ()
var
new
"p1"
"Widget"
var
new
new
var
new
new
"https://test.local"
var
new
var
await
"p1"
"Widget"
Fact
public async Task GetProductAsync_ReturnsNull_WhenNotFound ()
var
new
new
var
new
new
"https://test.local"
var
new
var
await
"missing"
public sealed class MockHttpMessageHandler (
HttpResponseMessage response ) : HttpMessageHandler
private
public
protected override Task<HttpResponseMessage> SendAsync (
HttpRequestMessage request,
CancellationToken cancellationToken )
return
### Testing DelegatingHandlers
in
by
public
sealed
class
ApiKeyHandlerTests
Fact
public async Task AddsApiKeyHeader ()
var
new
new
string
string
"ExternalApi:ApiKey"
"test-key-123"
var
new
new
var
new
var
new
new
"https://test.local"
await
"/test"
"test-key-123"
"X-Api-Key"
### Integration Testing with WebApplicationFactory
## Named vs Typed Clients -- Decision Guide
1
-2
string
in
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
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
Do
not
create
HttpClient
with
new
always
inject
IHttpClientFactory
or
a
typed
client
Direct
instantiation
causes
socket
exhaustion
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
Do
not
set
BaseAddress
with
a
trailing
path
new
Uri
https
combining
with
relative
URIs
Use
new
Uri
https
calls
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
Do
not
register
DelegatingHandlers
as
Singleton
they
are
pooled
with
the
HttpMessageHandler
pipeline
and
must
be
Transient
References
IHttpClientFactory
with
NET
https
Use
HttpClientFactory
to
implement
resilient
HTTP
requests
https
HttpClient
message
handlers
https
Typed
clients
https
Microsoft.Extensions.Http.Resilience
https
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
# Instead of:
Read: src/Services/OrderService.cs
Grep: "public void ProcessOrder"
# Use:
serena_find_symbol: "OrderService/ProcessOrder"
serena_get_symbols_overview: "src/Services/OrderService.cs"