Skip to content
Nuvyntra Labs

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

FeatureHow
HTTP methods[Get], [Post], [Put], [Delete], [Patch], [Head]
Path parameters/users/{id} matches int id or [AliasAs("id")]; {id?} is optional
Query parametersRemaining 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]
StreamingIAsyncEnumerable<T> (JSON Lines or SSE)
CompressionRequestBodyCompression / [CompressRequest] (gzip or brotli)
AuthorizationAuthorizationHeaderValueGetter (attach only; refresh is a sibling)
CancellationCancellationToken
Rich responseTask<IApiResponse<T>> (no throw on 4xx/5xx)
ErrorsApiException (HTTP response), ApiRequestException (transport)
JSONSystem.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

NeedPackage
Core generated clientPlugin.Maui.HttpForge
Test stubsPlugin.Maui.HttpForge.Testing
Newtonsoft.JsonPlugin.Maui.HttpForge.NewtonsoftJson
XML (XXE-safe)Plugin.Maui.HttpForge.Xml
Reflection fallbackNot planned — stay generated-only
dotnet add package Plugin.Maui.HttpForge.Testing
dotnet add package Plugin.Maui.HttpForge.NewtonsoftJson
dotnet add package Plugin.Maui.HttpForge.Xml
var 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.

Plugin.Maui.HttpForge on GitHub Packages

Source on GitHub

Discussion

Comment on Plugin.Maui.HttpForge. The thread lives on this component's GitHub repository (nuvyntralabs/Plugin.Maui.HttpForge). Sign in with GitHub — Giscus uses Discussions, Utterances uses Issues.