Linger.HttpClient.Standard 2.0.0-preview.1

This is a prerelease version of Linger.HttpClient.Standard.
dotnet add package Linger.HttpClient.Standard --version 2.0.0-preview.1
                    
NuGet\Install-Package Linger.HttpClient.Standard -Version 2.0.0-preview.1
                    
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="Linger.HttpClient.Standard" Version="2.0.0-preview.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Linger.HttpClient.Standard" Version="2.0.0-preview.1" />
                    
Directory.Packages.props
<PackageReference Include="Linger.HttpClient.Standard" />
                    
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 Linger.HttpClient.Standard --version 2.0.0-preview.1
                    
#r "nuget: Linger.HttpClient.Standard, 2.0.0-preview.1"
                    
#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 Linger.HttpClient.Standard@2.0.0-preview.1
                    
#: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=Linger.HttpClient.Standard&version=2.0.0-preview.1&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=Linger.HttpClient.Standard&version=2.0.0-preview.1&prerelease
                    
Install as a Cake Tool

Linger.HttpClient.Standard

The standard IHttpClient implementation based on System.Net.Http.HttpClient.

Features

  • HttpClientFactory connection management
  • JSON, form, and custom HttpContent requests
  • RFC 7807 ProblemDetails and legacy error-array parsing
  • Per-request headers without shared mutable authentication state
  • Raw HttpResponseMessage access and long-lived streaming
  • Streaming uploads based on StreamContent
  • Temporary-file commit semantics for streaming downloads
  • User-cancellation propagation and HttpClient.Timeout handling

Installation and registration

dotnet add package Linger.HttpClient.Standard
services.AddHttpClient<IHttpClient, StandardHttpClient>(client =>
{
    client.BaseAddress = new Uri("https://api.example.com/");
    client.Timeout = TimeSpan.FromSeconds(30);
    client.DefaultRequestHeaders.Accept.ParseAdd("application/json");
});

WinForms and console applications can construct the client directly from a base URL. In this mode, StandardHttpClient owns the underlying client; reuse the instance for the application lifetime and dispose it during shutdown:

using var client = new StandardHttpClient("https://api.example.com/");

Configure the underlying client during construction when you need a timeout, fixed headers, or Accept-Language:

using var client = new StandardHttpClient(
    "https://api.example.com/",
    configureClient: httpClient =>
    {
        httpClient.Timeout = TimeSpan.FromSeconds(30);
        httpClient.DefaultRequestHeaders.AcceptLanguage.ParseAdd("zh-CN");
    });

Supply a caller-owned HttpClient when custom handlers, authentication, certificates, or proxies are required. Disposing StandardHttpClient does not dispose the external client. You can also let URL mode own a custom handler:

using var client = new StandardHttpClient(
    "https://api.example.com/",
    accessTokenHandler);
using var httpClient = new HttpClient
{
    BaseAddress = new Uri("https://api.example.com/")
};
using var client = new StandardHttpClient(httpClient, logger);

Typed calls

var getResult = await client.GetAsync<User>(
    "users/42",
    queryParams: new { IncludeRoles = true },
    cancellationToken: cancellationToken);

var postResult = await client.PostAsync<User>(
    "users",
    new CreateUserRequest("Ada"),
    cancellationToken: cancellationToken);

var patchResult = await client.CallApi<User>(
    "users/42",
    HttpMethod.Patch,
    new UpdateUserRequest("Grace"),
    cancellationToken: cancellationToken);

Query-object property metadata is cached. Collection properties produce repeated keys, while numbers and dates use invariant formatting.

Authentication and headers

Fixed authorization header

When one client instance represents one user and every request uses the same token, configure the default authorization header when creating the client:

var accessToken = "eyJ...";
using var client = new StandardHttpClient(
    "https://api.example.com/",
    configureClient: httpClient =>
    {
        httpClient.DefaultRequestHeaders.Authorization =
            new AuthenticationHeaderValue("Bearer", accessToken);
    });

Reuse the client for the application lifetime instead of creating one per request. Use a DelegatingHandler for token refresh or other dynamic behavior rather than mutating shared DefaultRequestHeaders while requests are in flight.

Using DelegatingHandler

A DelegatingHandler can attach authentication, culture, or other common request information before sending. The handler modifies only the current HttpRequestMessage:

public sealed class AccessTokenHandler(string accessToken) : DelegatingHandler
{
    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        request.Headers.Authorization =
            new AuthenticationHeaderValue("Bearer", accessToken);
        return base.SendAsync(request, cancellationToken);
    }
}

