Skip to content
Nuvyntra Labs

Install

dotnet add package Plugin.Maui.HttpForge

Package ID: Plugin.Maui.HttpForge. Current package is 1.1.1 on nuget.org and GitHub Packages. Install only the sibling or optional packages the host actually needs — HttpForge alone is enough for a typed client.

Restore from GitHub Packages: Use nuvyntralabs GitHub Packages from a C# project

dotnet add package Plugin.Maui.HttpForge.Testing
dotnet add package Plugin.Maui.HttpForge.NewtonsoftJson
dotnet add package Plugin.Maui.HttpForge.Xml

Quick start

using Plugin.Maui.HttpForge;

builder
    .UseMauiApp<App>()
    .UseHttpForge();

builder.Services.AddHttpForgeClient<IUserApi>(client =>
{
    client.BaseAddress = new Uri("https://api.example.com");
});

Resolve IUserApi from DI and call the interface. Without the MAUI host, use RestService.For<IUserApi>("https://api.example.com").

Pipeline

AddHttpForgeClient<T>() returns IHttpClientBuilder. That is the composition point for DelegatingHandlers and Microsoft HTTP pipelines.

ViewModel
    → IUserApi (HttpForge-generated)
        → IHttpClientFactory
            → GET cache               (ApiCache handler, optional)
            → Auth / 401 refresh      (SecureSession or ApiResilience)
            → Retry / circuit / queue (ApiResilience or Polly)
            → HttpClient
                → HTTPS API

IHttpClientFactory invokes handlers in reverse add order. Add resilience first, then cache, so a CacheFirst hit never enters retry. Register host options first (UseHttpForge, UseApiResilience, UseSecureSession, UseApiCache, UseSmartUpload), then attach each typed client.

NeedComposeAlternative
Retry, circuit breaker, offline POST queueApiResiliencePolly / Microsoft.Extensions.Http.Resilience
GET response cache (CacheFirst / SWR)ApiCacheHost-owned cache
Tokens / 401 refreshSecureSession or ApiResilienceMSAL / Auth0 / host-owned handler
Chunked resume after process deathSmartUploadtus / host-owned chunks
TLS / SPKI pin (fail-closed)TlsPinNative TrustManager / NSURLSession pin

ApiResilience — retry, circuit, offline queue, 401

Plugin.Maui.ApiResilience wraps the same HttpClient the generated client uses. Do not implement retry inside the HttpForge interface.

using Plugin.Maui.ApiResilience;
using Plugin.Maui.HttpForge;

builder
    .UseMauiApp<App>()
    .UseHttpForge()
    .UseApiResilience(options =>
    {
        options.Retry.MaxRetryAttempts = 3;
        options.CircuitBreaker.BreakDuration = TimeSpan.FromSeconds(15);
        options.OfflineQueue.Enabled = true;
        options.TokenRefresh.Enabled = true;
    });

builder.Services.AddSingleton<IAccessTokenProvider, AuthTokenProvider>();

builder.Services
    .AddHttpForgeClient<IUserApi>(client =>
    {
        client.BaseAddress = new Uri("https://api.example.com");
    })
    .AddApiResilience();

If the org already standardized on Polly or Microsoft resilience:

builder.Services
    .AddHttpForgeClient<IUserApi>(client =>
    {
        client.BaseAddress = new Uri("https://api.example.com");
    })
    .AddStandardResilienceHandler();

ApiCache — GET CacheFirst / SWR

Plugin.Maui.ApiCache remembers GET responses. Attach the handler to the typed client. Do not also call IApiCache.GetAsync for those same URLs or you will cache twice.

using Plugin.Maui.ApiCache;
using Plugin.Maui.ApiResilience;
using Plugin.Maui.HttpForge;

builder
    .UseMauiApp<App>()
    .UseHttpForge()
    .UseApiResilience()
    .UseApiCache(options =>
    {
        options.DefaultExpiration = TimeSpan.FromMinutes(30);
        options.DefaultPolicy = CachePolicy.CacheFirst;
    });

builder.Services
    .AddHttpForgeClient<IUserApi>(client =>
    {
        client.BaseAddress = new Uri("https://api.example.com");
    })
    .AddApiResilience()
    .AddApiCache();

IUserApi.GetUser(id) then goes through CacheFirst (or the configured policy). Cached responses include X-ApiCache-Hit, X-ApiCache-Stale, and X-ApiCache-Policy. IApiCache.GetAsync is the path-based entry point when there is no typed interface. Invalidate after a local write:

await cache.InvalidateByPrefixAsync("/users");

