.NET MAUI REST API hívások HttpClient-tel: Az alapoktól a haladó mintákig

Építs robusztus REST API réteget .NET MAUI-ban: HttpClient, IHttpClientFactory, Refit, platform-natív handlerek, hibakezelés és token-alapú autentikáció — működő kódpéldákkal.

.NET MAUI HttpClient REST API útmutató 2026

Bevezetés

Legyünk őszinték: manapság szinte lehetetlen olyan mobilalkalmazást találni, ami ne kommunikálna valamilyen szerver oldali API-val. Felhasználói adatok lekérdezése, képek feltöltése, push notification tokenek regisztrálása — mindez HTTP kérésen keresztül történik. A .NET MAUI világában a HttpClient az elsődleges eszközünk erre, de meglepően sok csapda vár a fejlesztőre, aki nem ismeri a helyes használat szabályait.

Ebben az útmutatóban végigvezetlek azon, hogyan építhetsz robusztus REST API réteget a .NET MAUI alkalmazásodban. Szó lesz az IHttpClientFactory használatáról, a platform-natív handlerekről, a Refit könyvtárról (ami őszintén szólva megváltoztatja az életed), a hibakezelésről, az autentikációról, és még az offline szinkronizálásról is. Gyakorlati kódpéldákat kapsz, amiket azonnal bevethetsz a projektedben.

Miért ne hozz létre saját HttpClient példányt?

Az egyik leggyakoribb hiba — amit sajnos rengeteg .NET MAUI fejlesztő elkövet — hogy minden API híváshoz új HttpClient példányt hoznak létre. Ez két komoly problémát okoz:

  • Socket exhaustion — Amikor egy HttpClient példányt megszüntetsz, az alatta lévő socket nem szabadul fel azonnal. Ha az alkalmazásod gyakran hív API-t, hamar elfogynak a rendelkezésre álló socketek. Ez az a fajta bug, amit nehéz debugolni, mert fejlesztés közben ritkán jön elő.
  • DNS-változások figyelmen kívül hagyása — Ha singleton-ként használod a HttpClient-et, az nem veszi észre a DNS-rekordok változásait. Terheléselosztó vagy failover környezetben ez komoly gondot okozhat.

A megoldás? Az IHttpClientFactory használata, ami mindkét problémát elegánsan kezeli.

IHttpClientFactory beállítása .NET MAUI-ban

Az IHttpClientFactory a .NET beépített megoldása a HttpClient példányok életciklusának kezelésére. A .NET MAUI projektekben a dependency injection rendszeren keresztül regisztrálhatod a MauiProgram.cs fájlban — a folyamat nagyon hasonlít ahhoz, amit ASP.NET Core-ból már ismerhetsz.

Alap konfiguráció

// MauiProgram.cs
using Microsoft.Extensions.Http;

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        builder
            .UseMauiApp<App>()
            .ConfigureFonts(fonts =>
            {
                fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
            });

        // HttpClient regisztráció IHttpClientFactory-val
        builder.Services.AddHttpClient("ApiClient", client =>
        {
            client.BaseAddress = new Uri("https://api.example.com/");
            client.DefaultRequestHeaders.Add("Accept", "application/json");
            client.Timeout = TimeSpan.FromSeconds(30);
        });

        // Szolgáltatások regisztrálása
        builder.Services.AddSingleton<IApiService, ApiService>();
        builder.Services.AddTransient<MainViewModel>();

        return builder.Build();
    }
}

Named vs. Typed HttpClient

A fenti példa úgynevezett named client-et, vagyis névvel ellátott klienst használ. Egyszerű esetekben ez tökéletesen működik.

Nagyobb projektekben viszont érdemes typed client-re váltani, ami típusbiztos hozzáférést biztosít:

// Typed HttpClient regisztráció
builder.Services.AddHttpClient<IApiService, ApiService>(client =>
{
    client.BaseAddress = new Uri("https://api.example.com/");
    client.DefaultRequestHeaders.Add("Accept", "application/json");
});

