Install
dotnet add package Plugin.Maui.HttpForgePackage 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 APIIHttpClientFactory 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.
| Need | Compose | Alternative |
|---|---|---|
| Retry, circuit breaker, offline POST queue | ApiResilience | Polly / Microsoft.Extensions.Http.Resilience |
| GET response cache (CacheFirst / SWR) | ApiCache | Host-owned cache |
| Tokens / 401 refresh | SecureSession or ApiResilience | MSAL / Auth0 / host-owned handler |
| Chunked resume after process death | SmartUpload | tus / 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();| Host | Token handler | ApiResilience TokenRefresh |
|---|---|---|
| Android / iOS with SecureSession | .AddSecureSession() | Off |
| Any HttpForge TFM without SecureSession | IAccessTokenProvider | On |
| Already on MSAL / Auth0 | That SDK's handler | Off |
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
| Temptation | Why not |
|---|---|
| Retry attributes on the HttpForge interface | Resilience belongs on the handler pipeline |
| IApiCache.GetAsync around an HttpForge GET | Double-caches when .AddApiCache() is already on the client |
| .AddSecureSession() and ApiResilience token refresh together | Two 401 refresh loops |
| Authorization getter inside HttpForge | Use SecureSession or ApiResilience |
| [Multipart] for multi-megabyte resume | Use SmartUpload |
| SecureSession on Mac Catalyst / Windows | That plugin is Android + iOS |
| Observability just to “see HTTP” | Use ILogger or Diagnostics breadcrumbs if you already have them |
| Package reference from HttpForge → those siblings | Keeps the REST client usable without the suite |