
Maui Dependency Injection
- 51 installs
- 163 repo stars
- Updated July 6, 2026
- davidortinau/maui-skills
Covers dependency injection in .NET MAUI apps including service registration, lifetime selection, and constructor injection.
About
Guides dependency injection in .NET MAUI apps covering service registration, lifetime selection (Singleton/Transient/Scoped) and constructor injection. A developer uses it when structuring services and dependencies in a MAUI app.
- Service registration and lifetime selection
- Constructor injection patterns
Maui Dependency Injection by the numbers
- 51 all-time installs (skills.sh)
- Ranked #593 of 1,039 Mobile Development skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/davidortinau/maui-skills --skill maui-dependency-injectionAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 51 |
|---|---|
| repo stars | ★ 163 |
| Last updated | July 6, 2026 |
| Repository | davidortinau/maui-skills ↗ |
What it does
Covers dependency injection in .NET MAUI apps including service registration, lifetime selection, and constructor injection.
Files
Dependency 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 `AddScoped` in MAUI — there is no built-in scope per page.
Using it without manually creating IServiceScope gives you singletonbehaviour 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 constructorinjection 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
Dependency Injection API Reference
Service Registration in MauiProgram.cs
Register services on builder.Services inside CreateMauiApp():
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder.UseMauiApp<App>();
// Services
builder.Services.AddSingleton<IDataService, DataService>();
builder.Services.AddTransient<IApiClient, ApiClient>();
// ViewModels
builder.Services.AddTransient<MainViewModel>();
builder.Services.AddTransient<DetailViewModel>();
// Pages
builder.Services.AddTransient<MainPage>();
builder.Services.AddTransient<DetailPage>();
return builder.Build();
}Lifetime Reference
| Lifetime | Use When | Examples |
|---|---|---|
AddSingleton<T> | Shared state, expensive to create, or app-wide config | Database connection, settings service, HttpClient factory |
AddTransient<T> | Stateless, lightweight, or per-request usage | ViewModels, pages, API call wrappers |
AddScoped<T> | Per-scope lifetime (rarely used in MAUI — no built-in scope per page) | Scoped unit-of-work in manually created scopes |
Constructor Injection
Inject dependencies through the constructor. The DI container resolves them automatically when the type is itself resolved from the container:
public class MainViewModel
{
private readonly IDataService _dataService;
private readonly IApiClient _apiClient;
public MainViewModel(IDataService dataService, IApiClient apiClient)
{
_dataService = dataService;
_apiClient = apiClient;
}
}ViewModel → Page Pattern
Register both the ViewModel and the Page. Inject the ViewModel into the Page constructor and assign it as BindingContext:
public partial class MainPage : ContentPage
{
public MainPage(MainViewModel viewModel)
{
InitializeComponent();
BindingContext = viewModel;
}
}Automatic Resolution via Shell Navigation
When Pages are registered in the DI container and registered as Shell routes, Shell resolves them (and their dependencies) automatically:
// In MauiProgram.cs
builder.Services.AddTransient<DetailPage>();
builder.Services.AddTransient<DetailViewModel>();
// Route registration (AppShell.xaml.cs or startup)
Routing.RegisterRoute(nameof(DetailPage), typeof(DetailPage));
// Navigation — DI resolves DetailPage and its DetailViewModel
await Shell.Current.GoToAsync(nameof(DetailPage));Explicit Resolution
When constructor injection is not available, resolve services explicitly:
// From any Element with a Handler
var service = this.Handler.MauiContext.Services.GetService<IDataService>();IServiceProvider Injection
Inject IServiceProvider when you need to resolve services dynamically:
public class MyService
{
private readonly IServiceProvider _serviceProvider;
public MyService(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public void DoWork()
{
var api = _serviceProvider.GetRequiredService<IApiClient>();
}
}Platform-Specific Service Registration
Use preprocessor directives to register platform-specific implementations:
// In MauiProgram.cs
#if ANDROID
builder.Services.AddSingleton<INotificationService, AndroidNotificationService>();
#elif IOS || MACCATALYST
builder.Services.AddSingleton<INotificationService, AppleNotificationService>();
#elif WINDOWS
builder.Services.AddSingleton<INotificationService, WindowsNotificationService>();
#endifInterface-First Pattern for Testability
Define interfaces for services so implementations can be swapped in tests:
public interface IDataService
{
Task<List<Item>> GetItemsAsync();
}
public class DataService : IDataService
{
public async Task<List<Item>> GetItemsAsync() { /* ... */ }
}
// Register the interface → implementation mapping
builder.Services.AddSingleton<IDataService, DataService>();In tests, substitute a mock without touching production code:
var services = new ServiceCollection();
services.AddSingleton<IDataService, FakeDataService>();