A typed client esetén az ApiService konstruktorába közvetlenül injektálódik egy HttpClient példány, amit az IHttpClientFactory kezel a háttérben. Szerintem ez a legtisztább megközelítés a legtöbb projektnél.

Platform-natív HTTP handlerek

Egy dolog, amit sokan nem tudnak: a .NET MAUI alapértelmezetten platform-natív HTTP handlereket használ. Ez komoly teljesítményelőny, mert minden platformon az operációs rendszer saját hálózati rétegét kapod:

  • AndroidAndroidMessageHandler, ami a natív Java/Android hálózati stackre épül
  • iOSNSUrlSessionHandler, ami az NSURLSession API-t használja
  • WindowsWinHttpHandler, a Windows natív HTTP implementációja

Az előnyök? Jobb teljesítmény, kisebb alkalmazásméret, TLS 1.2/1.3 támogatás, és a platform tanúsítvány-kezelésének automatikus használata. Gyakorlatilag ingyen kapod mindezt.

Ha szükséged van a natív handler testreszabására, a ConfigurePrimaryHttpMessageHandler metódussal teheted meg:

builder.Services.AddHttpClient("ApiClient", client =>
{
    client.BaseAddress = new Uri("https://api.example.com/");
})
.ConfigurePrimaryHttpMessageHandler(() =>
{
#if ANDROID
    return new AndroidMessageHandler
    {
        AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate
    };
#elif IOS
    return new NSUrlSessionHandler
    {
        AllowAutoRedirect = true
    };
#else
    return new SocketsHttpHandler
    {
        AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,
        PooledConnectionLifetime = TimeSpan.FromMinutes(5)
    };
#endif
});

API szolgáltatás réteg felépítése

Na, itt jön a lényeg. A legjobb megközelítés, ha az API hívásokat egy dedikált szolgáltatásrétegbe szervezed. Így a kód újrafelhasználható, tesztelhető, és a ViewModelek nem függenek közvetlenül a HTTP implementációtól.

Adatmodell

using System.Text.Json.Serialization;

public class Termek
{
    [JsonPropertyName("id")]
    public int Id { get; set; }

    [JsonPropertyName("name")]
    public string Nev { get; set; } = string.Empty;

    [JsonPropertyName("price")]
    public decimal Ar { get; set; }

    [JsonPropertyName("description")]
    public string Leiras { get; set; } = string.Empty;

    [JsonPropertyName("category")]
    public string Kategoria { get; set; } = string.Empty;

    [JsonPropertyName("imageUrl")]
    public string KepUrl { get; set; } = string.Empty;
}

Az API szolgáltatás interfész és implementáció

public interface IApiService
{
    Task<List<Termek>> TermekekLekerdezese();
    Task<Termek?> TermekLekerdezese(int id);
    Task<Termek> TermekLetrehozasa(Termek termek);
    Task<bool> TermekFrissitese(Termek termek);
    Task<bool> TermekTorlese(int id);
}

public class ApiService : IApiService
{
    private readonly HttpClient _httpClient;
    private readonly JsonSerializerOptions _jsonOptions;

    public ApiService(HttpClient httpClient)
    {
        _httpClient = httpClient;
        _jsonOptions = new JsonSerializerOptions
        {
            PropertyNameCaseInsensitive = true,
            DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
        };
    }

    public async Task<List<Termek>> TermekekLekerdezese()
    {
        // Stream-alapú deszerializálás a jobb memóriahatékonyságért
        using var response = await _httpClient.GetAsync("api/products");
        response.EnsureSuccessStatusCode();

        await using var stream = await response.Content.ReadAsStreamAsync();
        var termekek = await JsonSerializer
            .DeserializeAsync<List<Termek>>(stream, _jsonOptions);
        return termekek ?? new List<Termek>();
    }

