AutoHttpClient.Generator is an AOT-safe, compile-time typed HTTP client for .NET. Annotate an interface with [HttpClient], decorate methods with [Get], [Post], [Put], [Delete], or [Patch], and the generator emits a strongly-typed implementation plus DI registration at build time.
AutoDispatch.Generator— compile-time MediatR-style dispatchAutoMap.Generator— compile-time DTO/entity mappingSwevo.AutoAssert— fluent assertions without commercial licensing
- Compile-time generated clients — no dynamic proxy generation, no reflection-heavy dispatch layer
- AOT-safe request dispatch — generated C# calls
HttpClientdirectly with no reflection-based proxy, and passing an explicitJsonSerializerOptions(ideally backed by aJsonSerializerContext) to the constructor orAddAutoHttpClients(jsonOptions)produces zero trim/AOT warnings, measured against Refit insamples/AotBenchmark/BENCHMARK.md - Minimal ceremony — plain interfaces plus attributes, no hand-written wrappers
- DI-ready —
AddAutoHttpClients()registers every generated client forIServiceCollection - Strongly typed — route values, query parameters, headers, and JSON bodies all come from your method signature
- Refit (v16+) has also moved to a Roslyn source generator and ships an official Native AOT path (
RestService.ForGenerated<T>+ aJsonSerializerContext), so it's no longer purely runtime-proxy based. Both libraries can now produce warning-free trimmed/AOT builds when given explicit JSON type info. The real differences today are ergonomics: AutoHttpClient.Generator's generated client can be constructed directly (new) or resolved from DI with zero extra AOT-specific API surface, has build-time diagnostics for common mistakes (see Diagnostics), and ships an OpenAPI scaffolding tool. Seesamples/AotBenchmark/BENCHMARK.mdfor a measured, head-to-head trim/AOT comparison. - RestSharp is a runtime HTTP abstraction with reflection-oriented configuration rather than compile-time emitted clients
- AutoHttpClient.Generator keeps everything as generated source in your build output: explicit, trim-friendly, and (for the request-building path) zero-reflection
dotnet add package AutoHttpClient.GeneratorThen register the generated clients:
builder.Services.AddAutoHttpClients();using AutoHttpClient;
[HttpClient]
public interface IOrdersApi
{
[Get("/api/orders/{id}")]
Task<Order?> GetOrderAsync(int id, CancellationToken ct = default);
[Get("/api/orders")]
Task<List<Order>> GetOrdersAsync([Query("status")] string? status = null, CancellationToken ct = default);
[Post("/api/orders")]
Task<Order> CreateOrderAsync([Body] CreateOrderRequest request, CancellationToken ct = default);
[Put("/api/orders/{id}")]
Task<Order> UpdateOrderAsync(int id, [Body] UpdateOrderRequest request, CancellationToken ct = default);
[Delete("/api/orders/{id}")]
Task DeleteOrderAsync(int id, CancellationToken ct = default);
[Get("/api/orders/{id}/status")]
Task<HttpResponseMessage> GetOrderStatusRawAsync(int id, CancellationToken ct = default);
}Register the generated implementation:
builder.Services.AddAutoHttpClients();This emits an internal sealed client implementation and a DI registration similar to:
services.AddHttpClient("global::IOrdersApi").AddTypedClient<IOrdersApi>(httpClient => new OrdersApiClient(httpClient, jsonOptions));AddAutoHttpClients() (parameterless) is a convenience overload that falls back to
JsonSerializerOptions.Web; it's annotated [RequiresUnreferencedCode]/
[RequiresDynamicCode] so it only warns if you actually use it. For a fully
warning-free trimmed/Native AOT build, pass your own options instead — ideally backed by
a source-generated JsonSerializerContext:
builder.Services.AddAutoHttpClients(new JsonSerializerOptions(MyJsonContext.Default.Options)
{
TypeInfoResolver = MyJsonContext.Default,
});See samples/AotBenchmark/BENCHMARK.md for a
measured, zero-warning comparison against Refit using this pattern.
AutoHttpClient.Generator classifies parameters using these rules:
| Parameter style | Behavior |
|---|---|
[Body] |
Serialized as JSON request content |
[Query("name")] |
Added to the query string using the provided name |
[Query] or unattributed non-route parameter |
Added to the query string using the parameter name |
Array/IEnumerable<T> query parameter (not string) |
Expanded into one repeated name=value entry per element |
[Header("X-Name")] |
Added as an HTTP header |
[HeaderCollection] |
Expands an IDictionary<string, string?> (or any IEnumerable<KeyValuePair<string, string?>>) parameter into one header per entry |
[QueryMap] |
Expands an IDictionary<string, string?> (or any IEnumerable<KeyValuePair<string, string?>>) parameter into one query string entry per pair |
| Route parameter | Any parameter whose name appears in the route template, e.g. {id} |
CancellationToken |
Passed through to HttpClient and JSON helpers |
[Get("/api/orders")]
Task<List<Order>> GetOrdersAsync([Query("status")] string? status = null, int page = 1, CancellationToken ct = default);
[Post("/api/orders")]
Task<Order> CreateOrderAsync([Body] CreateOrderRequest request, [Header("X-Tenant")] string tenant, CancellationToken ct = default);
[Get("/api/orders/{id}")]
Task<Order?> GetOrderAsync(int id, CancellationToken ct = default);
[Get("/api/orders")]
Task<List<Order>> SearchOrdersAsync([QueryMap] IDictionary<string, string?> filters, CancellationToken ct = default);
[Get("/api/orders")]
Task<List<Order>> GetOrdersForTenantAsync([HeaderCollection] IDictionary<string, string?> headers, CancellationToken ct = default);Apply constant headers to every request generated for an interface or a specific method with [Headers("Name: Value")], similar to Refit:
using AutoHttpClient;
[HttpClient(BaseAddress = "https://api.example.com")]
[Headers("X-Api-Version: 1.0")]
public interface IOrdersApi
{
[Get("/api/orders")]
[Headers("Accept: application/json")]
Task<List<Order>> GetOrdersAsync(CancellationToken ct = default);
}Method-level [Headers] take priority over interface-level ones when the same header name is declared in both places. [Headers] can be applied multiple times on the same target.
Mark a method [Multipart] and decorate its parameters with [Part] to send a multipart/form-data request — useful for file uploads:
using AutoHttpClient;
[HttpClient(BaseAddress = "https://api.example.com")]
public interface IUploadsApi
{
[Post("/api/uploads")]
[Multipart]
Task<UploadResult> UploadAsync(
[Part("file", "photo.png")] Stream file,
[Part("description")] string description,
[Part] UploadMetadata metadata,
CancellationToken ct = default);
}Part parameter types are handled automatically:
| Parameter type | Generated content |
|---|---|
string |
StringContent |
byte[] |
ByteArrayContent |
Stream (or subclass) |
StreamContent |
| Anything else | JSON-serialized via JsonContent.Create |
[Part(name, fileName)] controls the form field name and, optionally, the file name sent to the server; both default to the parameter name / no file name. A [Multipart] method cannot also declare a [Body] parameter (AH004).
| Return type | Generated behavior |
|---|---|
Task |
Sends the request and throws ApiException on a non-success status code |
Task<T> |
Sends the request, checks for success, and deserializes JSON with ReadFromJsonAsync<T>() |
Task<T?> |
Same as Task<T> but preserves nullable result types |
Task<HttpResponseMessage> |
Returns the raw response without any success check |
IObservable<T> |
Wraps the same request/response/deserialize logic in an IObservable<T> — see Observable return types below |
IAsyncEnumerable<T> |
Streams a JSON array response lazily — see Streaming with IAsyncEnumerable below |
Methods can also return IObservable<T> instead of Task<T> for interop with Rx-style code (a single HTTP call surfaced as an observable sequence, matching Refit's IObservable<T> support):
using AutoHttpClient;
[HttpClient]
public interface IOrdersApi
{
[Get("/api/orders/{id}")]
IObservable<Order> GetOrderAsync(int id);
}
// usage
ordersApi.GetOrderAsync(42).Subscribe(
order => Console.WriteLine(order.Id),
ex => Console.WriteLine($"Failed: {ex.Message}"));- The call doesn't start until
Subscribeis called; disposing the returned subscription cancels the in-flight request. - On success, the observer receives exactly one
OnNextfollowed byOnCompleted. On failure (including a non-success status code, which throwsApiException), the observer receivesOnErrorinstead. - No dependency on
System.Reactiveis required — it's a minimalIObservable<T>/IObserver<T>bridge using only BCL types. - A
CancellationTokenparameter isn't meaningful on anIObservable<T>-returning method (cancellation is via the subscription'sIDisposable.Dispose()instead), so don't declare one there.
Methods can return IAsyncEnumerable<T> to lazily stream a large JSON array response one element at a time instead of buffering the whole array in memory — something Refit doesn't support:
using AutoHttpClient;
using System.Collections.Generic;
using System.Threading;
[HttpClient]
public interface IOrdersApi
{
[Get("/api/orders")]
IAsyncEnumerable<Order> StreamOrdersAsync(CancellationToken ct = default);
}
// usage
await foreach (var order in ordersApi.StreamOrdersAsync(ct))
{
Console.WriteLine(order.Id);
}- The response and its underlying stream are disposed automatically when enumeration completes or the
await foreachis exited early (viabreak, an exception, or cancellation). - Deserialization uses
System.Text.Json.JsonSerializer.DeserializeAsyncEnumerable<T>, so elements are yielded as they're parsed instead of waiting for the entire response body. - A non-success status code throws
ApiExceptionbefore any elements are yielded. - Declare an explicit
CancellationTokenparameter (as shown above) to cancel enumeration —[EnumeratorCancellation]/.WithCancellation()support is intentionally not implemented to keep the generated code simple. [Retry](below) can't be combined withIAsyncEnumerable<T>methods (diagnosticAH007) because C# iterator methods can't wrapyield returnin atry/catch.
For a one-attribute resilience policy, use [Resilience] on a method. It applies timeout + retry defaults together:
Conservative→ 2 attempts, 500ms base delay, 15s timeoutStandard(default) → 3 attempts, 200ms base delay, 30s timeoutAggressive→ 5 attempts, 100ms base delay, 60s timeout
using AutoHttpClient;
[HttpClient]
public interface IOrdersApi
{
[Get("/api/orders/{id}")]
[Resilience(ResiliencePreset.Aggressive)]
Task<Order> GetOrderAsync(int id, CancellationToken ct = default);
}[Resilience] is method-level, so different endpoints can use different policies. If both [Resilience] and [Retry] are applied, explicit [Retry] values win for retry attempts/delay, while timeout still comes from the selected resilience preset.
Decorate a method with [Retry(maxAttempts, delayMilliseconds)] to automatically retry on transient failures — no dependency on Polly required:
using AutoHttpClient;
[HttpClient]
public interface IOrdersApi
{
[Get("/api/orders/{id}")]
[Retry(maxAttempts: 5, delayMilliseconds: 100)]
Task<Order> GetOrderAsync(int id, CancellationToken ct = default);
}- Defaults are
maxAttempts: 3, delayMilliseconds: 200if omitted. - A failure is considered transient (and retried) if it's an
ApiExceptionwith a 5xx,408, or429status code, or anHttpRequestException(e.g. a connection failure). Genuine cancellations (OperationCanceledException/TaskCanceledException) are never retried. - Delay between attempts grows exponentially (attempt 1 waits the base delay, attempt 2 waits 2x, attempt 3 waits 4x, and so on).
- The entire request (URL/query/header/body construction and the HTTP call) is retried, not just the deserialization step, since a failed
HttpClient.SendAsynccall may not have reached the server at all. - Not supported on
IAsyncEnumerable<T>-returning methods — see Streaming with IAsyncEnumerable above.
By default every generated method uses the single JsonSerializerOptions instance passed to the client's constructor (or registered via AddAutoHttpClients(jsonOptions)). Override it for an individual method with [JsonSerializerOptions(providerType, memberName)], pointing at a public static property or field of type JsonSerializerOptions:
using AutoHttpClient;
using System.Text.Json;
public static class LegacyApiJsonOptions
{
public static JsonSerializerOptions Options { get; } = new JsonSerializerOptions
{
PropertyNamingPolicy = null, // this endpoint still uses PascalCase
};
}
[HttpClient]
public interface IOrdersApi
{
[Get("/api/orders/{id}")]
[JsonSerializerOptions(typeof(LegacyApiJsonOptions), nameof(LegacyApiJsonOptions.Options))]
Task<Order> GetOrderAsync(int id, CancellationToken ct = default);
}If the referenced member doesn't exist (or isn't a public static JsonSerializerOptions property/field), diagnostic AH006 is reported and the method falls back to the client-wide options.
Non-success responses throw AutoHttpClient.ApiException (instead of a bare EnsureSuccessStatusCode() call) so you don't lose the response body:
try
{
var order = await ordersApi.GetOrderAsync(404, ct);
}
catch (AutoHttpClient.ApiException ex)
{
// ex.StatusCode, ex.ReasonPhrase, ex.Content (raw response body, best-effort)
}If you need the raw HttpResponseMessage instead (no exception thrown), use a Task<HttpResponseMessage> return type.
You can configure a base address directly on the interface attribute:
using AutoHttpClient;
[HttpClient(BaseAddress = "https://api.example.com")]
public interface IOrdersApi
{
[Get("/api/orders")]
Task<List<Order>> GetOrdersAsync(CancellationToken ct = default);
}The generated DI registration configures the typed client:
services.AddHttpClient<IOrdersApi, OrdersApiClient>(client =>
{
client.BaseAddress = new Uri("https://api.example.com");
});AutoHttpClient.Generator now includes a small repo-side scaffolding tool for converting an OpenAPI/Swagger JSON document into a partial interface decorated with AutoHttpClient attributes.
Run it with:
dotnet run --project tools/AutoHttpClient.OpenApiScaffold -- --input swagger.json --output IMyApiClient.g.cs --namespace MyApp.Clients --interface-name IMyApiClientThe generated file is a one-time scaffold that you add to your project, then the existing AutoHttpClient.Generator source generator consumes it normally.
[HttpClient]or[HttpClient(BaseAddress = "...")]when the spec declares a simple server URL[Get],[Post],[Put],[Delete],[Patch]based on each OpenAPI operation- Route parameters as normal method parameters
- Query parameters as
[Query("name")] - Request bodies as
[Body] Task<T>return types using referenced schema names where possible
- Optimized for common OpenAPI 3 JSON documents
- Swagger/OpenAPI 2 documents may work for basic paths/operations, but v3 is the primary target
- Best support is for JSON request/response bodies with named schemas, simple path/query/header parameters, and standard HTTP verbs
- Inline/anonymous schemas fall back to
JsonElement(or collections/dictionaries of known types where possible) - Advanced OpenAPI features such as
oneOf,anyOf, callbacks, multipart form uploads, and full DTO generation are not scaffolded yet - Named schemas are used as C# type names in the generated interface; you still need matching DTO types in your project
| Feature | AutoHttpClient.Generator | Refit (v16+) | RestSharp |
|---|---|---|---|
| Compile-time generated client | ✅ | ✅ (also generator-based) | ❌ |
| AOT-safe out of the box (no reflection-based JSON) | ✅ 0 trim/AOT warnings, measured — see BENCHMARK.md | ✅ with JsonSerializerContext |
❌ |
| Zero reflection dispatch | ✅ | ✅ | ❌ |
Native HttpClient typed client DI |
✅ | ✅ | |
| Interface-first API | ✅ | ✅ | ❌ |
| OpenAPI/Swagger scaffolding tool | ✅ (repo tool) | ✅ | |
| Build-time diagnostics | ✅ | Limited | ❌ |
| Typed exception with response body on failure | ✅ (ApiException) |
✅ (ApiException) |
|
| Collection query parameter expansion | ✅ | ✅ | |
| Multipart/form-data uploads | ✅ | ✅ | |
IObservable<T> return types |
✅ | ✅ | ❌ |
| Per-method JSON serializer override | ✅ ([JsonSerializerOptions]) |
RefitSettings instance, not per-method |
❌ |
IAsyncEnumerable<T> streaming responses |
✅ | ❌ | ❌ |
| Per-endpoint resilience presets | ✅ ([Resilience(Conservative/Standard/Aggressive)]) |
❌ | ❌ |
| Built-in retry with exponential backoff | ✅ ([Retry], no Polly needed) |
❌ (requires Polly + HttpClientFactory handlers) |
❌ |
| Code | Severity | Message |
|---|---|---|
AH001 |
Warning | Method on a [HttpClient] interface has no HTTP method attribute and will not be generated. |
AH002 |
Warning | Route template parameter has no matching method parameter. |
AH003 |
Error | Method has multiple [Body] parameters; only one is allowed. |
AH004 |
Error | Method is marked [Multipart] but also has a [Body] parameter. |
AH005 |
Warning | Parameter is marked [Part] but its method is not marked [Multipart]. |
AH006 |
Warning | [JsonSerializerOptions] provider member wasn't found (or isn't a public static JsonSerializerOptions property/field) — the client-wide options are used instead. |
AH007 |
Warning | [Retry] is combined with an IAsyncEnumerable<T>-returning method and is ignored, since iterator methods can't wrap yield return in a try/catch. |
The package emits these attributes at post-initialization time:
HttpClientAttributeGetAttributePostAttributePutAttributeDeleteAttributePatchAttributeBodyAttributeQueryAttributeHeaderAttributeHeadersAttributeHeaderCollectionAttributeQueryMapAttributeMultipartAttributePartAttributeApiException
AutoHttpClient.Generator uses the same interface-first approach as Refit. Migration is mostly a find-and-replace of attributes.
dotnet add package AutoHttpClient.Generator
dotnet remove package Refit
dotnet remove package Refit.HttpClientFactory// Before (Refit)
using Refit;
public interface IOrdersApi
{
[Get("/api/orders/{id}")]
Task<Order?> GetOrderAsync(int id, CancellationToken ct = default);
[Post("/api/orders")]
Task<Order> CreateOrderAsync([Body] CreateOrderRequest request, CancellationToken ct = default);
[Get("/api/orders")]
Task<List<Order>> GetOrdersAsync([AliasAs("status")] string? status = null);
}
// After (AutoHttpClient.Generator)
using AutoHttpClient;
[HttpClient]
public interface IOrdersApi
{
[Get("/api/orders/{id}")]
Task<Order?> GetOrderAsync(int id, CancellationToken ct = default);
[Post("/api/orders")]
Task<Order> CreateOrderAsync([Body] CreateOrderRequest request, CancellationToken ct = default);
[Get("/api/orders")]
Task<List<Order>> GetOrdersAsync([Query("status")] string? status = null);
}// Before (Refit)
builder.Services.AddRefitClient<IOrdersApi>()
.ConfigureHttpClient(c => c.BaseAddress = new Uri("https://api.example.com"));
// After (AutoHttpClient.Generator)
[HttpClient(BaseAddress = "https://api.example.com")]
public interface IOrdersApi { ... }
builder.Services.AddAutoHttpClients();| Refit | AutoHttpClient.Generator |
|---|---|
[Get("/path")] |
[Get("/path")] |
[Post("/path")] |
[Post("/path")] |
[Put("/path")] |
[Put("/path")] |
[Delete("/path")] |
[Delete("/path")] |
[Patch("/path")] |
[Patch("/path")] |
[Body] |
[Body] |
[AliasAs("name")] |
[Query("name")] |
[Header("X-Name")] |
[Header("X-Name")] |
[Headers("X: Y")] |
[Headers("X: Y")] |
[HeaderCollection] |
[HeaderCollection] |
[Multipart] / [AttachmentName] |
[Multipart] / [Part(name, fileName)] |
[Authorize] |
Use [Header("Authorization")] |
IObservable<T> return types and per-method JSON serializer overrides ([JsonSerializerOptions]) are both supported — see Observable return types and Per-method JsonSerializerOptions override above. AutoHttpClient.Generator also goes further than Refit with three built-in differentiators Refit doesn't offer at all: IAsyncEnumerable<T> streaming responses, Polly-free automatic retry, and per-endpoint resilience presets.
🌐 Full suite overview: swevo.github.io
| Package | Description |
|---|---|
| AutoLog.Generator | Compile-time high-performance logging — [Log(Level, Message)] on a partial method generates LoggerMessage.Define. AOT-safe. |
| AutoDispatch.Generator | Compile-time CQRS dispatcher — [Handler] generates a strongly-typed IDispatcher. No MediatR, no reflection. |
| AutoWire | Compile-time DI auto-registration — [Scoped]/[Singleton]/[Transient] generates IServiceCollection registration code. |
| AutoMap.Generator | Compile-time object mapping with generated extension methods. AOT-safe AutoMapper alternative. |
| AutoValidate.Generator | Compile-time FluentValidation wiring — discovers validators and generates AddValidators(). |
| AutoResult.Generator | Compile-time Result<T> — [TryWrap] generates Try*() wrappers for every public method. |
| AutoQuery.Generator | Compile-time LINQ query specs — [QuerySpec] generates a strongly-typed Apply(IQueryable<T>). |
| Package | Downloads | Description |
|---|---|---|
| AutoWire | Compile-time dependency injection auto-registration for | |
| AutoMap.Generator | Compile-time object mapping for | |
| AutoQuery.Generator | Compile-time query composition for IQueryable using Roslyn incremental source generators | |
| AutoArchitecture | Compile-time architecture/dependency-rule enforcement for | |
| AutoDispatch.Generator | Compile-time CQRS dispatcher for | |
| AutoLog.Generator | Compile-time high-performance logging for | |
| AutoValidate.Generator | Compile-time FluentValidation wiring for |
MIT