Loading...
Loading...
Guidance for configuring dependency injection in .NET MAUI apps — service registration in MauiProgram.cs, lifetime selection (Singleton / Transient / Scoped), constructor injection, Shell navigation auto-resolution, platform-specific registrations, and testability patterns. USE FOR: "dependency injection", "DI setup", "AddSingleton", "AddTransient", "AddScoped", "service registration", "constructor injection", "IServiceProvider", "MauiProgram DI", "register services", "BindingContext injection". DO NOT USE FOR: data binding (use maui-data-binding), Shell route configuration (use maui-shell-navigation), unit-test mocking frameworks (use standard xUnit and NSubstitute patterns).
npx skill4agent add managedcode/dotnet-skills maui-dependency-injectionMicrosoft.Extensions.DependencyInjectionMauiProgram.CreateMauiApp()builder.ServicesMauiProgram.csAddSingletonAddTransientAddScoped#ifMauiProgram.csAddSingletonAddTransientMauiProgram.CreateMauiApp()builder.ServicesAppShell.xaml.csBindingContext#ifnull| Lifetime | When to Use | Typical Types |
|---|---|---|
| Shared state, expensive to create, app-wide config | |
| Lightweight, stateless, or needs a fresh instance per use | Pages, ViewModels, per-call API wrappers |
| Per-scope lifetime with manually created | Scoped unit-of-work (rare in MAUI) |
⚠️ Avoidunless you manually manageAddScoped. MAUI has no built-in request scope like ASP.NET Core. A Scoped registration without an explicit scope silently behaves as a Singleton, leading to subtle bugs.IServiceScope
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder.UseMauiApp<App>();
// Services — Singleton for shared state
builder.Services.AddSingleton<IDataService, DataService>();
builder.Services.AddSingleton<ISettingsService, SettingsService>();
// HTTP — use typed or named clients via IHttpClientFactory
// Requires NuGet: Microsoft.Extensions.Http
builder.Services.AddHttpClient<IApiClient, ApiClient>();
// ViewModels — Transient for fresh state per navigation
builder.Services.AddTransient<MainViewModel>();
builder.Services.AddTransient<DetailViewModel>();
// Pages — Transient so constructor injection fires each time
builder.Services.AddTransient<MainPage>();
builder.Services.AddTransient<DetailPage>();
return builder.Build();
}public class MainViewModel
{
private readonly IDataService _dataService;
public MainViewModel(IDataService dataService)
{
_dataService = dataService;
}
public async Task LoadAsync() => Items = await _dataService.GetItemsAsync();
}BindingContextpublic partial class MainPage : ContentPage
{
public MainPage(MainViewModel viewModel)
{
InitializeComponent();
BindingContext = viewModel;
}
}// MauiProgram.cs
builder.Services.AddTransient<DetailPage>();
builder.Services.AddTransient<DetailViewModel>();
// AppShell.xaml.cs
Routing.RegisterRoute(nameof(DetailPage), typeof(DetailPage));
// Navigate — DI resolves DetailPage + DetailViewModel
await Shell.Current.GoToAsync(nameof(DetailPage));null#if ANDROID
builder.Services.AddSingleton<INotificationService, AndroidNotificationService>();
#elif IOS || MACCATALYST
builder.Services.AddSingleton<INotificationService, AppleNotificationService>();
#elif WINDOWS
builder.Services.AddSingleton<INotificationService, WindowsNotificationService>();
#else
builder.Services.AddSingleton<INotificationService, NoOpNotificationService>();
#endif// From any Element with a Handler
var service = this.Handler.MauiContext.Services.GetService<IDataService>();IServiceProviderpublic class NavigationService(IServiceProvider serviceProvider)
{
public T ResolvePage<T>() where T : Page
=> serviceProvider.GetRequiredService<T>();
}public interface IDataService
{
Task<List<Item>> GetItemsAsync();
}
// Production registration
builder.Services.AddSingleton<IDataService, DataService>();
// Test registration — swap without touching production code
var services = new ServiceCollection();
services.AddSingleton<IDataService, FakeDataService>();// ❌ ViewModel keeps stale state across navigations
builder.Services.AddSingleton<DetailViewModel>();
// ✅ Fresh instance each navigation
builder.Services.AddTransient<DetailViewModel>();<ShellContent ContentTemplate="...">builder.Servicesnull// ❌ Missing — injection silently skipped
// builder.Services.AddTransient<DetailPage>();
// ✅ Always register pages that need injection
builder.Services.AddTransient<DetailPage>();
builder.Services.AddTransient<DetailViewModel>();App.xamlInitializeComponent()CreateWindow()public partial class App : Application
{
private readonly IServiceProvider _services;
public App(IServiceProvider services)
{
_services = services;
InitializeComponent();
}
protected override Window CreateWindow(IActivationState? activationState)
{
// Safe — container is fully built
// Requires: builder.Services.AddTransient<AppShell>() in MauiProgram.cs
var appShell = _services.GetRequiredService<AppShell>();
return new Window(appShell);
}
}// ❌ Hides dependencies, hard to test
var svc = this.Handler.MauiContext.Services.GetService<IDataService>();
// ✅ Constructor injection — explicit and testable
public class MyViewModel(IDataService dataService) { }#ifGetService<T>()null#elseAddScopedIServiceScopeAddTransientAddSingletonMauiProgram.csAddTransientAddSingleton#ifCreateWindow()AddScopedIServiceScope