Named clients from appsettings.json, retry with backoff, per-call timeout, rate limiting and call logging — on top of IHttpClientFactory. Timeouts and network errors come back as status codes (408 / 502 / 500), not exceptions. Targets net8.0, net9.0, net10.0.
dotnet add package CSharpEssentials.HttpHelper
{
"HttpClientOptions": [
{ "Name": "github", "UseCompression": true }
]
}
builder.Services.AddHttpClients(builder.Configuration);
public class GitHubService(IhttpsClientHelperFactory factory)
{
public async Task<string> GetUserAsync(string user, CancellationToken ct)
{
var response = await factory.CreateOrGet("github")
.addRetryCondition(r => (int)r.StatusCode >= 500, retryCount: 3, backoffFactor: 2)
.addTimeout(TimeSpan.FromSeconds(10))
.SendAsync($"https://api.github.com/users/{user}", HttpMethod.Get,
headers: new Dictionary<string, string> { ["User-Agent"] = "my-app" },
cancellationToken: ct);
return await response.Content.ReadAsStringAsync(ct);
}
}
| Feature | How | Behaviour |
|---|---|---|
| Named clients | HttpClientOptions + AddHttpClients() | Pooled handlers via IHttpClientFactory |
| Retry with backoff | addRetryCondition(predicate, count, factor) | Wait before retry n = factorn seconds; adds X-Retry-Attempt |
| Timeout | addTimeout(TimeSpan) | Expiry returns 408 |
| Rate limiting | RateLimitOptions (sliding window) | Over the limit returns 429 |
| Transport errors | automatic | HttpRequestException returns 502, other exceptions 500 |
| Caller cancellation | CancellationToken | Throws OperationCanceledException (standard .NET) |
| Request callbacks | AddRequestAction | Request, response, retry attempt, elapsed; failures are isolated |
| Compression, proxy, certificate | config | Applied to the pooled SocketsHttpHandler |
| Mocking | IHttpMockScenario in DI | Matching requests never reach the network |
JsonContentBuilder sends body.ToString(), so pass an already serialized string.
var json = JsonSerializer.Serialize(new { name = "Alice", age = 30 });
var response = await factory.CreateOrGet("api")
.SendAsync("https://api.example.com/users", HttpMethod.Post,
new JsonContentBuilder(), json);
response.EnsureSuccessStatusCode();
var created = await response.Content.ReadFromJsonAsync<User>();
var helper = factory.CreateOrGet("api")
.setHeadersAndBearerAuthenticationSync(
new Dictionary<string, string> { ["Accept"] = "application/json" },
new httpsClientHelper.httpClientAuthenticationBearer(token))
.addRetryCondition(r => r.StatusCode == HttpStatusCode.TooManyRequests || (int)r.StatusCode >= 500,
retryCount: 3, backoffFactor: 2) // waits 2s, 4s, 8s
.addTimeout(TimeSpan.FromSeconds(5));
var response = await helper.SendAsync("https://api.example.com/orders", HttpMethod.Get);
{
"HttpClientOptions": [
{
"Name": "api",
"UseCompression": true,
"RateLimitOptions": {
"IsEnabled": true, "PermitLimit": 10, "Window": "00:00:01",
"SegmentsPerWindow": 2, "QueueLimit": 0, "AutoReplenishment": true
}
}
]
}
They solve different problems; this is what you get out of the box.
| HttpHelper | Refit | Flurl | Plain HttpClient | |
|---|---|---|---|---|
| Style | Named client + fluent helper | Typed interface | Fluent URL builder | Manual |
| Retry / backoff | Built in (Polly) | Add Polly / resilience handler | Add Polly or custom handler | Add Polly / resilience handler |
| Timeout | 408 response | TaskCanceledException | FlurlHttpTimeoutException | TaskCanceledException |
| Transport errors | 502 / 500 response | Exception | Exception | Exception |
| Rate limiting | Built in (config) | — | — | Add RateLimiter handler |
| JSON (de)serialization | You serialize | Automatic, typed | Automatic | System.Net.Http.Json |
| Compile-time typed API | No | Yes | No | No |
| Mocking in tests | Mock scenarios via DI | Mock handler | HttpTest | Mock handler |
Pick HttpHelper for a handful of third-party APIs where you want retry, timeout, rate limit and logging in one place. Pick Refit for a large, stable API you want typed. Pick Flurl for ad-hoc fluent calls with automatic JSON. They can coexist.