About maui-dependency-injection
This skill offers .NET MAUI developers guidance on dependency-injection lifetimes and registrations. It focuses on page and ViewModel registration, XAML initialization timing, and common platform-specific service pitfalls.
Key features
- 01Choose lifetimes based on state and use
- 02Register pages and ViewModels as transient
- 03Defer service resolution to CreateWindow
- 04Avoid overusing the service locator
- 05Check platform-specific registration coverage
Use cases
- 01Set ViewModel lifetimes for page navigation
- 02Troubleshoot pages missing injected dependencies
- 03Handle XAML resources parsed before DI is ready
- 04Prevent missing services from platform conditionals
Install
Copy the command and run it in a terminal.
npx skills add https:/ / github.com/ davidortinau/ maui-skills --skill maui-dependency-injectionAfter installing, type/maui-dependency-injectionin Claude Code to confirm it loaded.
Bundled filesreferences/dependency-injection-api.md
Details
From the project READMEDependency Injection in .NET MAUI
Lifetime Decision Framework
| Question | Answer → Lifetime |
|---|---|
| Does it hold shared state or is expensive to create? | AddSingleton |
| Is it stateless, lightweight, or per-request? | AddTransient |
Do you manage IServiceScope yourself? | AddScoped |
⚠️ Avoid
AddScopedin MAUI — there is no built-in scope per page. Using it without manually creatingIServiceScopegives you singleton behaviour silently, which is confusing and error-prone.
Singleton traps
// ❌ ViewModel registered as Singleton — stale data across navigations
builder.Services.AddSingleton<DetailViewModel>();
// ✅ ViewModels are Transient — fresh instance each navigation
builder.Services.AddTransient<DetailViewModel>();
Register Pages and ViewModels as Transient. Register services that hold shared state as Singleton (e.g.,
IDataService,HttpClientfactory).
Gotcha: XAML Resource Parsing vs. DI Timing
XAML resources (App.xaml styles, converters) are parsed during
InitializeComponent() — before the DI container is fully available. If a
resource or converter needs a service, resolve it in CreateWindow(), not
in the constructor.
// ❌ Resolving services during XAML parse — container may not be ready
public App(IDataService data)
{
InitializeComponent(); // XAML parses here
_data = data; // may fail for types not yet resolved
}
// ✅ Defer service resolution 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
var mainPage = _services.GetRequiredService<MainPage>();
return new Window(new AppShell());
}
}
Gotcha: Unregistered Page Silently Skips DI
If a Page is used in Shell XAML (<ShellContent ContentTemplate="...">) but
not registered in builder.Services, MAUI instantiates it with the
parameterless constructor. Dependencies are silently null — no exception.
// ❌ Page not registered — constructor injection silently skipped
// builder.Services.AddTransient<DetailPage>(); // missing!
// ✅ Always register pages that need injection
builder.Services.AddTransient<DetailPage>();
builder.Services.AddTransient<DetailViewModel>();
Anti-Pattern: Service Locator Overuse
// ❌ Service locator scattered through code — hard to test, hides dependencies
public void DoWork()
{
var service = this.Handler.MauiContext.Services.GetService<IDataService>();
service.Load();
}
// ✅ Constructor injection — explicit, testable
public class MyViewModel(IDataService dataService)
{
public void DoWork() => dataService.Load();
}
Use explicit resolution (
Handler.MauiContext.Services) only when constructor injection is genuinely unavailable (e.g., inside a custom handler or platform callback).
Platform-Specific Registration Pitfall
When using #if directives for platform services, ensure the interface
is always registered — otherwise consumers on untargeted platforms get a
runtime null.
// ❌ No registration on Windows — GetService returns null
#if ANDROID
builder.Services.AddSingleton<INotificationService, AndroidNotificationService>();
#elif IOS || MACCATALYST
builder.Services.AddSingleton<INotificationService, AppleNotificationService>();
#endif
// ✅ Cover all platforms or provide a no-op fallback
#if ANDROID
builder.Services.AddSingleton<INotificationService, AndroidNotificationService>();
#elif IOS || MACCATALYST
builder.Services.AddSingleton<INotificationService, AppleNotificationService>();
#elif WINDOWS
builder.Services.AddSingleton<INotificationService, WindowsNotificationService>();
#endif
Checklist
- Every Page and ViewModel that needs injection is registered in
MauiProgram.cs - Pages/ViewModels are
AddTransient; shared services areAddSingleton - Constructor injection used everywhere possible; service locator only as last resort
- Interfaces defined for any service you need to mock in tests
- Platform-specific
#ifregistrations cover all target platforms (or provide fallback) - Late-bound services resolved in
CreateWindow(), not during XAML parse -
AddScopedonly used when you manually manageIServiceScope