CSharpEssentials.HttpHelper
5.4.0
dotnet add package CSharpEssentials.HttpHelper --version 5.4.0
NuGet\Install-Package CSharpEssentials.HttpHelper -Version 5.4.0
<PackageReference Include="CSharpEssentials.HttpHelper" Version="5.4.0" />
<PackageVersion Include="CSharpEssentials.HttpHelper" Version="5.4.0" />
<PackageReference Include="CSharpEssentials.HttpHelper" />
paket add CSharpEssentials.HttpHelper --version 5.4.0
#r "nuget: CSharpEssentials.HttpHelper, 5.4.0"
#:package CSharpEssentials.HttpHelper@5.4.0
#addin nuget:?package=CSharpEssentials.HttpHelper&version=5.4.0
#tool nuget:?package=CSharpEssentials.HttpHelper&version=5.4.0
CSharpEssentials.HttpHelper
Targets: net8.0 · net9.0 · net10.0
CSharpEssentials.HttpHelper wraps IHttpClientFactory with a small fluent API: named clients configured in appsettings.json, retry (Polly), per-call timeout, rate limiting, per-request callbacks and built-in mocking for tests. Transport failures come back as a HttpResponseMessage (408 / 502 / 500) instead of exceptions.
- Quick Start
- Features
- Examples — JSON, form, headers & auth, retry, timeout, callbacks
- Comparison
- Documentation & Demo
🚀 Quick Start
1. Install
dotnet add package CSharpEssentials.HttpHelper
2. Configure a named client in appsettings.json:
{
"HttpClientOptions": [
{ "Name": "github", "UseCompression": true }
]
}
3. Register in Program.cs:
builder.Services.AddHttpClients(builder.Configuration);
4. Call — inject IhttpsClientHelperFactory, then CreateOrGet(name):
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);
}
}
CreateOrGet returns the same helper for the same name, so its settings (retry, timeout, headers, callbacks) stick across calls.
✨ Features
| Feature | How | Notes |
|---|---|---|
| Named clients from config | HttpClientOptions array + AddHttpClients(configuration) |
Built on IHttpClientFactory (pooled handlers, no socket exhaustion) |
| Retry with backoff | addRetryCondition(predicate, retryCount, backoffFactor) |
Delay before retry n = backoffFactorⁿ seconds. Adds an X-Retry-Attempt header |
| Timeout | addTimeout(TimeSpan) |
Expiry returns 408 {"error":"timeout",...}; defaults to the client's HttpClient.Timeout |
| Rate limiting | RateLimitOptions in config (sliding window) |
Over the limit returns 429 {"error":"rate_limit_exceeded"} |
| Request callbacks | AddRequestAction(...) / factory.AddActionOnRequest(...) |
Per client or global; receives request, response, retry attempt, elapsed |
| Body builders | JsonContentBuilder, XmlContentBuilder, StringContentBuilder(mediaType) (any text media type), FormUrlEncodedContentBuilder, NoBodyContentBuilder |
Implement IContentBuilder for anything else |
| Headers & auth | addHeaders, setHeadersAndBearerAuthenticationSync, setHeadersAndBasicAuthenticationSync, per-call headers |
|
| Compression, proxy, client certificate | UseCompression, httpProxy, Certificate in config |
Configured on the pooled SocketsHttpHandler |
| Logging | Failures are logged through LoggerHelper | Companion package, same Serilog pipeline; without AddLoggerHelper it falls back to Serilog's static Log.Logger |
| Mocking | Register IHttpMockScenario instances in DI |
Matching requests never reach the network; see Testing |
No exceptions for transport failures. SendAsync returns:
| Situation | Result |
|---|---|
Timeout (addTimeout or HttpClient.Timeout) |
408 RequestTimeout |
| Rate limit exceeded | 429 TooManyRequests |
HttpRequestException (DNS, connection refused…) |
502 BadGateway |
| Any other exception | 500 InternalServerError |
Caller cancels the CancellationToken |
OperationCanceledException is thrown (standard .NET contract) |
📚 Examples
JSON body
JsonContentBuilder sends body.ToString() as application/json, so pass an already serialized string:
using System.Text.Json;
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>(); // System.Net.Http.Json
Form data
var form = new Dictionary<string, string> { ["grant_type"] = "client_credentials", ["scope"] = "read" };
var response = await factory.CreateOrGet("auth")
.SendAsync("https://auth.example.com/token", HttpMethod.Post,
new FormUrlEncodedContentBuilder(), form);
Fluent: headers, auth, retry, timeout
Every configuration method returns the helper, so settings chain:
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);
setHeaders…Syncreplaces the client's default headers. Per-request headers (theheaders:argument ofSendAsync) don't touch shared state — prefer them when the same named client is used concurrently.
Rate limiting and compression (config)
{
"HttpClientOptions": [
{
"Name": "api",
"UseCompression": true,
"RateLimitOptions": {
"IsEnabled": true,
"PermitLimit": 10,
"Window": "00:00:01",
"SegmentsPerWindow": 2,
"QueueLimit": 0,
"AutoReplenishment": true
}
}
]
}
Per-request callbacks
factory.CreateOrGet("api").AddRequestAction(async (request, response, retryAttempt, elapsed) =>
{
Console.WriteLine($"{request.Method} {request.RequestUri} -> {(int)response.StatusCode} (retry #{retryAttempt})");
await Task.CompletedTask;
});
// all clients:
factory.AddActionOnRequest((req, res, attempt, elapsed) => Task.CompletedTask);
A throwing callback is logged and ignored — it never fails the HTTP call.
Logging and correlation
Opt-in, off by default, independent of AddRequestAction. One structured event per attempt (retries included) goes through the LoggerHelper pipeline, so your routes, sinks and masking apply.
{
"HttpHelperLogging": {
"LogRequests": [ "*" ],
"CorrelationIdHeader": "X-Correlation-ID"
}
}
LogRequests:"*"= every named client, or a list of client names (["Order", "Login"]). Empty or missing = off.CorrelationIdHeader: header added to outgoing requests. Null or missing = off.- Both are read through
IOptionsMonitor: editingappsettings.jsonturns them on/off without a restart.
Event: HTTP {Method} {RequestPath} responded {StatusCode} in {ElapsedMs} ms (attempt {RetryAttempt}), with properties HttpClientName, Method, RequestPath (query string removed), StatusCode, ElapsedMs, RetryAttempt and CorrelationId. Levels: Information for 2xx/3xx, Warning for 4xx, Error for 5xx, 408 and transport exceptions (caller cancellation is Warning; a timeout from addTimeout is Error; with a plain HttpClient or a hand-built pipeline the two cannot be told apart and log Warning). Headers and bodies are never logged.
Correlation id: the current Activity TraceId if present, otherwise a new GUID ("N" format). It is the same on every retry of one call, and a header you set yourself is never overwritten. .NET already sends the W3C traceparent header when an Activity exists; this header is for services that don't use OpenTelemetry. The id is forwarded to the upstream host, so it is opt-in. The generated id covers one call and its retries: to share one id across several calls (e.g. login, order, payment), set the header yourself on each call, as the Playground order scenario does.
IHttpClientFactory already logs System.Net.Http.HttpClient.<name>.* at Information (no retry attempt, no correlation id). To avoid duplicates, raise that category to Warning:
{ "Logging": { "LogLevel": { "System.Net.Http.HttpClient": "Warning" } } }
Hand-built handler (no IHttpClientFactory): the same events come from the public constructor, with an optional Serilog.ILogger target.
var handler = new HttpClientHandlerLogging(events, null, "playground", loggingOptionsMonitor, logger) { InnerHandler = new SocketsHttpHandler() };
clientName is matched ordinal against LogRequests. With logger omitted (null) events go through the LoggerHelper pipeline / Log.Logger; with an injected logger they do not carry ApplicationName / Action = "HttpHelper".
🧪 Testing with mock scenarios
Register a scenario in DI; requests it matches are answered by your factories (round-robin) and never hit the network:
services.AddSingleton<IHttpMockScenario>(new HttpMockScenario(
match: r => r.RequestUri?.Host == "mock.local",
responseFactory: new List<Func<Task<HttpResponseMessage>>>
{
() => Task.FromResult(new HttpResponseMessage(HttpStatusCode.TooManyRequests)),
() => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)),
}));
services.AddHttpClients(configuration);
With addRetryCondition(r => r.StatusCode == HttpStatusCode.TooManyRequests, 3, 0) the first call returns 429, the retry returns 200. Last matching scenario wins; a factory may also throw HttpRequestException to simulate a network failure.
Mocks honour timeout and caller cancellation. Use the (request, ct) => ... overload to observe the token:
services.AddSingleton<IHttpMockScenario>(new HttpMockScenario(
match: r => r.RequestUri?.Host == "slow.local",
responseFactory: new List<Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>>>
{
async (request, ct) => {
await Task.Delay(TimeSpan.FromSeconds(30), ct);
return new HttpResponseMessage(HttpStatusCode.OK);
},
}));
With addTimeout(TimeSpan.FromSeconds(1)) the call returns 408; cancelling the caller's token throws OperationCanceledException. Custom IHttpMockScenario implementations get the same request/token factories by exposing RequestResponseFactory (default interface member, empty by default; non-empty entries take precedence over ResponseFactory). Declare it public or explicit:
class SlowScenario : IHttpMockScenario
{
public Func<HttpRequestMessage, bool> Match { get; } = r => r.RequestUri?.Host == "slow.local";
public IReadOnlyList<Func<Task<HttpResponseMessage>>> ResponseFactory => [];
public IReadOnlyList<Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>>> RequestResponseFactory { get; } =
[async (request, ct) => { await Task.Delay(TimeSpan.FromSeconds(30), ct); return new HttpResponseMessage(HttpStatusCode.OK); }];
}
⚖️ Comparison with Refit, Flurl and plain HttpClient
They solve different problems; this table is about what you get out of the box.
| HttpHelper | Refit | Flurl | Plain HttpClient |
|
|---|---|---|---|---|
| Style | Named client + fluent helper | Typed interface ([Get("/users/{id}")]) |
Fluent URL builder | Manual |
| Setup | HttpClientOptions in appsettings |
Interface + AddRefitClient<T>() |
None (static Url/FlurlClient) |
AddHttpClient() |
Built on IHttpClientFactory |
Yes | Yes | Own client cache | Yes |
| Retry / backoff | Built in (Polly) | Add Polly / resilience handler | Add Polly or custom handler | Add Polly / resilience handler |
| Timeout → response | 408 response | TaskCanceledException |
FlurlHttpTimeoutException |
TaskCanceledException |
| Transport errors | Returned as 502 / 500 response | Exception (ApiException) |
Exception (FlurlHttpException) |
Exception |
| Rate limiting | Built in (config) | — | — | Add RateLimiter handler |
| Logging of calls | Built in (LoggerHelper) | ILogger / handler |
Event handlers | ILogger / handler |
| JSON (de)serialization | You serialize; read with ReadFromJsonAsync |
Automatic, typed | Automatic (PostJsonAsync, ReceiveJson<T>) |
System.Net.Http.Json |
| Compile-time typed API | No | Yes | No | No |
| Mocking in tests | Mock scenarios via DI | Mock HttpMessageHandler |
HttpTest |
Mock HttpMessageHandler |
Pick HttpHelper when you call a handful of third-party APIs and want retry, timeout, rate limit and logging configured in one place, with failures as plain status codes. Pick Refit for a large, stable API surface you want typed. Pick Flurl for ad-hoc fluent calls with automatic JSON. They can coexist in one app.
📖 Documentation & Demo
- HttpHelper page — Quick Start, examples, comparison
- Full site
🤝 Contributing
Contributions, issues, and feature requests are welcome! Feel free to open a pull request or issue.
📜 License
Distributed under the MIT License. See LICENSE for more information.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- CSharpEssentials.LoggerHelper (>= 5.4.0)
- Polly (>= 8.5.2)
- Serilog (>= 4.2.0)
-
net8.0
- CSharpEssentials.LoggerHelper (>= 5.4.0)
- Polly (>= 8.5.2)
- Serilog (>= 4.2.0)
-
net9.0
- CSharpEssentials.LoggerHelper (>= 5.4.0)
- Polly (>= 8.5.2)
- Serilog (>= 4.2.0)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on CSharpEssentials.HttpHelper:
| Package | Downloads |
|---|---|
|
CSharpEssentials.HangFireHelper
A library to help you to use HangFire |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 5.4.0 | 83 | 10/16/2026 |
| 5.3.3 | 106 | 10/16/2026 |
| 5.3.2 | 93 | 10/11/2026 |
| 5.3.1 | 88 | 10/10/2026 |
| 5.3.0 | 86 | 10/10/2026 |
| 5.2.11 | 87 | 10/9/2026 |
| 5.2.10 | 89 | 10/7/2026 |
| 5.2.9 | 82 | 10/6/2026 |
| 5.2.8 | 87 | 10/6/2026 |
| 5.2.7 | 82 | 10/6/2026 |
| 5.2.6 | 149 | 8/31/2026 |
| 5.2.4.1 | 113 | 8/27/2026 |
| 5.2.4 | 119 | 8/25/2026 |
| 5.2.3 | 120 | 8/25/2026 |
| 5.2.2.9 | 338 | 7/11/2026 |
| 5.2.2.8 | 137 | 7/11/2026 |
| 5.2.2.7 | 128 | 7/10/2026 |
| 5.2.2.6 | 123 | 7/6/2026 |
| 5.2.2.5 | 145 | 7/5/2026 |
| 5.2.2.4 | 132 | 7/5/2026 |