Opdateret: 15. juli 2026
Type-safe REST API-integration i .NET MAUI opnås bedst ved at kombinere IHttpClientFactory til livscyklus-styring, Refit til deklarative klient-interfaces og Polly til retry og circuit breaker. Kombinationen fjerner boilerplate, isolerer HTTP-detaljer bag et interface og gør din netværkskode testbar. I denne guide viser jeg dig hele opsætningen fra MauiProgram.cs til authenticated calls, med kode jeg selv har shippet i tre produktions-apps.
Hvorfor du aldrig bør bruge new HttpClient() i MAUI
Det klassiske fejltrin i mobile .NET-apps er at instantiere new HttpClient() pr. kald. Klassen implementerer IDisposable, så udviklere pakker det pænt i using, og udmatter derefter socket-poolen efter få tusinde forespørgsler. På Android manifesterer det sig som SocketException under load. På iOS ses det som mystiske timeouts. Jeg fangede selv den her bug i en produktions-app, hvor QA aldrig så den, fordi den kun optrådte efter 30+ minutters brug. Ikke sjovt at debugge fra release notes.
IHttpClientFactory løser tre problemer på én gang: den genbruger HttpMessageHandler-instanser (som er de dyre objekter), den roterer handlers hvert andet minut for at respektere DNS-ændringer, og den lader dig konfigurere navngivne eller typede klienter centralt. I .NET MAUI er factory tilgængelig via Microsoft.Extensions.Http-pakken og integrerer direkte med den indbyggede DI-container. Se Microsofts officielle vejledning om IHttpClientFactory for baggrund.
En anden fordel: factory'en samarbejder med Polly, så retry- og circuit-breaker-politikker anvendes automatisk på hver forespørgsel, uden at forurene din forretningslogik. Vi kobler det sammen i næste sektion.
Opsætning af IHttpClientFactory i MauiProgram
Så, lad os starte med at tilføje pakkerne. I .NET 9 og nyere er Microsoft.Extensions.Http allerede med som transitiv afhængighed, men du skal eksplicit tilføje resilience- og Refit-pakkerne:
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="9.0.0" />
<PackageReference Include="Refit.HttpClientFactory" Version="8.0.0" />Konfigurér derefter en navngiven klient i MauiProgram.cs. Personligt foretrækker jeg navngivne klienter over typede, fordi de er nemmere at ombryde med Refit senere:
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
});
// Base HttpClient konfiguration
builder.Services.AddHttpClient("MobileApi", client =>
{
client.BaseAddress = new Uri("https://api.example.com/v1/");
client.Timeout = TimeSpan.FromSeconds(30);
client.DefaultRequestHeaders.Add("User-Agent", "MobileTechLead/1.0");
});
return builder.Build();
}Bemærk at BaseAddress ender med en skråstreg. Uden den vil relative URI'er i Refit blive tolket forkert (og du får en time i debuggeren gratis). Timeout på 30 sekunder er min standard for mobile netværk; hvis dit API kører 3G-scenarier, kan du overveje 60 sekunder, men vær opmærksom på, at brugere opgiver længe før det.
Refit: type-safe klienter uden boilerplate
Refit lader dig deklarere API-endpoints som et interface med attributter, og genererer implementeringen ved kompilering. Det fjerner manuel URL-formatering, JSON-serialisering og fejlkontrol fra din view model-kode. Definér først din model og dit interface:
public record Product(int Id, string Name, decimal Price, string ImageUrl);
public record ProductPage(IReadOnlyList<Product> Items, int Total, int Page);
public interface IProductApi
{
[Get("/products")]
Task<ProductPage> GetProductsAsync(
[Query] int page = 1,
[Query] int pageSize = 20,
CancellationToken ct = default);
[Get("/products/{id}")]
Task<Product> GetProductAsync(int id, CancellationToken ct = default);
[Post("/products")]
Task<Product> CreateProductAsync(
[Body] Product product,
CancellationToken ct = default);
[Multipart]
[Post("/products/{id}/image")]
Task UploadImageAsync(
int id,
[AliasAs("file")] StreamPart image,
CancellationToken ct = default);
}Registrér Refit-klienten sammen med din navngivne HttpClient:
builder.Services
.AddRefitClient<IProductApi>()
.ConfigureHttpClient(c =>
{
c.BaseAddress = new Uri("https://api.example.com/v1/");
c.Timeout = TimeSpan.FromSeconds(30);
});Din view model modtager nu IProductApi via constructor injection, helt fri for HTTP-detaljer, hvilket gør den trivielt testbar. Se Refit-projektets officielle dokumentation for fulde attribut-oversigter, herunder [Header], [QueryUriFormat] og typede fejl-modeller via ApiException. Hvis du har brug for at genopfriske dependency injection-mønstre først, dækker vores guide til dependency injection i .NET MAUI alle service-livscyklusser i detaljer.
Resilience med Polly og retry-strategier
Mobile netværk fejler konstant: en tunnel, en overfyldt celletårn, en Wi-Fi-overgang. En stabil API-klient antager fejl som normen og reagerer med eksponentiel back-off. Microsoft.Extensions.Http.Resilience pakker Polly v8 med sensible standarder, som du kan tilføje til enhver HttpClient med én linje:
using Microsoft.Extensions.Http.Resilience;
builder.Services
.AddRefitClient<IProductApi>()
.ConfigureHttpClient(c => c.BaseAddress = new Uri("https://api.example.com/v1/"))
.AddStandardResilienceHandler(options =>
{
options.Retry.MaxRetryAttempts = 3;
options.Retry.BackoffType = DelayBackoffType.Exponential;
options.Retry.UseJitter = true;
options.AttemptTimeout.Timeout = TimeSpan.FromSeconds(10);
options.TotalRequestTimeout.Timeout = TimeSpan.FromSeconds(45);
options.CircuitBreaker.FailureRatio = 0.5;
});Standardhandler'en kombinerer fem strategier: timeout pr. forsøg, samlet timeout, retry med jitter, circuit breaker og rate limiter. Jitter er kritisk i mobile scenarier. Uden den vil hundredvis af klienter forsøge igen samtidig efter en netværksafbrydelse og forværre problemet.
Til produktion anbefaler jeg at logge circuit-breaker-transitioner via ILogger. Det er dit tidligste signal om et nedstrøms API-outage. Polly's telemetri integrerer med Microsoft.Extensions.Diagnostics, så du får dette gratis, hvis du allerede har opsat structured logging.
DelegatingHandler til authentication og logging
Bearer-tokens hører ikke hjemme i hvert enkelt Refit-kald. Brug en DelegatingHandler, som opsnapper alle forespørgsler for din klient og tilføjer headers centralt. Dette er også det rette sted til at håndtere token-fornyelse på 401-responses:
public sealed class AuthHeaderHandler : DelegatingHandler
{
private readonly ITokenProvider _tokens;
public AuthHeaderHandler(ITokenProvider tokens)
{
_tokens = tokens;
}
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken ct)
{
var token = await _tokens.GetAccessTokenAsync(ct);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
var response = await base.SendAsync(request, ct);
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
token = await _tokens.RefreshAsync(ct);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
response.Dispose();
response = await base.SendAsync(request, ct);
}
return response;
}
}Registrér handler'en og kæd den til din klient:
builder.Services.AddSingleton<ITokenProvider, SecureTokenProvider>();
builder.Services.AddTransient<AuthHeaderHandler>();
builder.Services
.AddRefitClient<IProductApi>()
.ConfigureHttpClient(c => c.BaseAddress = new Uri("https://api.example.com/v1/"))
.AddHttpMessageHandler<AuthHeaderHandler>()
.AddStandardResilienceHandler();Tokens bør gemmes i SecureStorage, aldrig i Preferences eller ren tekst. Vores guide til sikkerhed i .NET MAUI dækker dette i detaljer, inklusive biometrisk beskyttelse af refresh tokens.
iOS- og Android-platformkonfiguration
Både iOS og Android har platform-specifikke krav til HTTP-trafik i 2026. På iOS blokerer App Transport Security (ATS) al HTTP-trafik uden TLS 1.2+. Til produktion er det præcis hvad du vil have, men til lokal udvikling mod http://localhost:5000 skal du tilføje en undtagelse i Platforms/iOS/Info.plist:
<key>NSAppTransportSecurity</key>
<dict>
<key>NSExceptionDomains</key>
<dict>
<key>localhost</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
</dict>
</dict>
</dict>På Android skal du eksplicit tillade cleartext-trafik pr. domæne via en network security config. Opret Platforms/Android/Resources/xml/network_security_config.xml:
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">10.0.2.2</domain>
<domain includeSubdomains="true">localhost</domain>
</domain-config>
</network-security-config>Reference filen fra AndroidManifest.xml via <application android:networkSecurityConfig="@xml/network_security_config">. Android-emulatoren bruger 10.0.2.2 som alias for host-maskinens localhost, en detalje der har kostet mig flere timer over årene. Husk det, seriøst.
På Android bør du også opgradere til SocketsHttpHandler (default fra .NET 8) i stedet for den ældre Xamarin-handler. Det giver HTTP/2, connection pooling og bedre timeout-håndtering. .NET MAUI bruger den korrekte handler by default siden 8.0, men hvis du migrerer fra Xamarin.Forms, bør du eksplicit sætte <UseNativeHttpHandler>false</UseNativeHttpHandler> i din .csproj.
Håndtering af offline-tilstand og connectivity
En mobile-first API-klient skal reagere på ændringer i netværkstilstand. IConnectivity fra Microsoft.Maui.Essentials giver både en synkron tilstand og en event:
public sealed class ProductService
{
private readonly IProductApi _api;
private readonly IConnectivity _connectivity;
private readonly ILocalProductStore _store;
public ProductService(
IProductApi api,
IConnectivity connectivity,
ILocalProductStore store)
{
_api = api;
_connectivity = connectivity;
_store = store;
}
public async Task<IReadOnlyList<Product>> GetProductsAsync(CancellationToken ct)
{
if (_connectivity.NetworkAccess != NetworkAccess.Internet)
{
return await _store.GetAllAsync(ct);
}
try
{
var page = await _api.GetProductsAsync(ct: ct);
await _store.UpsertRangeAsync(page.Items, ct);
return page.Items;
}
catch (ApiException) when (await _store.HasCacheAsync(ct))
{
return await _store.GetAllAsync(ct);
}
}
}Dette mønster (stale-while-revalidate) er min standard. Vis cached data hurtigt, opdatér i baggrunden, og fald tilbage til cache ved API-fejl. Til skrive-operationer bruger jeg en outbox-tabel i SQLite, hvor pending mutations opbevares indtil netværket vender tilbage. Vores guide til SQLite og EF Core i .NET MAUI viser hvordan du opsætter den lokale store.
Test af dine API-klienter
Fordi din view model afhænger af IProductApi (interface) og ikke HttpClient (concrete), er unit tests trivielle:
[Fact]
public async Task LoadProducts_populates_collection()
{
var api = Substitute.For<IProductApi>();
api.GetProductsAsync(Arg.Any<int>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
.Returns(new ProductPage(new[] { new Product(1, "Test", 99m, "img.jpg") }, 1, 1));
var vm = new ProductListViewModel(api);
await vm.LoadCommand.ExecuteAsync(null);
Assert.Single(vm.Products);
}Til integration tests mod en fake HTTP-server bruger jeg WireMock.Net, som lader dig scripte responses inklusive fejl-scenarier. Det er langt mere realistisk end at mocke Refit-interfacet, fordi du også får dine DelegatingHandlers og Polly-strategier med i testen. Vores komplette teststrategi for .NET MAUI dækker både unit-, integration- og UI-tests. Se også Microsofts vejledning om test af IHttpClientFactory.
Ofte stillede spørgsmål
Hvad er forskellen mellem HttpClient og Refit i .NET MAUI?
HttpClient er den lav-niveau .NET-klasse til at sende HTTP-forespørgsler manuelt. Refit er et bibliotek der genererer en type-safe klient-implementering fra et interface, så du deklarerer endpoints med attributter i stedet for at bygge HttpRequestMessage-objekter selv. Refit bruger stadig HttpClient internt, så det er egentlig et abstraktionslag ovenpå.
Kan man bruge IHttpClientFactory i .NET MAUI?
Ja, IHttpClientFactory er fuldt understøttet i .NET MAUI via Microsoft.Extensions.Http-pakken. Registrér dine klienter i MauiProgram.cs med builder.Services.AddHttpClient(), og inject enten IHttpClientFactory eller en typed client i dine services.
Hvordan tilføjer man authentication headers til alle API-kald?
Opret en klasse der arver fra DelegatingHandler, overskriv SendAsync og tilføj request.Headers.Authorization. Registrér den derefter på din HttpClient med .AddHttpMessageHandler<YourHandler>(). Dette centraliserer auth-logik og lader dig håndtere 401-responses med token-fornyelse ét sted.
Hvordan implementerer man retry-logik med Polly i .NET MAUI?
Installér Microsoft.Extensions.Http.Resilience, og tilføj .AddStandardResilienceHandler() til din klient-registrering. Standardhandler'en giver retry med eksponentiel back-off og jitter, timeouts og circuit breaker, og alt kan tilpasses via en options-callback uden at ændre din API-kode.
Skal man bruge System.Text.Json eller Newtonsoft.Json med Refit?
Brug System.Text.Json. Det er default i Refit 8, betydeligt hurtigere, og fylder mindre i din app. Newtonsoft.Json er kun nødvendigt hvis du bruger avancerede features som custom JsonConverter-hierarkier eller [JsonExtensionData], som System.Text.Json først for nyligt har fået fuld paritet på.