    public async Task<Termek?> TermekLekerdezese(int id)
    {
        using var response = await _httpClient.GetAsync($"api/products/{id}");
        if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
            return null;

        response.EnsureSuccessStatusCode();
        await using var stream = await response.Content.ReadAsStreamAsync();
        return await JsonSerializer.DeserializeAsync<Termek>(stream, _jsonOptions);
    }

    public async Task<Termek> TermekLetrehozasa(Termek termek)
    {
        var json = JsonSerializer.Serialize(termek, _jsonOptions);
        using var content = new StringContent(json, Encoding.UTF8, "application/json");
        using var response = await _httpClient.PostAsync("api/products", content);
        response.EnsureSuccessStatusCode();

        await using var stream = await response.Content.ReadAsStreamAsync();
        return await JsonSerializer.DeserializeAsync<Termek>(stream, _jsonOptions)
            ?? throw new InvalidOperationException("A szerver nem adott vissza terméket.");
    }

    public async Task<bool> TermekFrissitese(Termek termek)
    {
        var json = JsonSerializer.Serialize(termek, _jsonOptions);
        using var content = new StringContent(json, Encoding.UTF8, "application/json");
        using var response = await _httpClient
            .PutAsync($"api/products/{termek.Id}", content);
        return response.IsSuccessStatusCode;
    }

    public async Task<bool> TermekTorlese(int id)
    {
        using var response = await _httpClient
            .DeleteAsync($"api/products/{id}");
        return response.IsSuccessStatusCode;
    }
}

Stream-alapú JSON deszerializálás

Érdemes megfigyelni, hogy a fenti kódban ReadAsStreamAsync() metódust használunk ReadAsStringAsync() helyett. Miért? Mert a stream-alapú deszerializálás nem tölti be a teljes választ a memóriába — ehelyett folyamatosan olvassa és dolgozza fel az adatokat. Nagy méretű API válaszoknál ez jelentős memóriamegtakarítást jelent, ami mobilon különösen fontos.

Refit: automatikus API-kliens generálás

Ha minimalizálni akarod a boilerplate kódot (és őszintén, ki ne akarná?), a Refit könyvtár tökéletes megoldás. A Refit egy C# interfészből automatikusan generálja a teljes HTTP kliens implementációt. Először telepítsd a szükséges NuGet csomagot:

dotnet add package Refit.HttpClientFactory

API interfész definiálása Refit-tel

using Refit;

public interface ITermekApi
{
    [Get("/api/products")]
    Task<List<Termek>> OsszesTermek();

    [Get("/api/products/{id}")]
    Task<Termek> TermekById(int id);

    [Post("/api/products")]
    Task<Termek> UjTermek([Body] Termek termek);

    [Put("/api/products/{id}")]
    Task FrissitTermek(int id, [Body] Termek termek);

    [Delete("/api/products/{id}")]
    Task TorolTermek(int id);
}

Refit regisztráció DI-vel

// MauiProgram.cs
builder.Services.AddRefitClient<ITermekApi>()
    .ConfigureHttpClient(client =>
    {
        client.BaseAddress = new Uri("https://api.example.com");
    });

És ennyi — nem vicc. A Refit automatikusan generálja az implementációt, beleértve a JSON szerializálást és deszerializálást is. A ViewModel-ben egyszerűen injektálod az ITermekApi-t:

public partial class TermekekViewModel : ObservableObject
{
    private readonly ITermekApi _api;

    public TermekekViewModel(ITermekApi api)
    {
        _api = api;
    }

    [ObservableProperty]
    private ObservableCollection<Termek> termekek = new();

    [RelayCommand]
    private async Task TermekekBetoltese()
    {
        var lista = await _api.OsszesTermek();
        Termekek = new ObservableCollection<Termek>(lista);
    }
}

Hibakezelés és újrapróbálkozás

Sajnos a hálózati kérések természetüknél fogva megbízhatatlanok. A kapcsolat megszakadhat, a szerver túlterhelt lehet, vagy egyszerűen timeout következhet be. Mobilon ez még gyakoribb — gondolj csak arra, amikor a felhasználó metrón ül és a hálózat folyamatosan ki-be kapcsol.

