Syntrony.HttpClient
2.0.1
dotnet add package Syntrony.HttpClient --version 2.0.1
NuGet\Install-Package Syntrony.HttpClient -Version 2.0.1
<PackageReference Include="Syntrony.HttpClient" Version="2.0.1" />
<PackageVersion Include="Syntrony.HttpClient" Version="2.0.1" />
<PackageReference Include="Syntrony.HttpClient" />
paket add Syntrony.HttpClient --version 2.0.1
#r "nuget: Syntrony.HttpClient, 2.0.1"
#:package Syntrony.HttpClient@2.0.1
#addin nuget:?package=Syntrony.HttpClient&version=2.0.1
#tool nuget:?package=Syntrony.HttpClient&version=2.0.1
Syntrony.HttpClient
A reusable HTTP client for Syntrony's .NET services. Centralizes HttpClient configuration via
IHttpClientFactory, exposes typed methods that wrap every response in a Result<T> (no
exceptions thrown for non-successful HTTP responses), supports multipart/form-data uploads and
streaming/binary downloads, and offers optional resilience policies with Polly. The package has no
dependency on ASP.NET Core's shared framework — it works from any project type (console, Worker
Service, isolated Azure Function, etc.).
Result<T> is Syntrony.Results.Result<T>, defined in the Syntrony core package and pulled in
automatically as a transitive dependency. It's the same contract type a Syntrony service returns,
so a client and the API it talks to share one shape — depending on it doesn't pull in ASP.NET
Core: the core package has no FrameworkReference.
Installation
dotnet add package Syntrony.HttpClient
Breaking change in 2.0.0:
Result<T>moved fromSyntrony.HttpClient.WrappertoSyntrony.Results(now owned by theSyntronycore package instead of living in this one). The JSON on the wire is identical andAddSyntronyHttpClientdidn't change — update theusingand recompile.
Basic usage
using Syntrony.HttpClient;
using Syntrony.HttpClient.Configuration;
builder.Services.AddSyntronyHttpClient(
client: "PaymentsApiClient",
configureOptions: options =>
{
options.BaseAddress = builder.Configuration["PAYMENTS_API_URL"];
options.Timeout = int.Parse(builder.Configuration["PAYMENTS_API_TIMEOUT"] ?? "100");
options.DefaultHeaders = new Dictionary<string, string>
{
["Accept"] = "application/json"
};
});
The client name ("PaymentsApiClient" in the example) is what IHttpClientFactory uses
internally — if the project only consumes a single external API, it can be omitted and the
ServiceExtension.DefaultClientName default is used instead.
Configuration via environment variables
The package does not read environment variables directly. It's the consuming project's
responsibility to resolve them (ASP.NET Core already loads environment variables as a
configuration provider by default) and pass them into the configureOptions delegate:
builder.Services.AddSyntronyHttpClient(options =>
{
builder.Configuration.GetSection("PaymentsApi").Bind(options);
});
Consuming the client
using Syntrony.Results;
public class PaymentsService(IHttpSyntronyClient httpClient)
{
public async Task<Result<PaymentDto>> GetPaymentAsync(string id)
{
var headers = new Dictionary<string, string>
{
["Authorization"] = $"Bearer {token}"
};
return await httpClient.GetAsync<PaymentDto>($"payments/{id}", headers);
}
}
All methods (GetAsync, PostAsync, PutAsync, PatchAsync, DeleteAsync) return a
Result<TResponse>:
var result = await httpClient.GetAsync<PaymentDto>("payments/123", headers: null);
if (result.IsSuccess)
return result.Value;
// result.Error contains the error response body
logger.LogWarning("Payments API failed: {Error}", result.Error);
File uploads (multipart/form-data)
Simple cases (plain fields plus byte[]/Stream properties) can go straight through PostAsync
with contentType: "multipart/form-data" — the library builds the content via reflection:
public class UploadRequest
{
public string Description { get; set; }
public byte[] File { get; set; }
}
await httpClient.PostAsync<UploadRequest, UploadResultDto>(
"documents",
new UploadRequest { Description = "Invoice", File = fileBytes },
headers: null,
contentType: "multipart/form-data");
For explicit field/file names or multiple files under the same field, use MultipartFormBuilder
and pass the resulting HttpContent directly (it's passed through as-is, ignoring contentType):
using Syntrony.HttpClient.Content;
var content = new MultipartFormBuilder()
.AddField("description", "Invoice")
.AddFile("file", fileStream, "invoice.pdf")
.Build();
await httpClient.PostAsync<HttpContent, UploadResultDto>("documents", content, headers: null);
Note: the package no longer references ASP.NET Core's
Microsoft.AspNetCore.Appshared framework, so it doesn't acceptIFormFiledirectly. In a web project, convert it first:formFile.OpenReadStream()and pass thatStreamtoMultipartFormBuilder.AddFile.
Downloading files / streaming responses
For large or binary payloads (PDFs, images, etc.), DownloadAsync uses
HttpCompletionOption.ResponseHeadersRead so the body is never fully buffered in memory. The
caller owns the returned Stream and must dispose it — disposing it releases the underlying
connection:
var result = await httpClient.DownloadAsync($"documents/{id}/file");
if (result.IsSuccess)
{
await using var stream = result.Value!;
await using var fileOnDisk = File.Create("invoice.pdf");
await stream.CopyToAsync(fileOnDisk);
}
For custom deserialization straight from the response stream (streaming JSON, CSV, protobuf, etc.)
without buffering it as a string first, use the GetAsync overload that takes a
Func<Stream, CancellationToken, Task<TResponse>>:
var result = await httpClient.GetAsync<ReportDto>(
"reports/large",
(stream, ct) => JsonSerializer.DeserializeAsync<ReportDto>(stream, cancellationToken: ct).AsTask());
Resilience (Polly)
Optional retries with exponential backoff (including client-side timeouts) and circuit breaker,
chained on the IHttpClientBuilder returned by AddSyntronyHttpClient:
builder.Services
.AddSyntronyHttpClient(options => options.BaseAddress = "https://api.example.com")
.AddSyntronyResilience(retryCount: 3, enableCircuitBreaker: true);
Use cases
- Consuming Syntrony's internal REST APIs (e.g.
evx-pl-sso-ms) with per-request auth headers. - Sending files via
multipart/form-data(byte[]/Stream, orMultipartFormBuilderfor explicit field/file names). - Downloading files or large/non-JSON payloads without buffering the full body in memory.
- HTTP calls with automatic retries on transient errors (5xx, timeouts,
HttpRequestException).
Known limitation (v1): the package supports a single Syntrony client per consuming project. Calling
AddSyntronyHttpClientmore than once with different names does not correctly isolate their options (SyntronyHttpClientOptionsis resolved without a name). Support for multiple named clients is planned for v2.
| 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 was computed. 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 was computed. 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. |
-
net8.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.2)
- Microsoft.Extensions.Http.Polly (>= 8.0.11)
- Microsoft.Extensions.Logging.Abstractions (>= 8.0.3)
- Syntrony (>= 1.2.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.