SyntaxCircus.Http.Resilience 0.1.5

There is a newer version of this package available.
See the version list below for details.
dotnet add package SyntaxCircus.Http.Resilience --version 0.1.5
                    
NuGet\Install-Package SyntaxCircus.Http.Resilience -Version 0.1.5
                    
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="SyntaxCircus.Http.Resilience" Version="0.1.5" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="SyntaxCircus.Http.Resilience" Version="0.1.5" />
                    
Directory.Packages.props
<PackageReference Include="SyntaxCircus.Http.Resilience" />
                    
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 SyntaxCircus.Http.Resilience --version 0.1.5
                    
#r "nuget: SyntaxCircus.Http.Resilience, 0.1.5"
                    
#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 SyntaxCircus.Http.Resilience@0.1.5
                    
#: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=SyntaxCircus.Http.Resilience&version=0.1.5
                    
Install as a Cake Addin
#tool nuget:?package=SyntaxCircus.Http.Resilience&version=0.1.5
                    
Install as a Cake Tool

SyntaxCircus.Http.Resilience

Build NuGet License: MIT

A typed API client base, a Polly-based resilient HttpClient registration helper, and a generic cached-token provider — the pieces that keep getting rewritten every time a product calls another API.

No support guaranteed. Published as-is and maintained on a best-effort basis. Issues and PRs are welcome, but there's no SLA — fork it or vendor what you need if that's not enough.

ApiClientBase

public sealed class WidgetApiClient(HttpClient httpClient) : ApiClientBase(httpClient)
{
    public Task<Widget?> GetWidgetAsync(string id, CancellationToken ct) => GetAsync<Widget>($"widgets/{id}", ct);

    public Task CreateWidgetAsync(Widget widget, CancellationToken ct) => PostAsync("widgets", widget, ct);
}

JSON GetAsync/GetWithETagAsync (conditional GET with a per-URL ETag cache)/PostAsync/PutAsync (with If-Match from the cached ETag)/DeleteAsync, and centralized error handling: a non-success response is translated into a ProblemDetailsException (StatusCode, Type, Title, Errors) when the body is an RFC 7807 ProblemDetails payload. Bearer-token attachment is left to the caller: for app-wide/singleton-safe auth (e.g. a CachedTokenProvider-backed client-credentials token), register a DelegatingHandler on the typed client via AddHttpMessageHandler<T>(); for auth scoped to something only available in the same DI scope as the typed client itself (e.g. a per-user/per-session token in a web app), override OnRequestSendingAsync in a derived class instead — AddHttpMessageHandler-registered handlers are resolved from a pooled, periodically-rotated handler scope, not the caller's ambient DI scope, so they must only depend on singleton-safe services.

public sealed class AuthenticatedWidgetApiClient(HttpClient httpClient, IMyScopedTokenAccessor tokenAccessor)
    : ApiClientBase(httpClient)
{
    protected override async Task OnRequestSendingAsync(HttpRequestMessage request, CancellationToken ct)
    {
        var token = await tokenAccessor.GetTokenAsync(ct);
        if (!string.IsNullOrWhiteSpace(token))
        {
            request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
        }
    }
}

GetWithETagAsync takes an optional useConditionalRequest parameter (default true, preserving the conditional-GET/304 behavior above). Pass false for a "load for edit" call that should always return a fresh body — no If-None-Match is sent and a 304 can never happen, but the response's ETag is still cached for a subsequent PutAsync/DeleteAsync on the same URL. Useful for long-lived typed-client instances (e.g. one per web-app session/circuit) where a caller reads the same URL more than once and always wants the current value, not a cached-away 304.

Need to send something the JSON verb helpers don't fit — multipart form content, a binary download, custom headers? Use SendAsync(HttpRequestMessage, ct) / ReadJsonAsync<T>(HttpResponseMessage, ct), protected members that run the same OnRequestSendingAsync/OnResponseReceivedAsync hooks, ProblemDetails translation, and ETag caching as the verb helpers, while leaving you in control of the request/response shape:

public async Task<Widget> UploadAsync(Stream file, CancellationToken ct)
{
    using var request = new HttpRequestMessage(HttpMethod.Post, "widgets/upload") { Content = new StreamContent(file) };
    using var response = await SendAsync(request, ct);
    return (await ReadJsonAsync<Widget>(response, ct))!;
}

AddResilientHttpClient

builder.Services.AddResilientHttpClient(
    "widgets-api",
    client => client.BaseAddress = new Uri("https://widgets.example.com"),
    retryCount: 3)
    .AddTypedClient<WidgetApiClient>();

Wraps the named HttpClient in a Polly retry (exponential backoff + jitter) and circuit-breaker pipeline, retrying transient errors and 429/5xx. Pass aiMode: true for AI/LLM provider clients where a 429 means "back off on purpose" rather than "something's broken" — it's excluded from retry/circuit-breaking in that mode.

CachedTokenProvider

var tokenProvider = new CachedTokenProvider(async ct =>
{
    var token = await FetchClientCredentialsTokenAsync(ct);
    return new CachedToken(token.AccessToken, DateTimeOffset.UtcNow.AddSeconds(token.ExpiresIn));
});

var accessToken = await tokenProvider.GetTokenAsync();

A semaphore-guarded token cache, refreshed under lock once it's within expirySkew (default 60s) of expiry. The acquisition delegate is entirely up to you — client-credentials grant, a custom token endpoint, whatever your worker-to-API auth needs.

Contributing

Issues and pull requests are welcome:

  • Keep changes focused, with a clear description of the behavior change.
  • Match the existing code style (see .editorconfig).
  • Call out any breaking changes to the public API in your PR description.

License

MIT — see LICENSE.txt.

Product Compatible and additional computed target framework versions.
.NET 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 (4)

Showing the top 4 NuGet packages that depend on SyntaxCircus.Http.Resilience:

Package Downloads
SyntaxCircus.Cmsify.Client

Typed .NET client for connecting to and managing the Cmsify headless CMS API.

SyntaxCircus.Cmsify.Client.DistributedCaching

Provider-neutral IDistributedCache add-on for the SyntaxCircus Cmsify .NET client.

SyntaxCircus.AI.Providers

Low-level typed HTTP clients for the Anthropic Messages API and the Gemini generateContent API: request/response DTOs, rate-limit handling, and Retry-After parsing. Not a unified provider abstraction — just the plumbing both APIs otherwise get reimplemented for.

SyntaxCircus.Cmsify.Components

Reusable, restylable Blazor components for editing and managing Cmsify content: field editors, a composed content edit form, and a content list view, with optional SDK-backed smart wrappers.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.2.1 140 9/4/2026
0.2.0-cmsify.1 310 8/31/2026
0.1.6 283 8/18/2026
0.1.5 94 8/18/2026
0.1.4 88 8/18/2026
0.1.3 106 8/18/2026
0.1.2 105 8/18/2026
0.1.1 106 8/16/2026