Install any skill in seconds. Free to start, no credit card required.
Get Started Free →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".
.claude/skills/managedcode-maui-dependency-injection/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 58% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 74% | 0% |
| case-04 | ✓→✓ | = Same ✓ | 79% | 0% |
| case-05 | ✓→✓ | = Same ✓ | 43% | 0% |
| case-06 | ✓→✓ | = Same ✓ | 106% | 0% |
.NET MAUI uses the same Microsoft.Extensions.DependencyInjection container as ASP.NET Core. All service registration happens in MauiProgram.CreateMauiApp() on builder.Services. The container is built once at startup and is immutable thereafter.
MauiProgram.csAddSingleton, AddTransient, and AddScoped#if directivesMauiProgram.cs fileAddSingleton for shared services, AddTransient for Pages and ViewModels.MauiProgram.CreateMauiApp() on builder.Services, grouping by category (services, HTTP, ViewModels, Pages).AppShell.xaml.cs so Shell navigation auto-resolves the full dependency graph.BindingContext.#if directives, ensuring every target platform is covered or has a fallback.null dependencies or missing-registration exceptions at runtime.| Lifetime | When to Use | Typical Types | |---|---|---| | AddSingleton<T>() | Shared state, expensive to create, app-wide config | HttpClient factory, settings service, database connection | | AddTransient<T>() | Lightweight, stateless, or needs a fresh instance per use | Pages, ViewModels, per-call API wrappers | | AddScoped<T>() | Per-scope lifetime with manually created IServiceScope | Scoped unit-of-work (rare in MAUI) |
Key rule: Register Pages and ViewModels as Transient. Register shared services as Singleton.
> ⚠️ Avoid AddScoped unless you manually manage IServiceScope. 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.
csharppublic 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(); }
Inject dependencies through constructor parameters. The container resolves them automatically when the type is itself resolved from DI.
csharppublic class MainViewModel { private readonly IDataService _dataService; public MainViewModel(IDataService dataService) { _dataService = dataService; } public async Task LoadAsync() => Items = await _dataService.GetItemsAsync(); }
Register both Page and ViewModel. Inject the ViewModel into the Page and assign it as BindingContext:
csharppublic partial class MainPage : ContentPage { public MainPage(MainViewModel viewModel) { InitializeComponent(); BindingContext = viewModel; } }
When a Page is registered in DI and as a Shell route, Shell resolves it (and its full dependency graph) automatically on navigation:
csharp// 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));
Use preprocessor directives to register platform implementations. Always cover every target platform or provide a no-op fallback to avoid runtime null.
csharp#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
Prefer constructor injection. Use explicit resolution only where injection is genuinely unavailable (custom handlers, platform callbacks):
csharp// From any Element with a Handler var service = this.Handler.MauiContext.Services.GetService<IDataService>();
For dynamic resolution, inject IServiceProvider:
csharppublic class NavigationService(IServiceProvider serviceProvider) { public T ResolvePage<T>() where T : Page => serviceProvider.GetRequiredService<T>(); }
Define interfaces for every service so implementations can be swapped in tests:
csharppublic 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>();
csharp// ❌ ViewModel keeps stale state across navigations builder.Services.AddSingleton<DetailViewModel>(); // ✅ Fresh instance each navigation builder.Services.AddTransient<DetailViewModel>();
If a Page appears in Shell XAML via <ShellContent ContentTemplate="..."> but is not registered in builder.Services, MAUI creates it with the parameterless constructor. Dependencies are silently null — no exception is thrown.
csharp// ❌ Missing — injection silently skipped // builder.Services.AddTransient<DetailPage>(); // ✅ Always register pages that need injection builder.Services.AddTransient<DetailPage>(); builder.Services.AddTransient<DetailViewModel>();
XAML resources in App.xaml are parsed during InitializeComponent() — before the container is fully available. Defer service-dependent work to CreateWindow():
csharppublic 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); } }
csharp// ❌ Hides dependencies, hard to test var svc = this.Handler.MauiContext.Services.GetService<IDataService>(); // ✅ Constructor injection — explicit and testable public class MyViewModel(IDataService dataService) { }
Forgetting a platform in #if blocks means GetService<T>() returns null at runtime on that platform. Always include an #else fallback or cover every target.
AddScoped in MAUI without creating IServiceScope manually gives Singleton behavior silently. Use AddTransient or AddSingleton instead unless you explicitly manage scopes.
MauiProgram.csAddTransient; shared services use AddSingleton#if registrations cover all target platforms or include a fallbackCreateWindow(), not run during XAML parseAddScoped only used alongside manually created IServiceScopeOther measured skills in the registry, with their headline benchmark lift.