Offline-first writes stay on OfflineSync, not ApiCache.

SecureSession — tokens and session lock

Plugin.Maui.SecureSession stores access/refresh tokens (via SecureStoragePlus), attaches Bearer, and retries once on 401. HttpForge can attach a static token via AuthorizationHeaderValueGetter; it does not refresh on 401. SecureSession targets Android and iOS. On Mac Catalyst or Windows, use ApiResilience IAccessTokenProvider instead.

Register a login client without the session handler, and the business client with it. Pick one 401 path — do not stack AddSecureSession() and ApiResilience token refresh on the same client.

using Plugin.Maui.ApiResilience;
using Plugin.Maui.HttpForge;
using Plugin.Maui.SecureSession;

builder.Services.AddSingleton<IAuthGateway, ShopAuthGateway>();

builder
    .UseMauiApp<App>()
    .UseHttpForge()
    .UseSecureSession(options =>
    {
        options.AccessTokenRefreshSkew = TimeSpan.FromSeconds(60);
        options.AcceptUnvalidatedTokens = false;
    })
    .UseApiResilience(options =>
    {
        options.TokenRefresh.Enabled = false;
        options.OfflineQueue.Enabled = true;
    });

builder.Services
    .AddHttpForgeClient<IAuthApi>(client =>
    {
        client.BaseAddress = new Uri("https://api.example.com");
    });

builder.Services
    .AddHttpForgeClient<IUserApi>(client =>
    {
        client.BaseAddress = new Uri("https://api.example.com");
    })
    .AddSecureSession()
    .AddApiResilience();
HostToken handlerApiResilience TokenRefresh
Android / iOS with SecureSession.AddSecureSession()Off
Any HttpForge TFM without SecureSessionIAccessTokenProviderOn
Already on MSAL / Auth0That SDK's handlerOff

SmartUpload — resumable files

Plugin.Maui.SmartUpload owns chunked upload, retry, and process-death resume. HttpForge [Multipart] / StreamPart is a single POST. Use HttpForge for the JSON API around the file. Use SmartUpload for the bytes when the file must survive a kill.

using Plugin.Maui.HttpForge;
using Plugin.Maui.SmartUpload;

builder
    .UseMauiApp<App>()
    .UseHttpForge()
    .UseSmartUpload(options =>
    {
        options.RequireHttps = true;
        options.ResumeInterruptedOnStart = true;
        options.DefaultChunkSize = 512 * 1024;
    });

builder.Services.AddHttpForgeClient<IMediaApi>(client =>
{
    client.BaseAddress = new Uri("https://api.example.com");
});
public interface IMediaApi
{
    [Post("/assets/{id}/complete")]
    Task Confirm(string id, CancellationToken cancellationToken = default);
}

var session = await uploads.EnqueueAsync(new UploadRequest
{
    FilePath = photoPath,
    Endpoint = new Uri("https://api.example.com/uploads"),
    Protocol = UploadProtocolKind.Tus,
    Headers =
    {
        ["Authorization"] = $"Bearer {accessToken}"
    }
});

uploads.SessionCompleted += async (_, e) =>
{
    await mediaApi.Confirm(e.Session.SessionId);
};

Do not set RequireHttps = false unless the host explicitly asked for http://.

Suggested registration order

builder
    .UseMauiApp<App>()
    .UseHttpForge()
    .UseSecureSession(...)
    .UseApiResilience(...)
    .UseApiCache(...)
    .UseSmartUpload(...);

builder.Services
    .AddHttpForgeClient<IAuthApi>(c => c.BaseAddress = new Uri("https://api.example.com"));

builder.Services
    .AddHttpForgeClient<IUserApi>(c => c.BaseAddress = new Uri("https://api.example.com"))
    .AddSecureSession()
    .AddApiResilience()
    .AddApiCache();

What not to wire

TemptationWhy not
Retry attributes on the HttpForge interfaceResilience belongs on the handler pipeline
IApiCache.GetAsync around an HttpForge GETDouble-caches when .AddApiCache() is already on the client
.AddSecureSession() and ApiResilience token refresh togetherTwo 401 refresh loops
401 refresh inside HttpForgeUse AuthorizationHeaderValueGetter only to attach a token; refresh with SecureSession or ApiResilience
[Multipart] for multi-megabyte resumeUse SmartUpload
SecureSession on Mac Catalyst / WindowsThat plugin is Android + iOS
Observability just to “see HTTP”Use ILogger or Diagnostics breadcrumbs if you already have them
Package reference from HttpForge → those siblingsKeeps the REST client usable without the suite

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.