Dependency Injection in .NET MAUI
.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.
When to Use
- Registering services, ViewModels, and Pages in
MauiProgram.cs - Choosing between
AddSingleton,AddTransient, andAddScoped - Wiring constructor injection for Pages and ViewModels
- Leveraging Shell navigation to auto-resolve DI-registered Pages
- Registering platform-specific service implementations with
#ifdirectives - Designing interfaces for testable service layers
When Not to Use
- XAML data-binding syntax or compiled bindings — use the maui-data-binding skill
- Shell route registration and query parameters — use the maui-shell-navigation skill
- Mocking frameworks or test runners — use standard .NET testing tools (xUnit, NUnit, MSTest) and mocking libraries (NSubstitute, Moq)
Inputs
- A .NET MAUI project with a
MauiProgram.csfile - Knowledge of which services, ViewModels, and Pages need registration
- Target platforms (Android, iOS, Mac Catalyst, Windows) for conditional registrations
Rules That Change the Answer
Do not introduce DI into a project that isn't using it, swap a working service lifetime, or add an interface purely for symmetry — only when the user asked or it fixes a real defect.
Answer narrowly, but completely. When you recommend a lifetime change, show the
registration code, and give the realistic alternatives rather than a single verdict —
for a unit-of-work or DbContext question that means AddTransient, an explicit
IServiceScopeFactory.CreateScope(), and the factory pattern
(AddDbContextFactory), with a note on when each fits. A one-line prescription is
usually a worse answer than a short menu with trade-offs.
Workflow
- Identify all services, ViewModels, and Pages that need to participate in dependency injection.
- Choose the correct lifetime for each type —
AddSingletonfor shared services,AddTransientfor Pages and ViewModels. - Register all types in
MauiProgram.CreateMauiApp()onbuilder.Services, grouping by category (services, HTTP, ViewModels, Pages). - Register Pages as Shell routes in
AppShell.xaml.csso Shell navigation auto-resolves the full dependency graph. - Wire each Page to its ViewModel via constructor injection, assigning the ViewModel as
BindingContext. - Add platform-specific registrations with
#ifdirectives, ensuring every target platform is covered or has a fallback. - Verify resolution works by running the app and confirming no
nulldependencies or missing-registration exceptions at runtime.
Lifetime Selection
Key rule: Register Pages and ViewModels as Transient by default. Register shared services as Singleton.
⚠️ Avoid
AddScopedunless you manually manageIServiceScope. MAUI has no built-in request scope like ASP.NET Core. MAUI creates oneIServiceScopeper window, so a Scoped service lives as long as that window; resolved from the root provider it silently behaves as a Singleton. Neither gives per-navigation freshness.
Registration Pattern in MauiProgram.cs
Constructor Injection
Inject dependencies through constructor parameters. The container resolves them automatically when the type is itself resolved from DI.
ViewModel → Page Wiring
Register both Page and ViewModel. Inject the ViewModel into the Page and assign it as BindingContext:
Shell Navigation Auto-Resolution
When a Page is registered in DI and as a Shell route, Shell resolves it (and its full dependency graph) automatically on navigation:
Passing parameters to a DI-resolved ViewModel
DI supplies the ViewModel's dependencies; navigation parameters arrive separately.
Don't try to inject them through the constructor — implement IQueryAttributable
on the ViewModel so it receives both:
Shell applies query attributes to the page and its BindingContext, so the
ViewModel receives them without any wiring in the page.
Platform-Specific Registration
Use preprocessor directives to register platform implementations. Always cover every target platform or provide a no-op fallback to avoid runtime null.
Explicit Resolution (Last Resort)
Prefer constructor injection. Use explicit resolution only where injection is genuinely unavailable (custom handlers, platform callbacks):
For dynamic resolution, inject IServiceProvider:
Interface-First Pattern for Testability
Define interfaces for every service so implementations can be swapped in tests:
Common Pitfalls
1. Singleton ViewModels Cause Stale Data
2. ContentTemplate Pages Are Not Created Through DI
Pages declared in Shell XAML via <ShellContent ContentTemplate="{DataTemplate views:DetailPage}"> are instantiated with Activator.CreateInstance (ElementTemplate.cs), not through the service provider. Constructor injection does not run on that path: if the page's only constructor takes dependencies, you get a MissingMethodException — not a silently null dependency.
Pages reached through Routing.RegisterRoute + GoToAsync are different: they go through ActivatorUtilities.GetServiceOrCreateInstance (Routing.cs), which injects registered dependencies even if the page type itself was never registered, and throws if a required dependency cannot be resolved.
If you need DI for a tab/flyout page, give it a parameterless constructor that resolves what it needs, or navigate to it by route instead of embedding it in ContentTemplate.
3. XAML Resource Parsing vs. DI Timing
XAML resources in App.xaml are parsed during InitializeComponent() — before the container is fully available. Defer service-dependent work to CreateWindow():
4. Service Locator Anti-Pattern
5. Missing Platform in Conditional Registration
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.
6. AddScoped Without Manual Scope
See the rule table above: AddScoped gives you either window lifetime or Singleton behaviour, never per-navigation freshness. Use AddTransient or AddSingleton unless you explicitly create and manage an IServiceScope.
Checklist
- Every Page and ViewModel that needs injection is registered in
MauiProgram.cs - Pages and ViewModels use
AddTransient; shared services useAddSingleton - Constructor injection used everywhere possible; service locator only as last resort
- Interfaces defined for services that need test substitution
- Platform-specific
#ifregistrations cover all target platforms or include a fallback - Service-dependent work deferred to
CreateWindow(), not run during XAML parse -
AddScopedused only when window lifetime is intended, or alongside a manually createdIServiceScope
