A .NET 9 solution collecting sixteen HttpClient usage patterns — named and
typed clients, IHttpClientFactory, Polly resilience, custom delegating
handlers, authentication, streaming, uploads, GraphQL, webhooks, and
concurrency — each as a runnable demonstration with tests.
The recurring theme is the recommended way vs. the hand-rolled way: several
patterns are implemented twice, once with Polly and once with custom handlers,
with the custom versions marked [Obsolete] and labelled educational.
| Project | What it is |
|---|---|
API |
The contacts API the demonstrations call |
Client |
Console app — runs one demonstration at a time |
Client.Test |
xUnit tests, 157 [Fact]/[Theory] cases |
Core |
Contact and the view models |
Persistence |
DataContext for the API |
All five target net9.0.
dotnet restore HttpClient.sln
dotnet test HttpClient.slnCI (.github/workflows/ci.yml) builds and tests on Ubuntu, Windows, and macOS;
code-quality.yml runs separately.
dotnet run --project API --launch-profile SelfHostSelfHost is the only profile and listens on https://localhost:5001.
⚠️ Port mismatch. Every named client inClient/HttpClientServices.cshas itsBaseAddressset tohttps://localhost:44354/— an IIS Express port that theSelfHostprofile doesn't use. Before running the console client against the local API, change either the client registrations or the API'sapplicationUrlso they agree.
Client/Program.cs picks the demonstration by which IService implementation is
registered. All sixteen are registered as concrete types; exactly one is also
registered as IService, and the rest sit as commented-out lines grouped by
theme. The checked-in selection is PollyResilienceService.
dotnet run --project ClientTo run a different one, comment out the PollyResilienceService registration
and uncomment another.
Basic
CRUDService— GET/POST/PUT/PATCH/DELETE against the contacts APISampleService— response-handling variations
IHttpClientFactory
HttpClientFactoryManagementService— factory lifetimes and named clientsHttpCustomMessageHandlerService— customDelegatingHandlers in the pipeline
Advanced
AuthenticationService— Bearer, Basic, and refresh-token flowsFileUploadService— multipart, stream, and progress-reporting uploadsErrorHandlingService— fault handling and consistent error surfacesConcurrencyService— parallel requests and throttlingCustomHeadersService— per-request configurationWebhookService— webhook delivery and batch processingGraphQLService— GraphQL overHttpClientStreamService— streaming large payloads without buffering
Hand-rolled (educational)
ResiliencePatternService— retry/circuit-breaker written by handPerformanceOptimizationService— compression, pooling, connection limitsProductionReadyHttpClientService— headers, security, monitoring
Recommended
PollyResilienceService— the Polly-based equivalent, and the default
HttpClientServices.AddHttpClientServices() sets up five clients plus one
educational one:
| Client | Timeout | Policies / handlers | Notes |
|---|---|---|---|
ContactsClient (named) |
30s | — | GZip/Deflate decompression |
PollyClient |
100s | Retry + circuit breaker + timeout | MaxConnectionsPerServer = 10 |
ProductionClient |
100s | Logging handler + retry + circuit breaker | Adds X-Client-Version |
PerformanceClient |
15s | — | MaxConnectionsPerServer = 50, no proxy |
ContactsClient (typed) |
— | Logging handler + retry | Configured in the class constructor |
ContactsClientCustomHandler |
30s | TimeOutDelegatingHandler, RetryPolicyDelegatingHandler |
[Obsolete], educational only |
The HttpClient timeout is deliberately longer than the Polly timeout policy on
the resilient clients — the outer Timeout has to outlast the whole
retry sequence, or it cancels the retries it was meant to allow.
Defined once and shared across clients:
- Retry —
HandleTransientHttpError()(which coversHttpRequestException, 5xx, and 408), 3 attempts, exponential backoff2^nseconds plus 0–100 ms of jitter so simultaneous clients don't retry in lockstep. - Circuit breaker — opens after 5 handled failures, stays open 30 seconds.
- Timeout — 10 seconds per attempt.
Order matters: .AddPolicyHandler wraps outermost-first, so retry sits outside
the circuit breaker, which sits outside the per-attempt timeout.
Client/MessageHandlers/ holds the hand-written equivalents —
RetryPolicyDelegatingHandler, TimeOutDelegatingHandler,
CircuitBreakerDelegatingHandler, and LoggingDelegatingHandler. The first
three are [Obsolete] on purpose: they exist to show what Polly does for you,
not to be used. LoggingDelegatingHandler is not obsolete and is wired into the
production and typed clients.
Client.Test/ has one test class per service. HandlersStub/ provides
HttpMessageHandler stubs that return fixed 200, 401, and 404 responses, so
the services can be exercised without a running API — which is why
dotnet test passes regardless of the port mismatch above.