Érdemes felkészülni ezekre a helyzetekre.

Központosított hibakezelés DelegatingHandler-rel

public class ApiHibaHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        try
        {
            var response = await base.SendAsync(request, cancellationToken);

            if (!response.IsSuccessStatusCode)
            {
                var body = await response.Content.ReadAsStringAsync(cancellationToken);
                throw response.StatusCode switch
                {
                    HttpStatusCode.Unauthorized =>
                        new UnauthorizedAccessException("A munkamenet lejárt. Kérlek, jelentkezz be újra."),
                    HttpStatusCode.Forbidden =>
                        new UnauthorizedAccessException("Nincs jogosultságod ehhez a művelethez."),
                    HttpStatusCode.NotFound =>
                        new KeyNotFoundException("A kért erőforrás nem található."),
                    HttpStatusCode.TooManyRequests =>
                        new InvalidOperationException("Túl sok kérés. Próbáld újra később."),
                    _ => new HttpRequestException(
                        $"API hiba: {(int)response.StatusCode} - {body}")
                };
            }

            return response;
        }
        catch (TaskCanceledException) when (!cancellationToken.IsCancellationRequested)
        {
            throw new TimeoutException("A kérés időtúllépés miatt megszakadt.");
        }
    }
}

// Regisztráció
builder.Services.AddTransient<ApiHibaHandler>();
builder.Services.AddHttpClient("ApiClient", client =>
{
    client.BaseAddress = new Uri("https://api.example.com/");
})
.AddHttpMessageHandler<ApiHibaHandler>();

Polly-alapú újrapróbálkozás

A Microsoft.Extensions.Http.Polly NuGet csomaggal automatikus újrapróbálkozást konfigurálhatsz átmeneti hibák esetére. Ezt személy szerint az egyik leghasznosabb kiegészítőnek tartom:

using Microsoft.Extensions.Http;
using Polly;

builder.Services.AddHttpClient("ApiClient", client =>
{
    client.BaseAddress = new Uri("https://api.example.com/");
})
.AddTransientHttpErrorPolicy(policy =>
    policy.WaitAndRetryAsync(3, retryAttempt =>
        TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))));

Ez a konfiguráció háromszor próbálja újra a kérést exponenciálisan növekvő várakozási idővel (2, 4, majd 8 másodperc), de kizárólag átmeneti hibák — tehát 5xx válaszkódok és hálózati hibák — esetén. Nem próbálkozik újra mondjuk egy 404-esnél, ami pontosan az, amit szeretnénk.

Autentikáció és token kezelés

A legtöbb API-hoz valamilyen hitelesítés szükséges. A leggyakoribb megoldás a Bearer token alapú autentikáció. A tokeneket érdemes a SecureStorage-ban tárolni, ami platform-specifikus titkosítást használ (Keychain iOS-en, EncryptedSharedPreferences Androidon).

public class AuthHeaderHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        var token = await SecureStorage.GetAsync("access_token");

        if (!string.IsNullOrEmpty(token))
        {
            request.Headers.Authorization =
                new AuthenticationHeaderValue("Bearer", token);
        }

        var response = await base.SendAsync(request, cancellationToken);

        // Ha a token lejárt, próbáljuk meg megújítani
        if (response.StatusCode == HttpStatusCode.Unauthorized)
        {
            var ujToken = await TokenMegujitas();
            if (ujToken is not null)
            {
                await SecureStorage.SetAsync("access_token", ujToken);
                request.Headers.Authorization =
                    new AuthenticationHeaderValue("Bearer", ujToken);
                response = await base.SendAsync(request, cancellationToken);
            }
        }

        return response;
    }

    private async Task<string?> TokenMegujitas()
    {
        var refreshToken = await SecureStorage.GetAsync("refresh_token");
        if (string.IsNullOrEmpty(refreshToken))
            return null;

        // Refresh token hívás a szerver felé
        // (egyszerűsített példa)
        using var client = new HttpClient();
        var content = new FormUrlEncodedContent(new[]
        {
            new KeyValuePair<string, string>("grant_type", "refresh_token"),
            new KeyValuePair<string, string>("refresh_token", refreshToken)
        });

        var response = await client.PostAsync(
            "https://api.example.com/auth/refresh", content);

        if (!response.IsSuccessStatusCode) return null;

        var result = await response.Content
            .ReadFromJsonAsync<TokenValasz>();
        if (result?.RefreshToken is not null)
            await SecureStorage.SetAsync("refresh_token", result.RefreshToken);
        return result?.AccessToken;
    }
}

