What it is
HttpForge is the contract layer. You declare HTTP REST as a C# interface. A source generator emits the HttpClient implementation at compile time — there is no runtime reflection request builder.
public interface IUserApi
{
[Get("/users/{id}")]
Task<User> GetUser(int id, CancellationToken cancellationToken = default);
[Post("/users")]
Task<User> CreateUser([Body] CreateUserRequest request);
}
var user = await api.GetUser(42);Registration
UseHttpForge() is the MAUI host hook. AddHttpForgeClient<T>() registers the generated client with IHttpClientFactory and returns IHttpClientBuilder.
using Plugin.Maui.HttpForge;
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.UseHttpForge();
builder.Services.AddHttpForgeClient<IUserApi>(client =>
{
client.BaseAddress = new Uri("https://api.example.com");
});
return builder.Build();
}
}Resolve IUserApi from DI. Without the host:
var api = RestService.For<IUserApi>("https://api.example.com");Contract surface
| Feature | How |
|---|---|
| HTTP methods | [Get], [Post], [Put], [Delete], [Patch], [Head] |
| Path parameters | /users/{id} matches int id or [AliasAs("id")]; {id?} is optional |
| Query parameters | Remaining parameters, [Query], query objects, collection formats, camel/snake/kebab keys |
| Query flags | [QueryName] writes ?archived with no value |
| JSON body | [Body], or [Body(BodySerializationMethod.JsonLines)] |
| Headers | [Headers("Accept: application/json")], [Header("X-Request-Id")] |
| Multipart | [Multipart] with StreamPart, ByteArrayPart, FileInfoPart, [FormObject] |
| Routes | [PathPrefix], [Url] (validate input — SSRF risk), [Timeout] |
| Streaming | IAsyncEnumerable<T> (JSON Lines or SSE) |
| Compression | RequestBodyCompression / [CompressRequest] (gzip or brotli) |
| Authorization | AuthorizationHeaderValueGetter (attach only; refresh is a sibling) |
| Cancellation | CancellationToken |
| Rich response | Task<IApiResponse<T>> (no throw on 4xx/5xx) |
| Errors | ApiException (HTTP response), ApiRequestException (transport) |
| JSON | System.Text.Json (optional JsonSerializerContext for AOT) |
Unsupported shapes fail at compile time (HFG001–HFG010). HttpForge is generated-only — there is no reflection fallback package.
1.1 request surface
1.1.0 adds the Refit-parity extras that were missing in 1.0: query objects, collection formats, naming presets, timeouts, runtime URLs, path prefixes, optional segments, valueless query flags, form-object flattening, streaming, request compression, and a token attach hook.
[PathPrefix("/api/v1")]
public interface IUserApi
{
[Get("/users")]
Task<List<User>> Search([Query] UserQuery query, [Query(CollectionFormat.Multi)] int[] ages);
[Get("/users/{id}/orders/{orderId?}")]
[Timeout(5_000)]
Task<List<Order>> GetOrders(int id, int? orderId);
[Get("/items")]
Task<List<Item>> List([QueryName] string flag);
[Get("/events")]
IAsyncEnumerable<Event> StreamEvents(CancellationToken cancellationToken);
}
settings.UrlParameterKeyFormatter = UrlParameterKeyFormatter.SnakeCase;
settings.AuthorizationHeaderValueGetter = (request, ct) => tokenStore.GetAccessTokenAsync(ct);
settings.RequestBodyCompression = RequestBodyCompression.Gzip;Multipart
[Multipart]
[Post("/users/{id}/photo")]
Task UploadPhoto(int id, [AliasAs("file")] StreamPart file);
await api.UploadPhoto(7, new StreamPart(stream, "photo.jpg", "image/jpeg"));Flatten a C# object into form fields with [FormObject] (name, address.city). HFG008 fires if [FormObject] is used without [Multipart].
Errors and rich responses
A Task<T> method throws ApiException when the server returns 4xx/5xx, and ApiRequestException when the transport fails (timeout, DNS, TLS). Task<IApiResponse<T>> does not throw on HTTP error status — inspect the response instead.
- ApiException — an HTTP response arrived and was not success.
- ApiRequestException — the request never completed as an HTTP response.
- IApiResponse<T> — status, headers, and deserialized body without throwing on 4xx/5xx.
JSON and AOT
The default serializer is System.Text.Json. Hosts that trim or publish AOT can supply a JsonSerializerContext. Hosts can also swap IHttpContentSerializer. Newtonsoft.Json and XML ship as optional packages — they are not in the core nupkg.
Optional packages
| Need | Package |
|---|---|
| Core generated client | Plugin.Maui.HttpForge |
| Test stubs | Plugin.Maui.HttpForge.Testing |
| Newtonsoft.Json | Plugin.Maui.HttpForge.NewtonsoftJson |
| XML (XXE-safe) | Plugin.Maui.HttpForge.Xml |
| Reflection fallback | Not planned — stay generated-only |
dotnet add package Plugin.Maui.HttpForge.Testing
dotnet add package Plugin.Maui.HttpForge.NewtonsoftJson
dotnet add package Plugin.Maui.HttpForge.Xmlvar http = new StubHttp
{
{ Route.Get("/users/{id}"), Reply.With(new User { Id = 7, Name = "octocat" }) }
};
var api = http.CreateClient<IUserApi>("https://api.example.com");
var user = await api.GetUser(7);
await http.VerifyAllCalledAsync();
settings.ContentSerializer = new NewtonsoftJsonContentSerializer();
settings.ContentSerializer = new XmlContentSerializer();XmlContentSerializer prohibits DTD processing and sets XmlResolver to null.
Not planned
HttpForge 1.1.0 ships the Refit-parity request surface. Reflection fallback remains out of scope — unsupported shapes fail at compile time (HFG001–HFG010). Write that one HttpClient method by hand.
Platforms and version
Version 1.1.1. Target frameworks: net10.0, net10.0-android (API 21+), net10.0-ios (iOS 15+), net10.0-maccatalyst (15+), and net10.0-windows10.0.19041.0 (Windows 10.0.17763+). CI packs the Windows TFM on windows-latest and merges it into the nupkg published from macOS. The 1.1.0 Refit-parity request surface is unchanged.