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
HttpClientpé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:
- Android —
AndroidMessageHandler, ami a natív Java/Android hálózati stackre épül - iOS —
NSUrlSessionHandler, ami azNSURLSessionAPI-t használja - Windows —
WinHttpHandler, 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.