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.
- 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 |
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 | ✅ | ✅ |
| 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]. |
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- Custom
JsonSerializerSettingsper method
For projects using any of these heavily, hold off on migrating until support lands.
🌐 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