Skip to content
NLNuvyntra Labs

Install

dotnet add package Plugin.Maui.HttpForge

Package ID: Plugin.Maui.HttpForge. Current NuGet is 1.0.1. Install only the sibling plugins the host actually needs — HttpForge alone is enough for a typed client.

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

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 does not attach Authorization by itself in v1. 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
Authorization getter inside HttpForgeUse 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