public class TokenValasz
{
    [JsonPropertyName("access_token")]
    public string AccessToken { get; set; } = string.Empty;

    [JsonPropertyName("refresh_token")]
    public string RefreshToken { get; set; } = string.Empty;
}

// Regisztráció
builder.Services.AddTransient<AuthHeaderHandler>();
builder.Services.AddHttpClient("ApiClient", client =>
{
    client.BaseAddress = new Uri("https://api.example.com/");
})
.AddHttpMessageHandler<AuthHeaderHandler>();

Hálózati kapcsolat ellenőrzése

Mobilalkalmazásoknál elengedhetetlen, hogy ellenőrizzük a hálózati állapotot, mielőtt API hívást indítunk. Nincs annál rosszabb felhasználói élmény, mint amikor az app percekig pörög, majd hibát dob, mert nincs net. A .NET MAUI beépített Connectivity API-ja erre ad egyszerű megoldást:

public class KonnektivitasService
{
    public bool VanInternetKapcsolat()
    {
        var profiles = Connectivity.Current.ConnectionProfiles;
        return Connectivity.Current.NetworkAccess == NetworkAccess.Internet;
    }

    public bool WifiKapcsolat()
    {
        return Connectivity.Current.ConnectionProfiles
            .Contains(ConnectionProfile.WiFi);
    }

    public void FigyeldAValtozast(Action<bool> callback)
    {
        Connectivity.Current.ConnectivityChanged += (s, e) =>
        {
            callback(e.NetworkAccess == NetworkAccess.Internet);
        };
    }
}

// Használat a ViewModel-ben
[RelayCommand]
private async Task AdatokBetoltese()
{
    if (!_konnektivitas.VanInternetKapcsolat())
    {
        await Shell.Current.DisplayAlert(
            "Nincs internet",
            "Kérlek, ellenőrizd a hálózati kapcsolatodat.",
            "OK");
        return;
    }

    // API hívás...
}

Helyi fejlesztés: csatlakozás localhost-hoz

Fejlesztés során gyakran szükséges, hogy a mobilalkalmazás a helyi gépen futó API-hoz csatlakozzon. Ez különösen Android emulátoron tud trükkös lenni, mert a localhost az emulátorra mutat, nem a fejlesztői gépre. Ezen nem egy kolléga törte már a fejét feleslegesen.

Android emulátor

Az Android emulátor a 10.0.2.2 címen éri el a hoszt gépet:

#if DEBUG
#if ANDROID
    var baseUrl = "https://10.0.2.2:5001";
#elif IOS
    var baseUrl = "https://localhost:5001";
#else
    var baseUrl = "https://localhost:5001";
#endif
#else
    var baseUrl = "https://api.production.com";
#endif

Clear-text forgalom engedélyezése (csak debug)

Ha a helyi API HTTP-t használ (nem HTTPS-t), Androidon külön engedélyezned kell a clear-text forgalmat. Éles környezetben ezt soha ne hagyd bekapcsolva:

// Platforms/Android/MainApplication.cs
#if DEBUG
[Application(UsesCleartextTraffic = true)]
#else
[Application]
#endif
public class MainApplication : MauiApplication
{
    public MainApplication(IntPtr handle, JniHandleOwnership ownership)
        : base(handle, ownership) { }

    protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
}

A teljes architektúra összefoglalása

