Dependency Injection i .NET MAUI: Komplet Guide til Services, Lifetimes og Testbarhed
Komplet guide til Dependency Injection i .NET MAUI: lær at registrere services, vælge lifetimes (Singleton, Transient, Scoped), bruge HttpClient korrekt og teste ViewModels med mocks.
Dependency Injection i .NET MAUI fungerer via den indbyggede Microsoft.Extensions.DependencyInjection-container, som eksponeres gennem MauiAppBuilder.Services i MauiProgram.cs. Du registrerer dine services, ViewModels og pages som Singleton, Transient eller Scoped, hvorefter MAUI's handler-system automatisk konstruerer dem via constructor-injection. Det er den samme container, som ASP.NET Core bruger, og når du først har styr på lifetimes og registreringsrækkefølgen, forsvinder 80 % af de "uventede crashes på iOS", som jeg har set i kodebaser gennem årene.
.NET MAUI bruger Microsoft.Extensions.DependencyInjection; du registrerer alt i MauiProgram.CreateMauiApp() via builder.Services.
Brug Singleton til state og tjenester, der lever hele app-livscyklussen, Transient til ViewModels og pages, og undgå Scoped i mobil-konteksten (den giver sjældent mening uden for HTTP-requests).
Constructor-injection er standarden; service locator-pattern via IServiceProvider bør kun bruges i edge-cases som Shell-route-handlers.
Registrér HttpClient via AddHttpClient<T>() for at få automatisk lifecycle-håndtering og undgå socket-exhaustion på Android.
Keyed services (.NET 8+) løser flere implementeringer af samme interface uden custom factories, og er perfekt til "dev vs. prod" backend-switching.
Testbarhed kommer gratis: erstat én AddSingleton<IApiClient, RealApiClient>() med en mock i dine unit tests, og resten af koden behøver ikke ændringer.
Hvad er Dependency Injection i .NET MAUI?
Dependency Injection (DI) er et mønster, hvor en klasse modtager sine afhængigheder udefra i stedet for at oprette dem selv. I .NET MAUI betyder det konkret, at du i stedet for at skrive var api = new ApiClient(); inde i din ViewModel modtager IApiClient api som constructor-parameter, og DI-containeren leverer den korrekte instans ved opstart.
MAUI baserer sig på den samme Microsoft.Extensions.DependencyInjection-container, som driver ASP.NET Core. Containeren bygges én gang under app-startup i MauiProgram.CreateMauiApp() og fryses derefter. Du kan altså ikke registrere nye typer ved runtime. Det er en bevidst designbeslutning fra .NET-teamet, dokumenteret i den officielle .NET MAUI Dependency Injection-dokumentation, og det er det første sted, hvor mobil adskiller sig fra web.
Hvorfor bruge DI overhovedet? Tre grunde, jeg holder ved: testbarhed (du kan udskifte den rigtige HttpClient med en fake i unit tests), løs kobling (din ViewModel kender kun et interface, ikke en konkret klasse) og lifetime-håndtering (containeren styrer disposal, så du ikke får memory leaks fra glemte Dispose()-kald). Ærligt talt har den sidste grund alene betalt sig hjem i de tre store produktions-apps, jeg har arbejdet på.
For et solidt grundlag i de arkitektoniske mønstre, der gør DI værdifuldt, anbefaler jeg at læse vores guide om MVVM-arkitektur i .NET MAUI. DI og MVVM er to sider af samme mønt.
Sådan registrerer du services i MauiProgram
Al registrering sker i MauiProgram.cs, den fil som Visual Studio genererer for dig. Her er et komplet, produktionsnært eksempel, der dækker logging, HTTP, lokal database, ViewModels og pages:
using Microsoft.Extensions.Logging;
using CommunityToolkit.Maui;
namespace MyApp;
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.UseMauiCommunityToolkit()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
});
// 1) Infrastructure: lever hele app-livscyklussen
builder.Services.AddSingleton<IPreferencesService, PreferencesService>();
builder.Services.AddSingleton<IConnectivityService, ConnectivityService>();
// 2) Data access: singleton fordi SQLite-forbindelsen er thread-safe
builder.Services.AddSingleton<IDatabaseService, DatabaseService>();
// 3) HTTP: brug AddHttpClient for korrekt lifecycle
builder.Services.AddHttpClient<IApiClient, ApiClient>(client =>
{
client.BaseAddress = new Uri("https://api.example.com/");
client.Timeout = TimeSpan.FromSeconds(30);
});
// 4) ViewModels: transient, så hver page får friske instanser
builder.Services.AddTransient<ProductListViewModel>();
builder.Services.AddTransient<ProductDetailViewModel>();
builder.Services.AddTransient<LoginViewModel>();
// 5) Pages: transient og altid registreret hvis du injicerer dem
builder.Services.AddTransient<ProductListPage>();
builder.Services.AddTransient<ProductDetailPage>();
builder.Services.AddTransient<LoginPage>();
#if DEBUG
builder.Logging.AddDebug();
#endif
return builder.Build();
}
}
Rækkefølgen er ligegyldig for containeren, fordi Microsoft.Extensions.DependencyInjection bygger en graf, ikke en sekvens. Men for læsbarhedens skyld grupperer jeg altid efter lagene: infrastructure, data, HTTP, ViewModels, pages. Når en ny udvikler joiner teamet, kan de finde præcis, hvor noget hører hjemme, uden at scrolle gennem 300 linjer.
Singleton, Transient eller Scoped, hvilken lifetime?
Lifetime-valget er det punkt, hvor flest teams skyder sig i foden. Her er, hvordan jeg tænker om det, baseret på den officielle Microsoft.Extensions.DependencyInjection-dokumentation og hvad der faktisk virker i mobile apps:
Må ikke holde referencer til pages/ViewModels, da det blokerer GC.
Transient
Tilstandsløse eller kortlivede
ViewModels, Pages, IMapper, validators
Disposable transients lever indtil containeren dør (kun et problem for singletons).
Scoped
Sjældent på mobil
Per-navigation state (avanceret)
MAUI har ikke et indbygget scope per page; du skal selv lave CreateScope().
I praksis ender 95 % af mine registreringer som enten Singleton eller Transient. Scoped giver mening i ASP.NET Core, hvor hver HTTP-request får sit eget scope. I en mobil-app er der intet naturligt scope, medmindre du bygger et selv (fx pr. logged-in bruger).
Captive dependency-problemet
Den klassiske fælde er en Singleton, der modtager en Transient som constructor-parameter. Containeren kan kun injicere den transient én gang, nemlig ved konstruktionen af singletonen, så transient'en bliver de facto en singleton, der lever for evigt. Dette kaldes en captive dependency, og DI-containeren advarer dig kun, hvis du opretter den med ValidateOnBuild = true:
Constructor-injection er standardvejen, og den fungerer ud af boksen, så længe både service og ViewModel er registreret. Her er et eksempel med CommunityToolkit.Mvvm:
For at få ViewModelen koblet til Page'n injicerer du ViewModelen i Page'ns constructor og sætter BindingContext:
public partial class ProductListPage : ContentPage
{
public ProductListPage(ProductListViewModel vm)
{
InitializeComponent();
BindingContext = vm;
}
}
Det er præcis den samme tilgang, jeg har anvendt i tre forskellige produktions-apps, og den skalerer rent op til 50+ ViewModels uden ceremoni. Når du kombinerer dette med en solid teststrategi for .NET MAUI, kan du teste hver ViewModel isoleret med fake services.
HttpClient og Refit med DI
Det største enkeltstående performance-problem, jeg har set i MAUI-apps, er fejlhåndtering af HttpClient. Mange udviklere registrerer den som singleton ("for at undgå socket exhaustion") eller opretter en ny ved hvert kald ("for at undgå DNS-problemer"). Begge dele er forkerte.
Den korrekte måde er AddHttpClient<T>(), som internt bruger IHttpClientFactory til at genbruge HttpMessageHandler-instanser. Du kan læse mere i Microsofts officielle IHttpClientFactory-guide:
Refit genererer implementeringen via source generators, så der er nul reflection-overhead ved runtime. Det er især vigtigt på iOS, hvor AOT-compilering ellers vil klage.
Keyed services i .NET 8+
En af de mest oversete forbedringer i nyere .NET-versioner er keyed services. Tidligere måtte du skrive en custom factory hver gang, du havde flere implementeringer af samme interface (fx en "live" og en "mock" backend, eller forskellige betalingsudbydere). Nu er det indbygget. Detaljerne findes i Microsofts release notes for keyed DI services.
// Registrér
builder.Services.AddKeyedSingleton<IPaymentProvider, StripeProvider>("stripe");
builder.Services.AddKeyedSingleton<IPaymentProvider, MobilePayProvider>("mobilepay");
// Injicér med [FromKeyedServices]
public class CheckoutViewModel
{
private readonly IPaymentProvider _stripe;
private readonly IPaymentProvider _mobilepay;
public CheckoutViewModel(
[FromKeyedServices("stripe")] IPaymentProvider stripe,
[FromKeyedServices("mobilepay")] IPaymentProvider mobilepay)
{
_stripe = stripe;
_mobilepay = mobilepay;
}
}
DI med Shell-navigation og query-parametre
Shell-routing er det sted, hvor DI typisk møder sin første store udfordring. Når du registrerer en route med Routing.RegisterRoute("product", typeof(ProductDetailPage)), instansieres siden via DI-containeren, så længe den er registreret som transient. Det fungerer perfekt for constructor-parametre, der er services.
Men hvad med ruteparametre som ?productId=42? Dem kan containeren ikke kende. Den rene løsning er at lade din ViewModel implementere IQueryAttributable:
public partial class ProductDetailViewModel : ObservableObject, IQueryAttributable
{
private readonly IApiClient _api;
public ProductDetailViewModel(IApiClient api) => _api = api;
[ObservableProperty]
private Product? product;
public async void ApplyQueryAttributes(IDictionary<string, object> query)
{
if (query.TryGetValue("productId", out var idObj)
&& int.TryParse(idObj?.ToString(), out var id))
{
Product = await _api.GetProductAsync(id);
}
}
}
// Naviger med:
await Shell.Current.GoToAsync($"product?productId={id}");
Dermed beholder du clean constructor-injection af services, og ruteparametre kommer ind ad en separat kanal, uden at du nogensinde skal røre IServiceProvider manuelt.
Testbarhed: mock services i unit tests
Hele pointen med DI er at gøre din kode testbar. Med en god ViewModel-arkitektur kan du teste den uden at starte MAUI overhovedet:
using NSubstitute;
using Xunit;
public class ProductListViewModelTests
{
[Fact]
public async Task LoadAsync_PopulatesProducts()
{
// Arrange
var api = Substitute.For<IApiClient>();
api.GetProductsAsync().Returns(new List<Product>
{
new(1, "Sko"),
new(2, "Bog")
});
var logger = Substitute.For<ILogger<ProductListViewModel>>();
var vm = new ProductListViewModel(api, logger);
// Act
await vm.LoadCommand.ExecuteAsync(null);
// Assert
Assert.Equal(2, vm.Products.Count);
Assert.False(vm.IsBusy);
}
}
Læg mærke til: ingen MauiAppBuilder, ingen container, ingen platform-afhængigheder. Det er sådan, vores unit test-pyramide for MAUI er bygget op. Over 90 % af testene kører uden en enhed eller emulator.
Produktionsfælder jeg er løbet ind i
Tre konkrete fælder fra de sidste tre års produktion. Skriv dem ned:
1. App og AppShell som singletons skjuler memory leaks
Hvis du registrerer pages som singletons "for performance", holder du på Visual Trees, der aldrig bliver garbage-collected. Resultat: efter 50 navigationer er din app oppe på 400 MB. Pages skal altid være Transient. Performance-gevinsten ved singleton-pages er en myte. Jeg har målt det. Hvis du vil grave dybere i den slags problemer, så se vores guide om ydeevneoptimering i .NET MAUI.
2. Service locator på IServiceProvider via Handler.MauiContext
Det er fristende at skrive Application.Current.Handler.MauiContext.Services.GetService<IFoo>(), når du er stuck. Lad være. Det er service locator-pattern, det skjuler afhængigheder, og det er umuligt at unit-teste. Brug det kun i App.xaml.cs ved opstart, hvor constructor-injection ikke er tilgængelig.
3. Async i constructors
DI-containeren kan ikke afvente Task. Hvis din service kræver async initialisering (fx læse fra disk), så implementér et IAsyncInitializable-mønster og kald InitializeAsync() første gang servicen bruges, gerne med en SemaphoreSlim for thread-safety. Jeg har set apps crashe på iOS startup udelukkende på grund af fire glemte .Result-kald i singleton-constructors.
Ofte stillede spørgsmål
Har .NET MAUI indbygget Dependency Injection?
Ja. .NET MAUI inkluderer Microsoft.Extensions.DependencyInjection som standard via MauiAppBuilder.Services. Du behøver ikke installere en tredjepartscontainer som Autofac eller DryIoc, da den indbyggede dækker 99 % af alle behov i en mobil-app.
Hvad er forskellen mellem Singleton, Transient og Scoped i .NET MAUI?
Singleton lever hele app-livscyklussen (én instans). Transient laver en ny instans hver gang nogen beder om typen. Scoped lever inden for et eksplicit scope, hvilket sjældent giver mening på mobil, da MAUI ikke har et indbygget scope per page. I praksis bruger man Singleton til services og Transient til ViewModels/Pages.
Hvordan registrerer jeg HttpClient korrekt i .NET MAUI?
Brug builder.Services.AddHttpClient<T>(), ikke AddSingleton eller AddTransient. AddHttpClient aktiverer IHttpClientFactory, der genbruger underliggende HttpMessageHandler-instanser og forhindrer både socket exhaustion og stale DNS-poster. Tilføj .AddStandardResilienceHandler() for indbygget retry og circuit breaker.
Kan jeg bruge keyed services i .NET MAUI?
Ja, hvis du kører .NET 8 eller nyere. Brug AddKeyedSingleton / AddKeyedTransient ved registrering og [FromKeyedServices("nøgle")] i constructors. Det er den reneste måde at håndtere flere implementeringer af samme interface på, fx forskellige betalingsudbydere eller dev/prod backends.
Hvordan tester jeg en ViewModel uden at starte MAUI?
Hvis din ViewModel kun afhænger af interfaces (ikke statiske Application.Current-kald eller native APIs), kan du instansiere den direkte i en xUnit-test med mockede dependencies, typisk via NSubstitute eller Moq. Ingen container, ingen MauiAppBuilder. Det er hele værdien ved DI på mobil.
Byg tilgængelige .NET MAUI-apps der opfylder WCAG 2.1 AA og EAA-kravene. Med praktiske kodeeksempler for SemanticProperties, skærmlæserunderstøttelse, farvekontrast og touch-targets.
Lær at sikre din .NET MAUI-app med SecureStorage, biometrisk login (fingeraftryk og Face ID), OAuth 2.0 via MSAL og certificate pinning. Komplet guide med praktiske kodeeksempler til .NET 10.
Lær at implementere push notifikationer i .NET MAUI med Firebase og APNs. Komplet guide med kodeeksempler til Android og iOS, notification channels, token-håndtering og fejlfinding.