Úvod — prečo vôbec riešiť REST API v mobilných appkách
Mobilné aplikácie dnes prakticky nikdy nefungujú len tak samy pre seba. V predchádzajúcich článkoch tejto série sme si ukázali, ako ukladať dáta lokálne pomocou SQLite s Repository vzorom a ako postaviť čistú MVVM architektúru s CommunityToolkit a dependency injection. No a teraz prichádza ten logický ďalší krok — napojenie na vzdialené API. Toto je ten moment, kedy vaša aplikácia začne naozaj žiť: synchronizácia dát, autentifikácia, notifikácie, prístup k cloudovým službám.
REST API je jednoducho štandard. Inak sa to povedať nedá.
V .NET MAUI máte k dispozícii plne vybavený HttpClient, ale — a to je dosť podstatné "ale" — jeho správne použitie nie je také priamočiare, ako by sa na prvý pohľad mohlo zdať. Nesprávna správa inštancií vedie k vyčerpaniu socketov, chýbajúca odolnosť voči chybám spôsobuje zamŕzanie UI a ignorovanie platformových špecifík znamená problémy s TLS na starších zariadeniach. Na tieto veci som si v praxi nabehol viackrát, než by som chcel priznať.
V tomto článku prejdeme naozaj kompletným procesom — od základnej konfigurácie HttpClient cez HttpClientFactory, servisnú vrstvu s MVVM vzorom, CRUD operácie, JSON serializáciu s AOT podporou, platformové handlery, kontrolu pripojenia až po odolnosť voči chybám s retry a circuit breaker vzormi. Všetko s praktickými príkladmi, ktoré si môžete rovno hodiť do svojich projektov.
Základy HttpClient v .NET MAUI
HttpClient je v podstate vaša brána k akémukoľvek HTTP API. V .NET MAUI ho môžete použiť rovnako ako v bežnej .NET aplikácii, ale treba počítať s niekoľkými dôležitými rozdielmi, ktoré vyplývajú z mobilného prostredia.
Najčastejšia chyba začiatočníkov? Vytváranie novej inštancie HttpClient pre každú požiadavku. Vyzerá to neškodne — veď čo sa môže stať, však? No, v praxi to vedie k tzv. socket exhaustion. Každá inštancia drží TCP spojenie, ktoré sa neuvoľní okamžite po zahodení objektu (kvôli TIME_WAIT stavu v TCP stacku). Na mobilnom zariadení s obmedzeným počtom portov vám appka jednoducho prestane vytvárať nové spojenia. A to je presne ten typ bugu, ktorý sa objaví až v produkcii.
Základná registrácia HttpClient ako singletonu v MauiProgram.cs:
// MauiProgram.cs — základná registrácia HttpClient
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
});
// Registrácia HttpClient ako singletonu
builder.Services.AddSingleton(sp =>
{
var client = new HttpClient
{
BaseAddress = new Uri("https://api.vasadomena.com/"),
Timeout = TimeSpan.FromSeconds(30)
};
client.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/json"));
return client;
});
return builder.Build();
}
}
Singleton HttpClient je rozhodne lepší ako transientný, pretože zdieľa TCP spojenia a pool DNS rezolúcií. Má však jeden dosť nepríjemný problém — neaktualizuje DNS záznamy. Takže ak sa IP adresa servera zmení (čo sa pri škálovaní v cloude deje bežne), singleton bude veselo používať starú adresu. A práve preto existuje lepšie riešenie.
HttpClientFactory — ako na HTTP klientov poriadne
HttpClientFactory rieši oba problémy naraz: zabraňuje vyčerpaniu socketov aj zabezpečuje správnu rotáciu DNS záznamov. Interne spravuje HttpMessageHandler inštancie v poole s predvolenou životnosťou 2 minúty. Keď si vytvoríte nového HttpClient cez factory, dostanete vlastne len ľahkú obálku nad zdieľaným handlerom — takže vytváranie a zahadzovanie klientov je efektívne a úplne bezpečné.
Najprv si pridajte NuGet balíček:
dotnet add package Microsoft.Extensions.Http
Pomenovaní klienti (Named Clients)
Pomenovaní klienti sa hodia hlavne vtedy, keď komunikujete s viacerými API, kde každé má inú konfiguráciu:
// MauiProgram.cs — registrácia pomenovaných klientov
builder.Services.AddHttpClient("ProductApi", client =>
{
client.BaseAddress = new Uri("https://api.vasadomena.com/");
client.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/json"));
client.Timeout = TimeSpan.FromSeconds(30);
});
builder.Services.AddHttpClient("AuthApi", client =>
{
client.BaseAddress = new Uri("https://auth.vasadomena.com/");
client.Timeout = TimeSpan.FromSeconds(15);
});
V službe si potom jednoducho vytiahnete klienta pomocou IHttpClientFactory:
public class ProductService
{
private readonly IHttpClientFactory _httpClientFactory;
public ProductService(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}
public async Task<List<Product>> GetProductsAsync()
{
// Vytvorenie klienta s predkonfigurovanými nastaveniami
using var client = _httpClientFactory.CreateClient("ProductApi");
var response = await client.GetAsync("api/products");
response.EnsureSuccessStatusCode();
return await response.Content
.ReadFromJsonAsync<List<Product>>() ?? [];
}
}
Typovaní klienti (Typed Clients)
Typovaní klienti idú ešte o krok ďalej — HttpClient sa injektuje priamo do vašej služby cez konštruktor. Osobne toto považujem za najčistejší prístup, pretože konfigurácia klienta a logika služby sú pekne pohromade na jednom mieste:
// Registrácia typovaného klienta
builder.Services.AddHttpClient<IProductService, ProductService>(client =>
{
client.BaseAddress = new Uri("https://api.vasadomena.com/");
client.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/json"));
});
Pri typovaných klientoch sa DI kontajner postará o všetko za vás — automaticky vytvorí HttpClient a injektuje ho do konštruktora. Žiadne manuálne volanie CreateClient, všetko beží transparentne na pozadí. Paráda.
Budovanie servisnej vrstvy s MVVM vzorom
Ak ste čítali náš článok o MVVM architektúre s CommunityToolkit (a ak nie, odporúčam to dohnať), viete, že kľúčom k udržateľnej aplikácii je separácia zodpovedností. ViewModel by nemal vedieť absolútne nič o HttpClient, HTTP hlavičkách ani JSON serializácii. Všetku túto "špinavú prácu" zabalíme do servisnej vrstvy za rozhranie, ktoré ViewModel jednoducho konzumuje.
Definícia rozhrania IApiService
Rozhranie definuje kontrakt — čo vaša servisná vrstva dokáže. A je to dôležité aj z ďalšieho dôvodu: v unit testoch môžete jednoducho poskytnúť mock implementáciu a testovať ViewModel bez reálneho servera.
// Rozhranie pre API službu
public interface IProductService
{
Task<List<ProductDto>> GetAllProductsAsync();
Task<ProductDto?> GetProductByIdAsync(int id);
Task<ProductDto?> CreateProductAsync(CreateProductRequest request);
Task<bool> UpdateProductAsync(int id, UpdateProductRequest request);
Task<bool> DeleteProductAsync(int id);
}
DTO modely
DTO (Data Transfer Object) modely reprezentujú dáta, ktoré posielate na server a prijímate z neho. Možno sa pýtate, prečo nepoužiť priamo doménové modely. Dôvod je jednoduchý — API sa môže zmeniť bez toho, aby to rozbilo internú logiku vašej aplikácie. Tá vrstva abstrakcie sa vám jedného dňa vráti (väčšinou práve vtedy, keď backend tím oznámi "malú zmenu v API").
// DTO modely pre komunikáciu s API
public class ProductDto
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public decimal Price { get; set; }
public string Category { get; set; } = string.Empty;
public bool IsAvailable { get; set; }
}
public class CreateProductRequest
{
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public decimal Price { get; set; }
public string Category { get; set; } = string.Empty;
}
public class UpdateProductRequest
{
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public decimal Price { get; set; }
public string Category { get; set; } = string.Empty;
public bool IsAvailable { get; set; }
}
Implementácia ApiService
Tu sa dostávame k jadru veci. Implementácia služby zapuzdruje všetku HTTP komunikáciu. Používame typovaného klienta, takže HttpClient dostaneme rovno cez konštruktor:
// Implementácia servisnej vrstvy
public class ProductService : IProductService
{
private readonly HttpClient _httpClient;
private readonly ILogger<ProductService> _logger;
public ProductService(HttpClient httpClient,
ILogger<ProductService> logger)
{
_httpClient = httpClient;
_logger = logger;
}
public async Task<List<ProductDto>> GetAllProductsAsync()
{
var response = await _httpClient.GetAsync("api/products");
response.EnsureSuccessStatusCode();
return await response.Content
.ReadFromJsonAsync<List<ProductDto>>() ?? [];
}
public async Task<ProductDto?> GetProductByIdAsync(int id)
{
return await _httpClient
.GetFromJsonAsync<ProductDto>($"api/products/{id}");
}
public async Task<ProductDto?> CreateProductAsync(
CreateProductRequest request)
{
var response = await _httpClient
.PostAsJsonAsync("api/products", request);
response.EnsureSuccessStatusCode();
return await response.Content
.ReadFromJsonAsync<ProductDto>();
}
public async Task<bool> UpdateProductAsync(
int id, UpdateProductRequest request)
{
var response = await _httpClient
.PutAsJsonAsync($"api/products/{id}", request);
return response.IsSuccessStatusCode;
}
public async Task<bool> DeleteProductAsync(int id)
{
var response = await _httpClient
.DeleteAsync($"api/products/{id}");
return response.IsSuccessStatusCode;
}
}
ViewModel, ktorý službu konzumuje
ViewModel komunikuje výlučne cez rozhranie IProductService. Vďaka CommunityToolkit.Mvvm sa príkazy a notifikácie o zmenách vlastností generujú automaticky — ak ste čítali náš článok o MVVM, toto vám bude povedomé:
// ViewModel používajúci servisnú vrstvu
[ObservableObject]
public partial class ProductListViewModel
{
private readonly IProductService _productService;
[ObservableProperty]
private ObservableCollection<ProductDto> _products = [];
[ObservableProperty]
private bool _isLoading;
[ObservableProperty]
private string _errorMessage = string.Empty;
public ProductListViewModel(IProductService productService)
{
_productService = productService;
}
[RelayCommand]
private async Task LoadProductsAsync()
{
try
{
IsLoading = true;
ErrorMessage = string.Empty;
var items = await _productService.GetAllProductsAsync();
Products = new ObservableCollection<ProductDto>(items);
}
catch (HttpRequestException ex)
{
ErrorMessage = "Nepodarilo sa načítať produkty. " +
"Skontrolujte internetové pripojenie.";
}
finally
{
IsLoading = false;
}
}
[RelayCommand]
private async Task DeleteProductAsync(ProductDto product)
{
var success = await _productService
.DeleteProductAsync(product.Id);
if (success)
{
Products.Remove(product);
}
}
}
CRUD operácie — GET, POST, PUT, DELETE v praxi
Dobre, poďme sa pozrieť na jednotlivé CRUD operácie trochu podrobnejšie. Dôraz dáme na správne spracovanie chýb, kontrolu stavových kódov a prácu s odpoveďami. V reálnej aplikácii totiž nestačí len zavolať EnsureSuccessStatusCode() a dúfať, že všetko pôjde hladko — potrebujete granulárne spracovanie rôznych chybových stavov, aby ste vedeli používateľovi povedať, čo sa vlastne stalo.
GET — získavanie dát
Pri GET požiadavkách je kľúčové rozlišovať medzi rôznymi chybovými stavmi. Používateľ chce vedieť, či mu vypršala session, alebo či je server preťažený — nie len "niečo sa pokazilo":
// GET s detailným spracovaním chýb
public async Task<ApiResponse<List<ProductDto>>> GetProductsAsync(
CancellationToken ct = default)
{
try
{
var response = await _httpClient.GetAsync("api/products", ct);
return response.StatusCode switch
{
HttpStatusCode.OK =>
ApiResponse<List<ProductDto>>.Success(
await response.Content
.ReadFromJsonAsync<List<ProductDto>>(ct) ?? []),
HttpStatusCode.Unauthorized =>
ApiResponse<List<ProductDto>>
.Fail("Relácia vypršala. Prihláste sa znova."),
HttpStatusCode.TooManyRequests =>
ApiResponse<List<ProductDto>>
.Fail("Príliš veľa požiadaviek. Skúste neskôr."),
_ => ApiResponse<List<ProductDto>>
.Fail($"Chyba servera: {(int)response.StatusCode}")
};
}
catch (TaskCanceledException)
{
return ApiResponse<List<ProductDto>>
.Fail("Požiadavka bola zrušená alebo vypršal timeout.");
}
catch (HttpRequestException ex)
{
return ApiResponse<List<ProductDto>>
.Fail($"Chyba siete: {ex.Message}");
}
}
// Pomocná trieda pre štruktúrované odpovede
public class ApiResponse<T>
{
public bool IsSuccess { get; init; }
public T? Data { get; init; }
public string? Error { get; init; }
public static ApiResponse<T> Success(T data) =>
new() { IsSuccess = true, Data = data };
public static ApiResponse<T> Fail(string error) =>
new() { IsSuccess = false, Error = error };
}
POST — vytváranie záznamov
Pri POST požiadavkách odosielate dáta na server a typicky dostávate späť vytvorený objekt aj s priradeným ID. Tu je dôležité správne spracovať aj validačné chyby — server vám môže povedať, že niečo v dátach nesedí:
// POST s validáciou odpovede
public async Task<ApiResponse<ProductDto>> CreateProductAsync(
CreateProductRequest request, CancellationToken ct = default)
{
try
{
var response = await _httpClient
.PostAsJsonAsync("api/products", request, ct);
if (response.StatusCode == HttpStatusCode.Created)
{
var product = await response.Content
.ReadFromJsonAsync<ProductDto>(ct);
return ApiResponse<ProductDto>.Success(product!);
}
// Spracovanie validačných chýb zo servera
if (response.StatusCode == HttpStatusCode.BadRequest)
{
var errorBody = await response.Content.ReadAsStringAsync(ct);
return ApiResponse<ProductDto>
.Fail($"Validačná chyba: {errorBody}");
}
return ApiResponse<ProductDto>
.Fail($"Neočakávaná chyba: {(int)response.StatusCode}");
}
catch (HttpRequestException ex)
{
return ApiResponse<ProductDto>.Fail($"Chyba siete: {ex.Message}");
}
}
PUT a DELETE
PUT aktualizuje existujúci záznam, DELETE ho odstráni. Nič prekvapivé. Obe operácie by mali vracať jasný výsledok o tom, čo sa stalo:
// PUT — aktualizácia existujúceho záznamu
public async Task<ApiResponse<ProductDto>> UpdateProductAsync(
int id, UpdateProductRequest request, CancellationToken ct = default)
{
try
{
var response = await _httpClient
.PutAsJsonAsync($"api/products/{id}", request, ct);
if (response.IsSuccessStatusCode)
{
var updated = await response.Content
.ReadFromJsonAsync<ProductDto>(ct);
return ApiResponse<ProductDto>.Success(updated!);
}
if (response.StatusCode == HttpStatusCode.NotFound)
return ApiResponse<ProductDto>
.Fail("Produkt nebol nájdený.");
if (response.StatusCode == HttpStatusCode.Conflict)
return ApiResponse<ProductDto>
.Fail("Konflikt — produkt bol medzitým zmenený.");
return ApiResponse<ProductDto>
.Fail($"Chyba: {(int)response.StatusCode}");
}
catch (HttpRequestException ex)
{
return ApiResponse<ProductDto>.Fail($"Chyba siete: {ex.Message}");
}
}
// DELETE — odstránenie záznamu
public async Task<ApiResponse<bool>> DeleteProductAsync(
int id, CancellationToken ct = default)
{
try
{
var response = await _httpClient
.DeleteAsync($"api/products/{id}", ct);
return response.StatusCode switch
{
HttpStatusCode.NoContent or HttpStatusCode.OK
=> ApiResponse<bool>.Success(true),
HttpStatusCode.NotFound
=> ApiResponse<bool>.Fail("Produkt nebol nájdený."),
_ => ApiResponse<bool>
.Fail($"Chyba: {(int)response.StatusCode}")
};
}
catch (HttpRequestException ex)
{
return ApiResponse<bool>.Fail($"Chyba siete: {ex.Message}");
}
}
Všimnite si jednu vec, ktorú veľa ľudí prehliada — každá metóda prijíma CancellationToken. Prečo je to dôležité? Keď používateľ opustí stránku skôr, než sa požiadavka dokončí, token zabezpečí jej zrušenie a uvoľnenie zdrojov. Bez toho vám požiadavky veselo bežia na pozadí, aj keď ich výsledok už nikoho nezaujíma.
Serializácia JSON s System.Text.Json
V .NET MAUI je System.Text.Json predvolený serializátor a pre väčšinu situácií je výkonnejší než Newtonsoft.Json. Ale pozor — ak plánujete používať NativeAOT alebo trimming (a v .NET 10 je to čoraz bežnejšie), musíte prejsť na zdrojové generátory (source generators). Bežná serializácia stojí na reflexii a tá sa s AOT kompiláciou jednoducho neznáša.
JsonSerializerContext pre AOT kompatibilitu
Zdrojové generátory vygenerujú serializačný kód už v čase kompilácie. Žiadna reflexia za behu, rýchlejší štart aplikácie, menší výsledný balík. Znie to ako win-win, a vlastne to tak aj je:
// Definícia kontextu pre zdrojový generátor
[JsonSerializable(typeof(ProductDto))]
[JsonSerializable(typeof(List<ProductDto>))]
[JsonSerializable(typeof(CreateProductRequest))]
[JsonSerializable(typeof(UpdateProductRequest))]
[JsonSourceGenerationOptions(
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
public partial class AppJsonContext : JsonSerializerContext
{
}
A takto to potom vyzerá v API volaniach:
// Serializácia s vygenerovaným kontextom
var json = JsonSerializer.Serialize(request,
AppJsonContext.Default.CreateProductRequest);
// Deserializácia s vygenerovaným kontextom
var products = await JsonSerializer.DeserializeAsync(
responseStream,
AppJsonContext.Default.ListProductDto,
cancellationToken);
// Alebo pri použití s HttpClient extensions
var response = await _httpClient.PostAsJsonAsync(
"api/products", request,
AppJsonContext.Default.CreateProductRequest, ct);
A tie výhody nie sú len teoretické — v benchmarkoch zdrojové generátory dosahujú až 40 % rýchlejšiu serializáciu a eliminujú alokácie spojené s reflexiou. V mobilnom prostredí, kde každá milisekunda a kilobajt pamäte fakt počíta, to rozhodne nie je zanedbateľné.
Platforma-špecifické HTTP handlery
.NET MAUI štandardne používa spravovaný (managed) HTTP handler. Ale na oboch hlavných mobilných platformách existujú natívne handlery, ktoré ponúkajú lepší výkon a plnú integráciu s platformovým TLS stackom. A úprimne — nemám dôvod ich nepoužiť.
NSUrlSessionHandler na iOS
Na iOS NSUrlSessionHandler deleguje HTTP komunikáciu na natívny NSUrlSession. To znamená plnú podporu App Transport Security (ATS), automatickú správu certifikátov a optimalizovaný networking stack priamo od Apple. Na Androide je situácia obdobná s AndroidMessageHandler:
// Registrácia natívnych handlerov podľa platformy
builder.Services.AddHttpClient("ProductApi", client =>
{
client.BaseAddress = new Uri("https://api.vasadomena.com/");
})
.ConfigurePrimaryHttpMessageHandler(() =>
{
#if IOS
// Natívny iOS handler — lepšia integrácia s ATS
return new NSUrlSessionHandler
{
AllowAutoRedirect = true,
MaxConnectionsPerHost = 6
};
#elif ANDROID
// Natívny Android handler
return new AndroidMessageHandler
{
AllowAutoRedirect = true,
AutomaticDecompression = DecompressionMethods.GZip
| DecompressionMethods.Deflate
};
#else
return new HttpClientHandler
{
AllowAutoRedirect = true,
AutomaticDecompression = DecompressionMethods.GZip
| DecompressionMethods.Deflate
};
#endif
});
Čo sa zmenilo s TLS v .NET 10
.NET 10 prináša vylepšenú podporu TLS 1.3 naprieč platformami. Na Androide AndroidMessageHandler teraz plne podporuje TLS 1.3 bez nutnosti akejkoľvek explicitnej konfigurácie — jednoducho to funguje. Natívne handlery automaticky využívajú najnovšie bezpečnostné aktualizácie platformy, čo je podstatná výhoda oproti spravovanému handleru, ktorý závisí od .NET runtime.
Kedy teda použiť natívny a kedy spravovaný handler? Krátka odpoveď: natívne handlery odporúčam vždy, keď vaša aplikácia cieli výhradne na mobilné platformy. Spravovaný handler dáva zmysel jedine pri zdieľaní kódu s desktopovou alebo serverovou aplikáciou, kde natívne handlery nie sú k dispozícii.
Kontrola sieťového pripojenia s IConnectivity
Mobilné zariadenia sú neustále v pohybe. Prechádzajú medzi Wi-Fi a mobilnými dátami, vstupujú do tunelov, dostávajú sa mimo dosah signálu. To je jednoducho realita mobilného sveta. Pred každým API volaním by ste mali skontrolovať dostupnosť siete — používateľ ocení zmysluplnú správu typu "ste offline" oveľa viac než nejaký kryptický error po 30 sekundách čakania.
.NET MAUI na toto poskytuje rozhranie IConnectivity. Dobrá správa: je už zaregistrované v DI kontajneri, takže ho jednoducho injektujete a ide sa:
// Služba s kontrolou pripojenia pred API volaním
public class ConnectedProductService : IProductService
{
private readonly IProductService _innerService;
private readonly IConnectivity _connectivity;
public ConnectedProductService(
[FromKeyedServices("api")] IProductService innerService,
IConnectivity connectivity)
{
_innerService = innerService;
_connectivity = connectivity;
}
public async Task<List<ProductDto>> GetAllProductsAsync()
{
// Kontrola pripojenia pred volaním API
if (_connectivity.NetworkAccess != NetworkAccess.Internet)
{
throw new InvalidOperationException(
"Žiadne internetové pripojenie. " +
"Skontrolujte nastavenia siete.");
}
return await _innerService.GetAllProductsAsync();
}
// ... ďalšie metódy s rovnakou kontrolou
}
Keď sa pripojenie zmení za behu
Okrem jednorazovej kontroly môžete reagovať na zmeny stavu pripojenia v reálnom čase. Toto je fajn napríklad pre automatickú synchronizáciu po obnovení pripojenia — používateľ sa vráti z tunela a dáta sa mu sami načítajú:
// Sledovanie zmien pripojenia vo ViewModeli
public partial class ProductListViewModel : ObservableObject
{
private readonly IConnectivity _connectivity;
[ObservableProperty]
private bool _isOffline;
public ProductListViewModel(
IProductService productService,
IConnectivity connectivity)
{
_connectivity = connectivity;
// Registrácia handlera pre zmeny pripojenia
_connectivity.ConnectivityChanged += OnConnectivityChanged;
// Počiatočný stav
IsOffline = _connectivity.NetworkAccess != NetworkAccess.Internet;
}
private async void OnConnectivityChanged(
object? sender, ConnectivityChangedEventArgs e)
{
IsOffline = e.NetworkAccess != NetworkAccess.Internet;
// Automatické načítanie dát po obnovení pripojenia
if (!IsOffline)
{
await LoadProductsAsync();
}
}
}
Jedna dôležitá vec — nezabudnite sa odhlásiť z udalosti pri zrušení ViewModelu, inak vám hrozia úniky pamäte. Alebo použite WeakReferenceMessenger z CommunityToolkit, čo sme riešili v článku o MVVM architektúre.
Odolnosť voči chybám — retry, timeout a circuit breaker
Sieťová komunikácia je zo svojej podstaty nespoľahlivá. Požiadavky zlyhávajú kvôli preťaženiu servera, dočasným výpadkom siete, timeoutom... jednoducho sa to deje. Namiesto toho, aby ste tieto scenáre riešili ručne v každej jednej metóde (čo je únavné a náchylné na chyby), použite Microsoft.Extensions.Http.Resilience — oficiálny balíček od Microsoftu postavený na Polly v8.
Pridajte NuGet balíček:
dotnet add package Microsoft.Extensions.Http.Resilience
StandardResilienceHandler — jednoducho a účinne
Najjednoduchší spôsob, ako pridať odolnosť, je zavolať AddStandardResilienceHandler(). Jeden riadok kódu a máte nakonfigurovaný kompletný pipeline odolnosti — retry logiku, circuit breaker, timeout aj rate limiter:
// MauiProgram.cs — pridanie štandardnej odolnosti
builder.Services.AddHttpClient<IProductService, ProductService>(client =>
{
client.BaseAddress = new Uri("https://api.vasadomena.com/");
})
.AddStandardResilienceHandler();
Štandardný handler obsahuje tieto stratégie (v tomto poradí):
- Rate limiter — obmedzuje počet súbežných požiadaviek
- Total request timeout — celkový timeout vrátane retries (predvolene 30 sekúnd)
- Retry — opakuje zlyhané požiadavky s exponenciálnym odstupom (predvolene 3 pokusy)
- Circuit breaker — po sérii zlyhaní úplne zastaví požiadavky na určitý čas
- Attempt timeout — timeout pre jednotlivý pokus (predvolene 10 sekúnd)
Prispôsobenie retry politiky pre mobil
Pre mobilné aplikácie budete skoro určite chcieť prispôsobiť predvolené hodnoty. Kratšie timeouty, menej retry pokusov — nikto na mobile nebude trpezlivo čakať 30 sekúnd, kým sa niečo načíta:
// Vlastná konfigurácia odolnosti pre mobilné prostredie
builder.Services.AddHttpClient<IProductService, ProductService>(client =>
{
client.BaseAddress = new Uri("https://api.vasadomena.com/");
})
.AddStandardResilienceHandler(options =>
{
// Celkový timeout — mobilní používatelia čakajú menej
options.TotalRequestTimeout.Timeout = TimeSpan.FromSeconds(20);
// Retry — 2 pokusy namiesto 3, kratšie intervaly
options.Retry.MaxRetryAttempts = 2;
options.Retry.Delay = TimeSpan.FromMilliseconds(500);
options.Retry.BackoffType = DelayBackoffType.Exponential;
options.Retry.UseJitter = true;
// Neopakovať POST požiadavky — mohli by spôsobiť duplikáty
options.Retry.ShouldHandle = args =>
{
if (args.Outcome.Result?.RequestMessage?.Method == HttpMethod.Post)
return ValueTask.FromResult(false);
return ValueTask.FromResult(
args.Outcome.Exception is HttpRequestException
|| args.Outcome.Result?.StatusCode
is HttpStatusCode.RequestTimeout
or HttpStatusCode.TooManyRequests
or HttpStatusCode.ServiceUnavailable);
};
// Circuit breaker — rýchlejšie otvorenie pre mobilné aplikácie
options.CircuitBreaker.SamplingDuration = TimeSpan.FromSeconds(10);
options.CircuitBreaker.BreakDuration = TimeSpan.FromSeconds(5);
options.CircuitBreaker.FailureRatio = 0.5;
// Timeout pre jednotlivý pokus
options.AttemptTimeout.Timeout = TimeSpan.FromSeconds(8);
});
Všimnite si jeden dôležitý detail — POST požiadavky zámerne neopakujeme. A má to dobrý dôvod: keď POST zlyhá s timeout chybou, nemáte absolútne žiadnu istotu, či server požiadavku spracoval alebo nie. Opakované odoslanie by mohlo vytvoriť duplikátny záznam. Pri POST operáciách je lepšie informovať používateľa a nechať rozhodnutie na ňom.
Kompletný príklad — všetko pokope
Fajn, poďme si teraz poskladať všetky kúsky do jedného funkčného celku. Nasledujúci príklad ukazuje kompletnú konfiguráciu v MauiProgram.cs, servisnú vrstvu s kontrolou pripojenia a ViewModel, ktorý to celé konzumuje.
MauiProgram.cs — kompletná registrácia
using Microsoft.Extensions.Http.Resilience;
using System.Net;
using System.Net.Http.Headers;
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
});
// Registrácia HttpClient s typovaným klientom
builder.Services
.AddHttpClient<IProductService, ProductService>(client =>
{
client.BaseAddress =
new Uri("https://api.vasadomena.com/");
client.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue(
"application/json"));
})
// Natívne handlery podľa platformy
.ConfigurePrimaryHttpMessageHandler(() =>
{
#if IOS
return new NSUrlSessionHandler();
#elif ANDROID
return new AndroidMessageHandler
{
AutomaticDecompression =
DecompressionMethods.GZip
| DecompressionMethods.Deflate
};
#else
return new HttpClientHandler();
#endif
})
// Odolnosť voči chybám
.AddStandardResilienceHandler(options =>
{
options.TotalRequestTimeout.Timeout =
TimeSpan.FromSeconds(20);
options.Retry.MaxRetryAttempts = 2;
options.Retry.Delay =
TimeSpan.FromMilliseconds(500);
options.Retry.BackoffType =
DelayBackoffType.Exponential;
options.Retry.UseJitter = true;
options.AttemptTimeout.Timeout =
TimeSpan.FromSeconds(8);
});
// Registrácia IConnectivity
builder.Services.AddSingleton(
Connectivity.Current);
// Registrácia ViewModelov
builder.Services
.AddTransient<ProductListViewModel>();
builder.Services
.AddTransient<ProductDetailViewModel>();
// Registrácia stránok
builder.Services.AddTransient<ProductListPage>();
builder.Services.AddTransient<ProductDetailPage>();
return builder.Build();
}
}
Kompletný ProductService
// Kompletná implementácia servisnej vrstvy
public class ProductService : IProductService
{
private readonly HttpClient _httpClient;
private readonly IConnectivity _connectivity;
private readonly ILogger<ProductService> _logger;
public ProductService(
HttpClient httpClient,
IConnectivity connectivity,
ILogger<ProductService> logger)
{
_httpClient = httpClient;
_connectivity = connectivity;
_logger = logger;
}
private void EnsureConnectivity()
{
if (_connectivity.NetworkAccess != NetworkAccess.Internet)
throw new InvalidOperationException(
"Nie je dostupné internetové pripojenie.");
}
public async Task<ApiResponse<List<ProductDto>>>
GetAllProductsAsync(CancellationToken ct = default)
{
EnsureConnectivity();
try
{
var products = await _httpClient
.GetFromJsonAsync(
"api/products",
AppJsonContext.Default.ListProductDto,
ct);
return ApiResponse<List<ProductDto>>
.Success(products ?? []);
}
catch (HttpRequestException ex)
{
_logger.LogError(ex,
"Zlyhalo načítanie produktov");
return ApiResponse<List<ProductDto>>
.Fail("Nepodarilo sa načítať produkty.");
}
catch (TaskCanceledException)
{
return ApiResponse<List<ProductDto>>
.Fail("Požiadavka vypršala.");
}
}
public async Task<ApiResponse<ProductDto>>
CreateProductAsync(
CreateProductRequest request,
CancellationToken ct = default)
{
EnsureConnectivity();
try
{
var response = await _httpClient
.PostAsJsonAsync(
"api/products",
request,
AppJsonContext.Default.CreateProductRequest,
ct);
if (response.IsSuccessStatusCode)
{
var product = await response.Content
.ReadFromJsonAsync(
AppJsonContext.Default.ProductDto, ct);
return ApiResponse<ProductDto>
.Success(product!);
}
return ApiResponse<ProductDto>
.Fail($"Chyba: {(int)response.StatusCode}");
}
catch (HttpRequestException ex)
{
_logger.LogError(ex,
"Zlyhalo vytváranie produktu");
return ApiResponse<ProductDto>
.Fail("Nepodarilo sa vytvoriť produkt.");
}
}
}
Kompletný ProductListViewModel
// ViewModel s kompletnou logikou
[ObservableObject]
public partial class ProductListViewModel
{
private readonly IProductService _productService;
private readonly IConnectivity _connectivity;
[ObservableProperty]
private ObservableCollection<ProductDto> _products = [];
[ObservableProperty]
private bool _isLoading;
[ObservableProperty]
private bool _isOffline;
[ObservableProperty]
private string _statusMessage = string.Empty;
public ProductListViewModel(
IProductService productService,
IConnectivity connectivity)
{
_productService = productService;
_connectivity = connectivity;
_connectivity.ConnectivityChanged +=
OnConnectivityChanged;
IsOffline = _connectivity.NetworkAccess
!= NetworkAccess.Internet;
}
[RelayCommand]
private async Task LoadProductsAsync()
{
if (IsOffline)
{
StatusMessage = "Ste offline. " +
"Zobrazujú sa lokálne dáta.";
return;
}
try
{
IsLoading = true;
StatusMessage = string.Empty;
var result = await _productService
.GetAllProductsAsync();
if (result.IsSuccess)
{
Products = new ObservableCollection<ProductDto>(
result.Data!);
StatusMessage =
$"Načítaných {Products.Count} produktov";
}
else
{
StatusMessage = result.Error
?? "Neznáma chyba";
}
}
finally
{
IsLoading = false;
}
}
private async void OnConnectivityChanged(
object? sender,
ConnectivityChangedEventArgs e)
{
IsOffline = e.NetworkAccess != NetworkAccess.Internet;
if (!IsOffline && Products.Count == 0)
{
await LoadProductsAsync();
}
}
}
Takto teda jednotlivé koncepty spolupracujú v praxi. HttpClientFactory s typovaným klientom sa stará o životný cyklus HTTP spojení, natívne handlery zabezpečujú optimálny výkon na každej platforme, resilience handler rieši opakovanie zlyhaných požiadaviek a kontrola pripojenia predchádza zbytočným volaniam. A to všetko za čistým rozhraním, ktoré ViewModel konzumuje bez toho, aby čokoľvek vedel o implementačných detailoch pod kapotou.
Často kladené otázky (FAQ)
Ako správne používať HttpClient v .NET MAUI?
Hlavné pravidlo: nikdy nevytvárajte novú inštanciu HttpClient pre každú požiadavku. Viem, že to znie jednoducho, ale je to prekvapivo častá chyba a vedie k vyčerpaniu socketov. Namiesto toho použite HttpClientFactory z balíčka Microsoft.Extensions.Http — ten spravuje pool HTTP handlerov a automaticky rotuje DNS záznamy. Zaregistrujte ho v MauiProgram.cs pomocou AddHttpClient() a injektujte buď IHttpClientFactory (pri pomenovaných klientoch) alebo priamo HttpClient (pri typovaných klientoch) do svojich služieb.
Aký je rozdiel medzi HttpClient a HttpClientFactory v MAUI?
HttpClient je trieda na odosielanie HTTP požiadaviek. Ak ju používate ako singleton, nerešpektuje zmeny DNS. Ak ju vytvárate nanovo pre každú požiadavku, vyčerpáte sockety. Taký klasický catch-22. HttpClientFactory rieši oba tieto problémy naraz — interne spravuje pool HttpMessageHandler inštancií s 2-minútovou životnosťou a vytvára ľahké HttpClient obálky, ktoré môžete pokojne vytvárať aj zahadzovať. V mobilných aplikáciách by ste mali vždy siahnuť po HttpClientFactory.
Ako riešiť problémy so sieťou v mobilnej aplikácii?
Najlepšie funguje trojitá stratégia (a áno, naozaj odporúčam všetky tri). Pred volaním skontrolujte pripojenie cez IConnectivity. Na úrovni HTTP klienta nakonfigurujte Microsoft.Extensions.Http.Resilience s retry logikou a circuit breakerom. A na úrovni UI reagujte na zmeny pripojenia cez udalosť ConnectivityChanged a zobrazujte offline stav. A ešte jedno — nezabudnite na CancellationToken pri každom API volaní. Umožňuje zrušiť prebiehajúce požiadavky, keď používateľ naviguje preč zo stránky.
Je lepšie použiť Refit alebo vlastný HttpClient v .NET MAUI?
Záleží na situácii (viem, tá odpoveď nikoho nebaví, ale je to tak). Refit je skvelý pre deklaratívne definovanie API rozhraní — generuje HTTP volania z anotácií na interface metódach, čo výrazne znižuje množstvo boilerplate kódu. Háčik je v tom, že závisí na reflexii, a to môže byť problém s NativeAOT. Vlastný HttpClient s typovanými klientmi z HttpClientFactory vám dáva plnú kontrolu, AOT kompatibilitu a lepšie možnosti debugovania. Takže pre väčšie projekty s množstvom endpointov zvážte Refit; pre projekty s AOT požiadavkami alebo komplexnejšou logikou (vlastné retry, cache a podobne) sa držte vlastnej implementácie.
Ako zabezpečiť API volania v .NET MAUI aplikácii?
Základ je HTTPS pre všetku komunikáciu — nikdy, ale naozaj nikdy neposielajte dáta cez nešifrované HTTP. Používajte natívne HTTP handlery (NSUrlSessionHandler, AndroidMessageHandler) pre plnú integráciu s platformovým TLS stackom. Tokeny ukladajte výhradne do SecureStorage, nie do Preferences ani do nejakého súboru. Pre citlivé API zvážte certificate pinning pomocou vlastného HttpMessageHandler. A nezabudnite na autorizačné hlavičky — implementujte DelegatingHandler, ktorý automaticky pridáva Bearer token ku každej požiadavke a obnovuje ho pred vypršaním. Toto vám ušetrí kopu opakujúceho sa kódu.