Install
dotnet add package Plugin.Maui.HttpForgePackage 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.XmlQuick 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 |
| TLS / SPKI pin (fail-closed) | TlsPin | Native 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();| 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 |
| 401 refresh inside HttpForge | Use AuthorizationHeaderValueGetter only to attach a token; refresh with 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 |