REST API v .NET MAUI: Kompletný sprievodca HttpClient, servisnou vrstvou a odolnosťou voči chybám

Naučte sa správne integrovať REST API v .NET MAUI — od HttpClientFactory cez MVVM servisnú vrstvu, platformové handlery, kontrolu pripojenia až po retry a circuit breaker vzory. Praktické príklady pripravené na okamžité použitie.

Ú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.

O Autorovi Editorial Team

Our team of expert writers and editors.