Nézzük meg, hogyan áll össze egy jól strukturált .NET MAUI alkalmazás API rétege:

┌─────────────────────────────────┐
│          ViewModel              │
│   (CommunityToolkit MVVM)       │
├─────────────────────────────────┤
│       IApiService               │
│   (interfész / Refit)           │
├─────────────────────────────────┤
│    DelegatingHandler-ek         │
│  ┌─────────┐  ┌──────────────┐  │
│  │  Auth    │  │  Hibakezelés │  │
│  │ Handler  │  │   Handler    │  │
│  └─────────┘  └──────────────┘  │
├─────────────────────────────────┤
│     IHttpClientFactory          │
│   (HttpClient életciklus)       │
├─────────────────────────────────┤
│   Platform-natív HTTP handler   │
│  Android │   iOS   │  Windows   │
└─────────────────────────────────┘

Ez az architektúra biztosítja a kód tesztelhetőségét (minden réteg mockolható), a platform-specifikus optimalizálást, és a központosított hibakezelést. A DelegatingHandler-ek láncolhatók, így az autentikáció, a naplózás és az újrapróbálkozás egymástól függetlenül konfigurálható.

Ha ebből az útmutatóból egy dolgot viszel magaddal, az legyen ez: soha ne hozz létre közvetlenül HttpClient-et, mindig használd az IHttpClientFactory-t, és szervezd a hálózati réteget dedikált szolgáltatásokba. A jövőbeli éned hálás lesz érte.

Gyakran Ismételt Kérdések

Használhatok-e HttpClientFactory-t .NET MAUI-ban?

Igen, az IHttpClientFactory teljes mértékben támogatott .NET MAUI-ban. A Microsoft.Extensions.Http NuGet csomag segítségével a MauiProgram.cs fájlban regisztrálhatod, pontosan úgy, mint ASP.NET Core-ban. A factory kezeli a HttpClient példányok életciklusát, megelőzve a socket exhaustion és a DNS cache problémákat.

Mi a különbség a Refit és a kézzel írt HttpClient hívások között?

A Refit egy interfész-alapú, deklaratív megközelítést kínál: definiálsz egy C# interfészt az API végpontokkal, a Refit pedig automatikusan generálja az implementációt (beleértve a JSON szerializálást). A kézzel írt megoldás több kontrollt ad, de több boilerplate kódot is igényel. Egyszerű CRUD API-khoz a Refit ideális; komplex, egyedi logikát igénylő esetekhez a kézi implementáció jobb választás lehet.

Hogyan tesztelhetem az API hívásokat unit tesztekben?

Az interfész-alapú megközelítés (legyen az IApiService vagy Refit ITermekApi) lehetővé teszi, hogy a tesztekben mock implementációt injektálj. Használhatsz Moq vagy NSubstitute könyvtárakat az interfész mockolásához. Az HttpClient tesztelésére pedig a MockHttpMessageHandler (MockHttp NuGet csomag) kiváló megoldás, amivel előre definiált válaszokat adhatsz a kérésekre.

Hogyan kezelhető az offline állapot API hívásoknál?

A legjobb megoldás az offline-first megközelítés: az adatokat SQLite-ban tárold helyileg, és háttérben szinkronizáld a szerverrel, amikor van internetkapcsolat. A Connectivity API segítségével figyelheted a hálózati állapot változásait, és automatikusan elindíthatod a szinkronizálást, amikor a kapcsolat helyreáll.

A platform-natív HTTP handler vagy a managed handler a jobb választás?

A platform-natív handler (AndroidMessageHandler, NSUrlSessionHandler) szinte mindig a jobb választás mobilon. Gyorsabb, kisebb méretű alkalmazást eredményez, és automatikusan támogatja a platform TLS konfigurációját. A managed handlert (SocketsHttpHandler) csak akkor érdemes használni, ha olyan funkciókra van szükséged, amit a natív handler nem támogat.

A Szerzőről Editorial Team

Our team of expert writers and editors.