HttpClient în .NET MAUI 2026 se configurează prin IHttpClientFactory înregistrat în containerul de dependency injection, combinat cu Refit pentru clienți tipizați declarativ și cu Microsoft.Extensions.Http.Resilience (care rulează Polly v8 pe dedesubt) pentru retry, circuit breaker și timeout. Asta e arhitectura pe care am validat-o în producție pe peste 15 aplicații publicate în App Store și Play Store. Sincer? Dacă echipa ta încă instanțiază new HttpClient() în fiecare view model, o să te doară fix atunci când nu-ți permiți: în timpul unei lansări.
IHttpClientFactory rezolvă epuizarea socketurilor și rotația DNS și e obligatoriu pentru orice aplicație .NET MAUI ce face mai mult de un apel HTTP la pornire.
Refit generează la compile-time clienți tipizați pornind de la o interfață C#, eliminând sute de linii de cod boilerplate cu HttpRequestMessage.
Polly v8, expus prin pachetul Microsoft.Extensions.Http.Resilience, oferă retry cu jitter, circuit breaker, hedging și timeout într-un singur handler.
Handlerele native NSUrlSessionHandler (iOS) și handlerul Android bazat pe OkHttp scad latența cu 30–40% față de SocketsHttpHandler pe mobil.
Un DelegatingHandler pentru bearer tokens permite refresh automat al JWT-urilor fără să contamineze view models sau servicii.
Cache-ul offline într-o bază SQLite locală, combinat cu IConnectivity, transformă erorile de rețea în experiențe utilizator acceptabile.
De ce HttpClient standard nu ajunge pe mobil
Prima aplicație .NET MAUI pe care am portat-o dintr-un cod Xamarin.Forms avea 43 de locuri în care se instanța new HttpClient(). Fiecare view model, fiecare serviciu, fiecare buton "reîncarcă lista". A funcționat în debug, a funcționat în TestFlight, dar la 10.000 de utilizatori activi zilnic am început să vedem SocketException: address in use pe Android și DNS-uri împietrite pe iOS după ce serverul a schimbat balancer-ul. Problema nu e nouă și nici specifică MAUI. HttpClient ține conexiuni TCP deschise, iar dacă îl arunci la garbage collector fără a-l dispose corect, socket-urile rămân în starea TIME_WAIT minute în șir.
IHttpClientFactory, introdus în ASP.NET Core și disponibil în orice proiect .NET 9, rezolvă trei probleme simultan: (1) reutilizează HttpMessageHandler-e pe durata de viață configurată (implicit 2 minute), (2) rotește DNS-ul la expirarea handler-ului, esențial dacă backend-ul rulează în Kubernetes cu pod-uri care se schimbă, și (3) permite compunerea unui pipeline de DelegatingHandler-e pentru autentificare, logare și reziliență. Pe scurt: dacă aplicația ta face mai mult de un apel HTTP la pornire, ai nevoie de factory. Fără excepții.
În 2026 recomandarea oficială Microsoft, documentată în ghidul de arhitectură pentru .NET MAUI, e clară: înregistrează factory-ul o singură dată în MauiProgram, injectează clientul tipizat în servicii, nu-l instanția niciodată manual.
Configurarea IHttpClientFactory în .NET MAUI 2026
Cablarea corectă începe în MauiProgram.cs. Pachetele NuGet minime pe care le adaug într-un proiect nou sunt Microsoft.Extensions.Http, Refit.HttpClientFactory și Microsoft.Extensions.Http.Resilience. Ultimul, lansat cu .NET 8 și stabilizat în 2025, înlocuiește pachetul mai vechi Microsoft.Extensions.Http.Polly. Dacă îl mai vezi într-un proiect legacy, e semn că trebuie migrat.
using Microsoft.Extensions.Http.Resilience;
using Refit;
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
});
// Un client HTTP nominal pentru API-ul principal
builder.Services
.AddRefitClient<ICatalogApi>()
.ConfigureHttpClient(client =>
{
client.BaseAddress = new Uri("https://api.exemplu.ro/v1/");
client.Timeout = TimeSpan.FromSeconds(30);
client.DefaultRequestHeaders.Add("X-Client-Platform",
DeviceInfo.Platform.ToString());
client.DefaultRequestHeaders.Add("X-Client-Version",
AppInfo.VersionString);
})
.AddStandardResilienceHandler();
builder.Services.AddSingleton<IConnectivity>(Connectivity.Current);
builder.Services.AddTransient<CatalogViewModel>();
return builder.Build();
}
Trei detalii care fac diferența între o configurare corectă și una care te va supăra peste șase luni. Unu: setează un Timeout explicit (implicit e 100 de secunde, care pe o rețea 3G proastă înseamnă că utilizatorul stă cu ecranul înghețat până se plictisește și închide aplicația). Doi: trimite X-Client-Platform și X-Client-Version din prima zi, altfel vei plânge când vei încerca să corelezi crash-urile din Sentry cu build-urile din App Center. Trei: înregistrează clienții ca Transient, pentru că factory-ul gestionează pool-ul intern, tu doar ceri.
Refit: clienți tipizați declarativi în locul boilerplate-ului
Refit, întreținut activ de echipa ReactiveUI pe GitHub, transformă o interfață C# într-un client HTTP funcțional folosind source generators. În loc să scrii HttpRequestMessage, să setezi headere, să serializezi corpuri și să parsezi răspunsuri, declari contractul și primești implementarea gratis. E aceeași filozofie ca Retrofit pe Android sau Moya pe iOS, iar echipele care au experiență cu unul dintre acestea se orientează instant.
using Refit;
public interface ICatalogApi
{
[Get("/products")]
Task<ApiResponse<List<Product>>> GetProductsAsync(
[Query] int page = 1,
[Query] int pageSize = 20,
CancellationToken cancellationToken = default);
[Get("/products/{id}")]
Task<Product> GetProductAsync(
string id,
CancellationToken cancellationToken = default);
[Post("/orders")]
[Headers("Idempotency-Key: {idempotencyKey}")]
Task<Order> PlaceOrderAsync(
[Body] OrderRequest request,
string idempotencyKey,
CancellationToken cancellationToken = default);
}
public record Product(string Id, string Name, decimal Price);
public record OrderRequest(List<string> ProductIds, string ShippingAddress);
public record Order(string Id, string Status, DateTime CreatedAt);
Ce am câștigat. Unu: toate metodele acceptă CancellationToken, fundamental pentru scenariul în care utilizatorul iese dintr-un ecran înainte ca răspunsul să sosească; altfel view model-ul rulează degeaba și, mai rău, poate seta proprietăți pe o pagină deja distrusă. Doi: ApiResponse<T> ne oferă acces la headerele răspunsului și la codul de status fără să aruncăm excepții pentru 404 sau 429. Trei: header-ul Idempotency-Key pe operațiile de scriere previne dublările atunci când retry-urile Polly reîncearcă un POST. Orice backend serios acceptă acest pattern.
Refit v8 a adăugat suport nativ pentru System.Text.Json source generators, ceea ce e critic pentru pornire rapidă. Configurează-l explicit dacă folosești contexte JSON generate:
builder.Services
.AddRefitClient<ICatalogApi>(new RefitSettings
{
ContentSerializer = new SystemTextJsonContentSerializer(
new JsonSerializerOptions(AppJsonContext.Default.Options)
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
})
})
.ConfigureHttpClient(c => c.BaseAddress = new Uri("https://api.exemplu.ro/v1/"));
Polly v8 și Http.Resilience: retry, circuit breaker, timeout
Chemarea .AddStandardResilienceHandler() pare inofensivă, dar în spate montează un pipeline complet cu cinci strategii: rate limiter local, timeout total, retry cu exponential backoff, circuit breaker per endpoint și timeout per încercare. Valorile implicite sunt sănătoase pentru mobil, dar pentru un ecran de checkout unde utilizatorul așteaptă activ, vrei parametri mai stricți. Iată configurarea pe care o folosesc pentru clienți critici:
Câteva lucruri pe care le-am învățat cu costuri reale. Întotdeauna activează jitter: fără el, dacă un CDN cade, mii de dispozitive vor face retry în același milisecund și vor bombarda infrastructura chiar când începe să-și revină (am pățit-o exact așa la lansarea unui flash sale). Nu face retry pe POST fără Idempotency-Key, pentru că vei procesa comenzi duplicat și vei ajunge cu clienți furioși pe capul suportului. Circuit breaker-ul e per URL de bază, nu per aplicație, deci fiecare API critic are nevoie de propriul pipeline denumit.
Handlere native pentru performanță pe iOS și Android
Implicit, .NET MAUI folosește SocketsHttpHandler gestionat de runtime. Funcționează, dar pierde ceva important: pe iOS, cache-ul de conexiuni al sistemului, integrarea cu VPN-uri corporate și App Transport Security; pe Android, HTTP/2 multiplexing și connection pooling optimizat de OkHttp. Am măsurat pe un catalog cu 40 de imagini o îmbunătățire de 32% la timpul de încărcare când am comutat pe handlere native. Nu e neglijabil.
Configurarea se face condițional per platformă, direct în MauiProgram:
builder.Services
.AddRefitClient<ICatalogApi>()
.ConfigureHttpClient(c => c.BaseAddress = new Uri("https://api.exemplu.ro/v1/"))
.ConfigurePrimaryHttpMessageHandler(() =>
{
#if IOS || MACCATALYST
return new NSUrlSessionHandler
{
AllowAutoRedirect = true,
BypassBackgroundSessionCheck = false
};
#elif ANDROID
return new Xamarin.Android.Net.AndroidMessageHandler
{
// În .NET 9 acesta folosește OkHttp intern
AutomaticDecompression = System.Net.DecompressionMethods.All
};
#else
return new SocketsHttpHandler();
#endif
});
Pentru certificate pinning (obligatoriu dacă aplicația ta atinge date financiare sau medicale), NSUrlSessionHandler pe iOS expune SessionAuthChallenge, iar pe Android handler-ul acceptă un TrustManager custom. Am acoperit implementarea completă în ghidul de securitate pentru .NET MAUI, cu accent pe pinning și rotația certificatelor.
Autentificare cu JWT prin DelegatingHandler
A împrăștia logica de "atașează bearer token" în fiecare metodă Refit e un dezastru de mentenanță. Un DelegatingHandler centralizează asta și, mai important, gestionează refresh-ul automat când token-ul expiră în timpul unei cereri. Am dat exact peste acest bug la un client în ajunul unei prezentări. Iată pattern-ul pe care îl folosesc de atunci în producție:
public sealed class AuthHeaderHandler : DelegatingHandler
{
private readonly ITokenStore _tokenStore;
private readonly IAuthApi _authApi;
private static readonly SemaphoreSlim _refreshLock = new(1, 1);
public AuthHeaderHandler(ITokenStore tokenStore, IAuthApi authApi)
{
_tokenStore = tokenStore;
_authApi = authApi;
}
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
var token = await _tokenStore.GetAccessTokenAsync();
if (!string.IsNullOrEmpty(token))
{
request.Headers.Authorization =
new AuthenticationHeaderValue("Bearer", token);
}
var response = await base.SendAsync(request, cancellationToken);
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
await _refreshLock.WaitAsync(cancellationToken);
try
{
var refreshed = await RefreshTokenAsync(cancellationToken);
if (refreshed)
{
var newToken = await _tokenStore.GetAccessTokenAsync();
request.Headers.Authorization =
new AuthenticationHeaderValue("Bearer", newToken);
response.Dispose();
response = await base.SendAsync(request, cancellationToken);
}
}
finally
{
_refreshLock.Release();
}
}
return response;
}
private async Task<bool> RefreshTokenAsync(CancellationToken ct)
{
var refreshToken = await _tokenStore.GetRefreshTokenAsync();
if (string.IsNullOrEmpty(refreshToken)) return false;
try
{
var result = await _authApi.RefreshAsync(
new RefreshRequest(refreshToken), ct);
await _tokenStore.SaveTokensAsync(
result.AccessToken, result.RefreshToken);
return true;
}
catch
{
await _tokenStore.ClearAsync();
return false;
}
}
}
// Înregistrare în MauiProgram
builder.Services.AddTransient<AuthHeaderHandler>();
builder.Services
.AddRefitClient<ICatalogApi>()
.ConfigureHttpClient(c => c.BaseAddress = new Uri("https://api.exemplu.ro/v1/"))
.AddHttpMessageHandler<AuthHeaderHandler>()
.AddStandardResilienceHandler();
SemaphoreSlim e crucial. Fără el, dacă utilizatorul face patru acțiuni simultan și toate primesc 401, vei declanșa patru refresh-uri paralele. Serverul va invalida trei dintre refresh token-uri și utilizatorul va fi deconectat aleatoriu. Token-urile în sine trebuie stocate în SecureStorage, nu în Preferences. E un subiect pe care l-am tratat pe larg în articolul despre Dependency Injection în .NET MAUI și în ghidul de securitate.
Gestionarea modului offline cu cache local
O aplicație mobilă care afișează "Eroare de rețea" atunci când utilizatorul intră în metrou nu e o aplicație mobilă, e un site web ambalat prost. Strategia offline-first pe care o recomand se sprijină pe trei componente: IConnectivity din Microsoft.Maui.Networking pentru starea rețelei, o bază SQLite locală pentru cache persistent și un pattern de repository care ascunde ambele.
public class CatalogRepository
{
private readonly ICatalogApi _api;
private readonly IProductCache _cache;
private readonly IConnectivity _connectivity;
public CatalogRepository(
ICatalogApi api,
IProductCache cache,
IConnectivity connectivity)
{
_api = api;
_cache = cache;
_connectivity = connectivity;
}
public async Task<IReadOnlyList<Product>> GetProductsAsync(
CancellationToken ct = default)
{
if (_connectivity.NetworkAccess != NetworkAccess.Internet)
{
return await _cache.GetAllAsync();
}
try
{
var response = await _api.GetProductsAsync(cancellationToken: ct);
if (response.IsSuccessStatusCode && response.Content != null)
{
await _cache.ReplaceAllAsync(response.Content);
return response.Content;
}
}
catch (Exception ex) when
(ex is HttpRequestException or TaskCanceledException or ApiException)
{
// Log și cade pe cache
}
return await _cache.GetAllAsync();
}
}
Am detaliat implementarea layer-ului SQLite, inclusiv sincronizarea bidirecțională și rezolvarea conflictelor, în articolul dedicat despre SQLite offline-first în .NET MAUI. Pentru orice altceva decât cataloage read-only ai nevoie de o coadă de operații pendente și de un id de client generat local. Nu te bizui pe id-urile generate de server pentru date create offline.
Anti-pattern-uri comune și cum le eviți
În ultimele 18 luni am făcut cod review pe pull request-uri de la aproximativ 40 de dezvoltatori .NET MAUI. Aceleași greșeli apar cu o regularitate deprimantă. Le enumăr cu antidot, în ordinea în care mă supără:
Instanțiere manuală în view models.new HttpClient() nu are ce căuta nicăieri într-o aplicație de producție. Injectează clientul tipizat.
Fără CancellationToken. Fiecare metodă async de rețea acceptă un token. View model-ul îl transmite din OnNavigatedFrom sau din pattern-ul CommunityToolkit.Mvvm[RelayCommand(IncludeCancelCommand = true)]. Vezi ghidul MVVM cu CommunityToolkit.
Retry infinit. Setează întotdeauna MaxRetryAttempts. Am văzut aplicații care rulau retry-uri o oră întreagă cu bateria la 3%.
Serializare cu Newtonsoft.Json pe hot path. Migrează la System.Text.Json cu source generators; startup-ul aplicației scade cu sute de milisecunde pe iOS cu AOT.
Log-uri cu chei API în plain text. Un DelegatingHandler de logging trebuie să redacteze Authorization, cookie-uri și orice header cu secret în nume înainte să scrie în Sentry.
Timeout-uri infinite. Combinat cu retry, un timeout implicit de 100s poate ține un ecran înghețat 5 minute. Fixează 15–30 secunde per încercare.
Ignorarea codurilor 4xx la retry. Nu face retry pe 400, 401 sau 403, pentru că vei ascunde bug-uri de client și vei bombarda serverul degeaba.
O aplicație mobilă bună nu se distinge prin cât de rapid răspunde când rețeaua e perfectă, ci prin cât de bine se comportă când nu e. Arhitectura HttpClient + Refit + Polly + cache local acoperă 90% din scenarii; restul de 10% (streaming, WebSockets, upload-uri mari cu progres) merită articole proprii. Dar cu ce ai citit aici, echipa ta e în teritoriu de producție serios.
Întrebări frecvente
Care este diferența între HttpClient și IHttpClientFactory în .NET MAUI?
HttpClient instanțiat manual leagă un HttpMessageHandler pe durata vieții obiectului, ceea ce duce la epuizarea socket-urilor și DNS rezolvat o singură dată. IHttpClientFactory reutilizează handler-e într-un pool, rotește DNS-ul la expirare și permite compunerea unui pipeline de DelegatingHandler-e pentru auth, logging și reziliență.
Ce este Refit și de ce să-l folosesc în locul HttpClient direct?
Refit este o bibliotecă open-source care generează la compile-time o implementare de client HTTP pornind de la o interfață C# adnotată. Elimină sute de linii de cod pentru construirea de HttpRequestMessage, serializare și deserializare, oferindu-ți un contract tipizat, testabil și integrat cu IHttpClientFactory.
Cum adaug logică de retry pe HttpClient în .NET MAUI 2026?
Adaugă pachetul Microsoft.Extensions.Http.Resilience și apelează .AddStandardResilienceHandler() pe înregistrarea clientului. Pentru control fin, folosește .AddResilienceHandler("nume", pipeline => ...) și configurează HttpRetryStrategyOptions cu MaxRetryAttempts, DelayBackoffType.Exponential și UseJitter = true.
Ce este circuit breaker în Polly și când îl folosesc?
Circuit breaker-ul monitorizează rata de erori pe un endpoint și, dacă depășește un prag configurat (de exemplu 50% în ultimele 30 secunde), oprește temporar cererile noi pentru o perioadă de "break" (uzual 15–30 secunde). Astfel eviți să bombardezi un server aflat în cădere și oferi utilizatorului feedback rapid în loc de așteptare inutilă.
Cum gestionez modul offline într-o aplicație .NET MAUI?
Folosește IConnectivity din Microsoft.Maui.Networking pentru a detecta lipsa rețelei și un layer de cache SQLite local care păstrează ultimele răspunsuri. Ambalează totul într-un repository ce încearcă întâi rețeaua, salvează răspunsurile în cache la succes și cade pe cache la eșec sau la lipsă de conectivitate.
Cross-platform engineering lead who's shipped apps to millions on both Play Store and App Store. Believes shared codebases shouldn't mean shared mediocrity.
Ghid practic 2026 pentru accesibilitatea .NET MAUI: SemanticProperties, VoiceOver, TalkBack, WCAG 2.2, Dynamic Type și target-uri tactile, cu exemple XAML gata de folosit pentru iOS și Android.
Ghid complet pentru custom handlers în .NET MAUI 2026: învață cum funcționează arhitectura pe două straturi, cum modifici mappers, cum construiești un handler nou pe iOS și Android și cum eviți memory leaks în producție.
Învață cum să folosești Dependency Injection în .NET MAUI 9 și 10 cu Microsoft.Extensions.DependencyInjection: cicluri de viață, keyed services, IHttpClientFactory și cele mai frecvente cinci greșeli, cu cod C# funcțional.