Automatic refresh normally retains the complete server Token, uses SemaphoreSlim to prevent duplicate refreshes under concurrency, and calls the refresh endpoint through a separate client without the authentication handler. See the Linger.HttpClient.WinForms example for the complete client implementation and lifecycle; see the Linger.AspNetCore.Jwt README for the server endpoints.

Dynamic or multi-user authentication

When a shared client can send requests for different users, pass dynamic authentication per request instead of mutating shared DefaultRequestHeaders.Authorization:

var headers = new Dictionary<string, string>
{
    ["Authorization"] = $"Bearer {accessToken}",
    ["X-Correlation-Id"] = correlationId
};

var result = await client.GetAsync<User>(
    "users/me",
    headers: headers,
    cancellationToken: cancellationToken);

Fixed service credentials can also be configured during AddHttpClient registration. Use a default authorization header for a fixed token on a dedicated instance, and use the headers parameter when tokens can differ between requests.

The client does not append culture. Add it explicitly as a query parameter or through a custom DelegatingHandler when required.

File uploads

var fileStream = File.OpenRead("report.pdf");
var result = await client.UploadFileAsync<UploadResponse>(
    "files",
    HttpMethod.Post,
    fileStream,
    "report.pdf",
    formData: new Dictionary<string, string>
    {
        ["category"] = "report"
    },
    cancellationToken: cancellationToken);

Uploads use StreamContent and do not copy the complete file into memory. The input stream is disposed when the request completes.

File downloads

var progress = new Progress<(long downloaded, long? total)>(value =>
{
    Console.WriteLine($"{value.downloaded}/{value.total}");
});

var result = await client.DownloadToFileAsync(
    "files/report.pdf",
    "report.pdf",
    progress: progress,
    cancellationToken: cancellationToken);

The download flow uses ResponseHeadersRead, writes a same-directory temporary file, and replaces the destination only after download and flush succeed. Cancellation or failure removes the temporary file and preserves an existing destination.

Raw responses and long-lived streams

Use SendAsync when you need response headers, SSE, or incremental content processing. It reuses the StandardHttpClient base address, default headers, and DelegatingHandler pipeline, including authentication refresh:

using var response = await client.SendAsync(
    "events",
    HttpMethod.Get,
    cancellationToken: cancellationToken);

response.EnsureSuccessStatusCode();
using var stream = await response.Content.ReadAsStreamAsync();
await ProcessStreamAsync(stream, cancellationToken);

The caller owns the returned HttpResponseMessage. Raw calls do not parse unsuccessful responses or convert network and timeout exceptions into ApiResult.

Error handling

var result = await client.GetAsync<User>("users/42", cancellationToken: cancellationToken);

if (!result.IsSuccess)
{
    Console.WriteLine($"HTTP: {result.StatusCode}");
    Console.WriteLine(result.ErrorMsg);

    foreach (var error in result.Errors)
    {
        Console.WriteLine($"{error.Code}: {error.Message}");
    }
}

Parsing order:

  1. ProblemDetails (application/problem+json or standard problem fields)
  2. Legacy IEnumerable<Error> arrays
  3. Status-code message and raw response text

An arbitrary JSON object is not treated as ProblemDetails merely because it can be deserialized. Full exception details go to logs; ErrorMsg never contains Exception.ToString().

Cancellation and timeout

User cancellation preserves standard .NET semantics and throws OperationCanceledException. Timeout is configured through HttpClient.Timeout and returns a failed ApiResult. Use GetWithTimeoutAsync for a different one-off GET timeout; for other requests, pass a token from a caller-owned CancellationTokenSource configured with CancelAfter.

Custom JSON and errors

Derive from StandardHttpClient and override GetRequestJsonOptions, GetResponseJsonOptions, or GetErrorMessageAsync when a server requires a custom format.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  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. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 is compatible.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2.0.0-preview.1 34 8/29/2026
1.6.4 101 8/16/2026
1.6.3 100 8/5/2026
1.6.2 116 8/2/2026
1.6.0 104 7/25/2026
1.5.5 108 7/23/2026
1.5.4-preview 87 7/21/2026
1.5.3-preview 95 7/20/2026
1.5.2-preview 96 7/19/2026
1.5.1-preview 105 7/15/2026
1.5.0-preview 91 7/14/2026
1.4.4-preview 104 6/16/2026
1.4.3-preview 101 6/15/2026
1.4.2 114 5/20/2026
1.4.1-preview 108 5/12/2026
1.4.0 108 5/6/2026
1.3.3-preview 96 5/5/2026
1.3.2-preview 110 4/29/2026
1.3.1-preview 101 4/28/2026
1.3.0-preview 99 4/27/2026
Loading failed