Maui Dependency Injection
.NET (Microsoft)OfficialPublished by .NET (Microsoft)
- Skill
- Tool integration
- Software development
- MIT
- No issues found
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
This skill provides comprehensive instructions for configuring dependency injection (DI) in .NET MAUI projects. It covers service registration within MauiProgram.cs, selecting appropriate service lifetimes (Singleton, Transient, Scoped), implementing constructor injection for ViewModels and Pages, and integrating DI with Shell navigation. It also addresses platform-specific service registration and patterns for improving testability through interface-based design.
What it does
- Explains how to register services, ViewModels, and Pages in MauiProgram.cs.
- Provides rules for choosing between Singleton, Transient, and Scoped lifetimes.
- Demonstrates constructor injection patterns for Pages and ViewModels.
- Shows how to integrate DI with Shell navigation for automatic dependency resolution.
- Details how to handle platform-specific service implementations using preprocessor directives.
- Offers best practices for testability using interface-first design.
When to use it
- When setting up the DI container in a new .NET MAUI application.
- When adding new services or ViewModels that require dependencies.
- When refactoring code to improve testability through dependency inversion.
- When needing to resolve platform-specific implementations of a shared interface.
- When troubleshooting issues with service resolution or stale state in ViewModels.
Advantages
- Promotes decoupled, testable, and maintainable code architecture.
- Ensures consistent service lifetime management across the application.
- Simplifies dependency management through automatic constructor injection.
- Provides clear patterns for handling platform-specific logic safely.
Usage examples
- “How do I register a ViewModel and a Page in .NET MAUI for DI?”
- The agent provides the code to add the types to builder.Services in MauiProgram.cs using AddTransient, and shows how to inject the ViewModel into the Page constructor.
- “Should I use AddSingleton or AddTransient for my Page?”
- The agent explains that Pages should typically be registered as Transient to ensure a fresh instance per navigation, avoiding stale state issues.
- “How do I register a service that behaves differently on Android and iOS?”
- The agent provides an example using #if ANDROID and #elif IOS directives within MauiProgram.cs to register the appropriate implementation for each platform.
- “Why is my injected service null in my Page?”
- The agent guides the user to check if the Page and its dependencies are registered in the DI container and if the Page is being navigated to via Shell routes.
Install
Download the skill as a ZIP:
https://autogineer.com/agent-skills/dotnet/maui-dependency-injection/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip maui-dependency-injection.zip -d .agents/skills
Documentation by .NET (Microsoft)
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.csChoosing between
AddSingleton,AddTransient, andAddScopedWiring constructor injection for Pages and ViewModels
Leveraging Shell navigation to auto-resolve DI-registered Pages
Registering platform-specific service implementations with
#ifdirectivesDesigning 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.csfileKnowledge of which services, ViewModels, and Pages need registration
Target platforms (Android, iOS, Mac Catalyst, Windows) for conditional registrations
Rules That Change the Answer
| Situation | Do this | Why |
|---|---|---|
| Registering a Page or ViewModel | Prefer AddTransient | A fresh instance per navigation avoids stale state, and a Singleton page cannot be re-added to the visual tree after it is removed. Singleton is defensible for a genuinely single-instance page (e.g. a root tab you want to keep warm) |
| Registering shared/expensive state | AddSingleton | One instance app-wide (settings, DB connection, HttpClient handler) |
Tempted to use AddScoped | Use AddTransient (or AddSingleton if sharing is intended) | MAUI has no built-in request scope like ASP.NET Core's HTTP pipeline. MAUI does create one IServiceScope per window, so a Scoped service lives as long as that window — and resolved from the root provider it behaves like a Singleton. Neither gives you per-navigation freshness |
| Navigating to a DI-registered page | Register the page and its ViewModel, then Routing.RegisterRoute | Shell.Current.GoToAsync resolves the page through DI and injects its constructor dependencies |
| Platform-specific implementation | #if per platform with every platform covered | A missing platform branch leaves the service unregistered and throws at resolution time |
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.
// Explicit scope when you genuinely need unit-of-work semantics
using var scope = scopeFactory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<MyDbContext>();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
| 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-window lifetime, or a manually created IServiceScope | Scoped unit-of-work (rare in MAUI) |
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
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();
}Constructor Injection
Inject dependencies through constructor parameters. The container resolves them automatically when the type is itself resolved from DI.
public class MainViewModel
{
private readonly IDataService _dataService;
public MainViewModel(IDataService dataService)
{
_dataService = dataService;
}
public async Task LoadAsync() => Items = await _dataService.GetItemsAsync();
}ViewModel → Page Wiring
Register both Page and ViewModel. Inject the ViewModel into the Page and assign it as BindingContext:
public partial class MainPage : ContentPage
{
public MainPage(MainViewModel viewModel)
{
InitializeComponent();
BindingContext = viewModel;
}
}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:
// 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));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:
public class DetailViewModel : ObservableObject, IQueryAttributable
{
readonly IDataService _data; // ← injected by DI
public DetailViewModel(IDataService data) => _data = data;
public void ApplyQueryAttributes(IDictionary<string, object> query)
{
// ← supplied by navigation
if (query.TryGetValue("id", out var id))
LoadAsync(id.ToString()!);
}
}
// Navigate with a parameter — the page and its ViewModel still come from DI
await Shell.Current.GoToAsync($"{nameof(DetailPage)}?id={product.Id}");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.
#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>();
#endifExplicit Resolution (Last Resort)
Prefer constructor injection. Use explicit resolution only where injection is genuinely unavailable (custom handlers, platform callbacks):
// From any Element with a Handler
var service = this.Handler.MauiContext.Services.GetService<IDataService>();For dynamic resolution, inject IServiceProvider:
public class NavigationService(IServiceProvider serviceProvider)
{
public T ResolvePage<T>() where T : Page
=> serviceProvider.GetRequiredService<T>();
}Interface-First Pattern for Testability
Define interfaces for every service so implementations can be swapped in tests:
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>();Common Pitfalls
1. Singleton ViewModels Cause Stale Data
// ❌ ViewModel keeps stale state across navigations
builder.Services.AddSingleton<DetailViewModel>();
// ✅ Fresh instance each navigation
builder.Services.AddTransient<DetailViewModel>();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.
// Registering the page and its dependencies keeps both paths working
builder.Services.AddTransient<DetailPage>();
builder.Services.AddTransient<DetailViewModel>();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():
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);
}
}4. Service Locator Anti-Pattern
// ❌ Hides dependencies, hard to test
var svc = this.Handler.MauiContext.Services.GetService<IDataService>();
// ✅ Constructor injection — explicit and testable
public class MyViewModel(IDataService dataService) { }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
References
Files
- ATTRIBUTION.mdAdded by AutoGineer861 B
- LICENSELicence file added by AutoGineer1.1 KiB
- SKILL.md14.1 KiB
- references/dependency-injection-api.md4.1 KiB
LICENSE
The MIT License (MIT) Copyright (c) .NET Foundation and Contributors All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Requirements and compatibility
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Security
No issues found by our static scan· No issues found
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Published by
- .NET (Microsoft)
- Publisher
- .NET (Microsoft) (Official)
- Source
- dotnet/skills · plugins/dotnet-maui/skills/maui-dependency-injection · commit 96c3d68
- Licence
- MIT License (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by .NET (Microsoft) or .NET (Microsoft).
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download maui-dependency-injection.zip