Volledige opzet van dependency injection in .NET MAUI 9 en 10: MauiAppBuilder.Services, lifetimes, ViewModels, keyed services en valkuilen op Android en iOS.
Dependency injection in .NET MAUI gebeurt via de MauiAppBuilder.Services-collectie in MauiProgram.cs, die identiek werkt aan de IServiceCollection-API uit ASP.NET Core. Je registreert interfaces met AddSingleton, AddScoped of AddTransient, en MAUI lost ze automatisch op via constructor-injectie in pages en ViewModels. In dit artikel doorlopen we de volledige opzet voor .NET MAUI 9 en .NET 10, leggen we de drie service-lifetimes uit aan de hand van praktijkscenario's, en bespreken we de valkuilen die je app kunnen breken op iOS en Android.
MauiAppBuilder.Services is de standaard IServiceCollection; alle Microsoft.Extensions.DependencyInjection-extensies werken direct.
Singleton leeft net zo lang als de app, Transient maakt elke resolve een nieuwe instantie aan, en Scoped wordt in MAUI alleen zinvol bij HybridWebView of BlazorWebView.
ViewModels en pages registreer je meestal als Transient. Dat voorkomt vastzittende state tussen Shell-navigaties.
Constructor-injectie is de aanbevolen methode. Service locator-patronen via Handler.MauiContext.Services zijn alleen geschikt voor platformcode.
Op Android en iOS resulteert het registreren van een page als Singleton vaak in de fout "The visual element is already a child of another element". Gebruik dan Transient.
Vanaf .NET 9 ondersteunt MAUI KeyedServices, handig om meerdere implementaties van dezelfde interface te onderscheiden.
Wat is dependency injection in .NET MAUI?
Dependency injection (DI) is een ontwerppatroon waarbij een object zijn afhankelijkheden via de constructor (of soms via properties) krijgt aangereikt, in plaats van die afhankelijkheden zelf te instantiëren. In .NET MAUI is dit patroon ingebakken: het framework draait op dezelfde Microsoft.Extensions.DependencyInjection-container die ASP.NET Core gebruikt. Dat betekent dat alle bekende uitbreidingsmethoden (AddHttpClient, AddLogging, AddOptions) zonder extra werk beschikbaar zijn.
De winst is concreet: je ViewModels worden testbaar omdat je een nepversie van IDataService kunt injecteren, en je vermijdt de tight coupling die Xamarin.Forms-apps vaak liet ontsporen. Eerlijk gezegd is dit voor mij de grootste architecturele verbetering in de overgang van Xamarin naar MAUI. Waar je vroeger een DependencyService.Resolve<T>() moest aanroepen, gebruik je nu gewoon een nette constructor-parameter.
Het instappunt voor elke MAUI-app is MauiProgram.CreateMauiApp(). Hierin krijg je een MauiAppBuilder die naast UseMauiApp, fonts en handlers ook een Services-collectie blootlegt waar je al je registraties kwijt kunt. So, een typische opzet voor een productiewaardige app ziet er zo uit:
// MauiProgram.cs
using CommunityToolkit.Maui;
using Microsoft.Extensions.Logging;
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.UseMauiCommunityToolkit()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
});
// Infrastructuur
builder.Services.AddSingleton<IConnectivity>(_ => Connectivity.Current);
builder.Services.AddSingleton<IPreferences>(_ => Preferences.Default);
builder.Services.AddHttpClient<IWeatherApi, WeatherApi>(client =>
{
client.BaseAddress = new Uri("https://api.example.com/");
client.Timeout = TimeSpan.FromSeconds(15);
});
// Applicatie-services
builder.Services.AddSingleton<IUserSession, UserSession>();
builder.Services.AddTransient<IWeatherService, WeatherService>();
// ViewModels en pages
builder.Services.AddTransient<DashboardViewModel>();
builder.Services.AddTransient<DashboardPage>();
#if DEBUG
builder.Logging.AddDebug();
#endif
return builder.Build();
}
}
Twee dingen om op te merken. AddHttpClient registreert automatisch een typed client met een eigen HttpClient-instantie en een IHttpClientFactory-achterkant, wat het socket-uitputtingsprobleem van Xamarin-tijden oplost. En AddSingleton met een factory-lambda is de manier om bestaande statische instanties (zoals Connectivity.Current) in DI te brengen zonder ze opnieuw te wrappen.
Singleton, Scoped en Transient: welke lifetime kies je?
De drie service-lifetimes bepalen hoe vaak de container een nieuwe instantie aanmaakt. De keuze beïnvloedt geheugen, threading en gedrag tussen navigaties. Onderstaande tabel vat de praktijk samen voor .NET MAUI 9 en 10.
Dure constructors of objecten die exclusieve resources beheren
De vuistregel die ik in mijn laatste twee MAUI-projecten heb aangehouden: state singleton, gedrag transient. Een UserSession die de ingelogde gebruiker bijhoudt registreer je als singleton. Een UserDetailViewModel die je twee keer kunt openen voor verschillende gebruikers registreer je als transient. Voor DbContext uit Entity Framework Core gebruik je sinds .NET 8 bij voorkeur AddDbContextFactory. Die geeft je per call een verse context zonder threading-conflicten op mobiele platformen.
De page-constructor accepteert de ViewModel als parameter en zet die op BindingContext. Beide zijn als transient geregistreerd, dus elke keer dat je naar DashboardPage navigeert krijg je een verse ViewModel met een schone state. Voor pages die echt nooit twee keer tegelijk bestaan (bijvoorbeeld een tabblad in TabBar) kun je overwegen ze als singleton te registreren. Maar wees voorzichtig: state blijft dan staan tussen sessies, inclusief loading-spinners die nooit zijn gestopt.
DI gebruiken met Shell-navigatie en query parameters
Bij Shell.Current.GoToAsync("user?id=42") roept MAUI de geregistreerde page-constructor aan via de DI-container en wijst daarna eventuele [QueryProperty]-waarden toe. Dit betekent dat je services die ID's nodig hebben in OnAppearing of OnNavigatedTo moet aanspreken, niet in de constructor. De constructor draait namelijk vóórdat de query parameter is gezet (ik liep hier persoonlijk tegenaan tijdens een release-sprint, en de NullReferenceException die je dan krijgt is bijzonder verraderlijk).
[QueryProperty(nameof(UserId), "id")]
public partial class UserDetailViewModel : ObservableObject
{
private readonly IUserService _users;
public UserDetailViewModel(IUserService users) => _users = users;
[ObservableProperty]
private int userId;
partial void OnUserIdChanged(int value)
{
// veilig: query parameter is nu gezet
_ = LoadAsync(value);
}
private async Task LoadAsync(int id) { /* ... */ }
}
Voor diepere achtergrond over routes en deep links, zie onze gids over .NET MAUI Shell-navigatie met routes en deep linking. Belangrijk: registreer Shell-routes in AppShell.xaml.cs met Routing.RegisterRoute nadat je de pages in DI hebt opgenomen. Anders krijgt de container je page niet te zien tijdens navigatie.
Platform-specifieke services injecteren
Een veelvoorkomende use case is een service met verschillende implementaties per OS, bijvoorbeeld IBiometricAuth die op Android Keystore gebruikt en op iOS de Secure Enclave. De cleanste aanpak gebruikt conditionele compilatie in MauiProgram:
De implementaties leven in de Platforms/-mappen en compilen alleen mee in hun doel-platform. Dit is ook hoe je sinds .NET 8 het DependencyService-patroon uit Xamarin volledig vervangt. Voor lezers die nog midden in een migratie zitten leest onze diepe duik over .NET MAUI handlers en platform-specifieke integratie verder uit op handlers.
Keyed services in .NET 9 en .NET 10
Met .NET 8 introduceerde Microsoft KeyedServices in Microsoft.Extensions.DependencyInjection, en MAUI 9 en 10 ondersteunen het volledig. Hiermee registreer je meerdere implementaties van dezelfde interface onder een sleutel. Handig voor analytics-providers, feature-flagged backends of A/B-varianten:
De volgende fouten zien we wekelijks terugkomen in StackOverflow-vragen en GitHub-issues op de dotnet/maui repository:
Pages als Singleton registreren
Een page mag in MAUI maar één parent tegelijk hebben. Als je een page als singleton registreert en de gebruiker er twee keer naartoe navigeert (push, pop, push), krijg je InvalidOperationException: The visual element is already a child of another element. Houd pages transient, tenzij je ze maar één keer per app-sessie laat zien.
Async constructors
De DI-container kan geen async code in constructors uitvoeren. Verplaats initialisatie naar een aparte InitializeAsync() die je aanroept vanuit OnAppearing, of gebruik de IAsyncInitialization-pattern met een lazy Task in een singleton-service.
Captive dependencies
Een singleton die een transient injecteert houdt die transient effectief voor altijd vast: een captive dependency. Het resultaat: je transient-ViewModel wordt eigenlijk een singleton in disguise. Vermijd dit door in de singleton een IServiceProvider te injecteren en de transient on-demand op te lossen, of door het ontwerp te herzien.
Services testen met xUnit en NSubstitute
De grootste winst van DI? Dat ViewModels testbaar worden zonder MAUI-runtime. In een aparte xUnit-projectreferentie kun je ViewModels rechtstreeks instantiëren met fake implementaties:
public class DashboardViewModelTests
{
[Fact]
public async Task Refresh_zet_temperature_uit_service()
{
var weather = Substitute.For<IWeatherService>();
weather.GetCurrentAsync(Arg.Any<CancellationToken>()).Returns(21.5);
var session = Substitute.For<IUserSession>();
session.DisplayName.Returns("Anna");
var vm = new DashboardViewModel(weather, session);
await vm.RefreshCommand.ExecuteAsync(null);
Assert.Equal(21.5, vm.CurrentTemperature);
Assert.Equal("Hallo Anna", vm.Greeting);
}
}
Geen emulator, geen UI-thread, milliseconden per test. Voor scenario's waar je de volledige DI-container wilt opbouwen kun je ServiceCollection rechtstreeks aanmaken in de test, dezelfde registraties uitvoeren als in MauiProgram, en zo integratietests draaien op de container-configuratie zelf.
Veelgestelde vragen
Wat is het verschil tussen AddSingleton, AddScoped en AddTransient in .NET MAUI?
AddSingleton maakt één instantie aan die zo lang leeft als de app, AddTransient maakt elke resolve een nieuwe instantie, en AddScoped maakt één instantie per scope. In een gewone XAML-MAUI-app bestaat er geen automatische scope buiten BlazorWebView of HybridWebView, dus AddScoped gedraagt zich daar als AddTransient. Kies AddSingleton voor state, AddTransient voor ViewModels en pages.
Kan ik DependencyService uit Xamarin nog gebruiken in .NET MAUI?
Technisch werkt DependencyService nog tijdens een Xamarin-migratie, maar het is gedeprecateerd en Microsoft raadt aan over te stappen op de standaard IServiceCollection-DI via MauiAppBuilder.Services. De moderne API biedt constructor-injectie, lifetimes, keyed services en betere testbaarheid; voordelen die DependencyService simpelweg niet heeft.
Hoe los ik een service op buiten een page of ViewModel?
Gebruik Handler.MauiContext.Services in handler-code, of Application.Current.Handler.MauiContext.Services elders. Vanaf .NET 9 is IPlatformApplication.Current.Services de aanbevolen ingang. Beperk dit tot infrastructuurcode; voor businesslogica blijft constructor-injectie de juiste keuze.
Waarom krijg ik "The visual element is already a child of another element"?
Deze exception treedt op wanneer je een page als singleton registreert en er meerdere keren naartoe navigeert. MAUI's visuele boom staat maar één parent per element toe. Verander de registratie naar AddTransient en de fout verdwijnt.
Ondersteunt .NET MAUI keyed services?
Ja, sinds .NET 8 en volledig getest in MAUI 9 en 10. Je registreert met AddKeyedSingleton, AddKeyedScoped of AddKeyedTransient, en injecteert via het [FromKeyedServices("naam")]-attribute op constructor-parameters. Ideaal voor meerdere implementaties van dezelfde interface.
Zo verbeter je .NET MAUI CollectionView performance: virtualisatie, compiled bindings met x:DataType en platte DataTemplates voor stabiele 60 fps scrollen.
Leer hoe je CommunityToolkit.Mvvm gebruikt in .NET MAUI 9 om boilerplate te elimineren met source generators — inclusief ObservableProperty, RelayCommand, async commands, validatie en messaging.
Leer alles over .NET MAUI Shell navigatie: routes registreren, data doorgeven met query parameters, deep linking op Android en iOS, en een testbare MVVM-navigatieservice bouwen met praktische .NET 10 codevoorbeelden.