Linger.HttpClient.Standard
2.0.0-preview.1
dotnet add package Linger.HttpClient.Standard --version 2.0.0-preview.1
NuGet\Install-Package Linger.HttpClient.Standard -Version 2.0.0-preview.1
<PackageReference Include="Linger.HttpClient.Standard" Version="2.0.0-preview.1" />
<PackageVersion Include="Linger.HttpClient.Standard" Version="2.0.0-preview.1" />
<PackageReference Include="Linger.HttpClient.Standard" />
paket add Linger.HttpClient.Standard --version 2.0.0-preview.1
#r "nuget: Linger.HttpClient.Standard, 2.0.0-preview.1"
#:package Linger.HttpClient.Standard@2.0.0-preview.1
#addin nuget:?package=Linger.HttpClient.Standard&version=2.0.0-preview.1&prerelease
#tool nuget:?package=Linger.HttpClient.Standard&version=2.0.0-preview.1&prerelease
Linger.HttpClient.Standard
The standard IHttpClient implementation based on System.Net.Http.HttpClient.
Features
HttpClientFactoryconnection management- JSON, form, and custom
HttpContentrequests - RFC 7807 ProblemDetails and legacy error-array parsing
- Per-request headers without shared mutable authentication state
- Raw
HttpResponseMessageaccess and long-lived streaming - Streaming uploads based on
StreamContent - Temporary-file commit semantics for streaming downloads
- User-cancellation propagation and
HttpClient.Timeouthandling
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:
- ProblemDetails (
application/problem+jsonor standard problem fields) - Legacy
IEnumerable<Error>arrays - 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 | Versions 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. |
-
.NETFramework 4.7.2
- Linger.HttpClient.Contracts (>= 2.0.0-preview.1)
- Linger.Json (>= 2.0.0-preview.1)
- Linger.Utils (>= 2.0.0-preview.1)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.10)
-
.NETStandard 2.0
- Linger.HttpClient.Contracts (>= 2.0.0-preview.1)
- Linger.Json (>= 2.0.0-preview.1)
- Linger.Utils (>= 2.0.0-preview.1)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.10)
-
net10.0
- Linger.HttpClient.Contracts (>= 2.0.0-preview.1)
- Linger.Json (>= 2.0.0-preview.1)
- Linger.Utils (>= 2.0.0-preview.1)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.10)
-
net8.0
- Linger.HttpClient.Contracts (>= 2.0.0-preview.1)
- Linger.Json (>= 2.0.0-preview.1)
- Linger.Utils (>= 2.0.0-preview.1)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.10)
-
net9.0
- Linger.HttpClient.Contracts (>= 2.0.0-preview.1)
- Linger.Json (>= 2.0.0-preview.1)
- Linger.Utils (>= 2.0.0-preview.1)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.10)
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 |