CSharpEssentials.HttpHelper 5.4.0

dotnet add package CSharpEssentials.HttpHelper --version 5.4.0
                    
NuGet\Install-Package CSharpEssentials.HttpHelper -Version 5.4.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="CSharpEssentials.HttpHelper" Version="5.4.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="CSharpEssentials.HttpHelper" Version="5.4.0" />
                    
Directory.Packages.props
<PackageReference Include="CSharpEssentials.HttpHelper" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add CSharpEssentials.HttpHelper --version 5.4.0
                    
#r "nuget: CSharpEssentials.HttpHelper, 5.4.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package CSharpEssentials.HttpHelper@5.4.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=CSharpEssentials.HttpHelper&version=5.4.0
                    
Install as a Cake Addin
#tool nuget:?package=CSharpEssentials.HttpHelper&version=5.4.0
                    
Install as a Cake Tool

CSharpEssentials.HttpHelper

Frameworks CodeQL NuGet Downloads Last Commit GitHub Discussions Issues

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

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…Sync replaces the client's default headers. Per-request headers (the headers: argument of SendAsync) 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: editing appsettings.json turns 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

🤝 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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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
Loading failed