Dependency Injection in .NET MAUI: The Built-In Container
One of the best architectural decisions in MAUI was adopting the same dependency injection container that ASP.NET Core uses. If you've built web APIs with .NET, you already know how this works. No more DependencyService.Register or third-party containers just to get basic DI.
Registration in MauiProgram
All service registration happens in MauiProgram.cs via the builder.Services collection:
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
});
// Services
builder.Services.AddSingleton<IApiClient, ApiClient>();
builder.Services.AddSingleton<ISettingsService, SettingsService>();
builder.Services.AddTransient<IOrderService, OrderService>();
// View models
builder.Services.AddTransient<MainViewModel>();
builder.Services.AddTransient<OrderDetailViewModel>();
// Pages
builder.Services.AddTransient<MainPage>();
builder.Services.AddTransient<OrderDetailPage>();
return builder.Build();
}
}
The three lifetimes work exactly as they do in ASP.NET Core:
- Singleton — one instance for the entire application lifetime
- Transient — new instance every time it's requested
- Scoped — one instance per scope (less commonly used in MAUI than in web apps)
Constructor Injection
Pages and view models receive their dependencies through constructors:
public class OrderDetailViewModel : ObservableObject
{
private readonly IOrderService _orderService;
private readonly ISettingsService _settings;
public OrderDetailViewModel(
IOrderService orderService,
ISettingsService settings)
{
_orderService = orderService;
_settings = settings;
}
[RelayCommand]
private async Task LoadOrderAsync(int orderId)
{
var order = await _orderService.GetByIdAsync(orderId);
// ...
}
}
For pages, inject the view model and assign it to the binding context:
public partial class OrderDetailPage : ContentPage
{
public OrderDetailPage(OrderDetailViewModel viewModel)
{
InitializeComponent();
BindingContext = viewModel;
}
}
When Shell navigates to OrderDetailPage, the DI container resolves it automatically, injecting the view model and all its dependencies.
Registering Platform-Specific Services
Combine DI with partial classes for clean platform abstraction:
// Shared interface
public interface IBiometricService
{
Task<bool> AuthenticateAsync(string reason);
}
// Platform implementations via partial classes
public partial class BiometricService : IBiometricService
{
public partial Task<bool> AuthenticateAsync(string reason);
}
// Registration
builder.Services.AddSingleton<IBiometricService, BiometricService>();
The container doesn't care which platform implementation gets compiled in — it just resolves IBiometricService to whatever BiometricService is at runtime.
HttpClient with IHttpClientFactory
For HTTP services, use IHttpClientFactory just as you would in ASP.NET Core:
builder.Services.AddHttpClient<IApiClient, ApiClient>(client =>
{
client.BaseAddress = new Uri("https://api.example.com/");
client.DefaultRequestHeaders.Add("Accept", "application/json");
});
This gives you proper HttpClient lifecycle management — no socket exhaustion from creating clients manually:
public class ApiClient : IApiClient
{
private readonly HttpClient _httpClient;
public ApiClient(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<List<Order>> GetOrdersAsync()
{
return await _httpClient.GetFromJsonAsync<List<Order>>("orders")
?? new List<Order>();
}
}
Service Resolution Without Constructor Injection
Sometimes you need to resolve a service outside a constructor — in a converter, a behaviour, or a static context. Use Application.Current.Handler.MauiContext.Services:
var service = Application.Current?.Handler?.MauiContext?.Services
.GetService<ISettingsService>();
This should be a last resort. If you find yourself doing this often, it's usually a sign that your architecture needs restructuring. Prefer constructor injection wherever possible.
Common Patterns
View Model-First Navigation
Register both pages and view models, then resolve pages through DI when navigating. This ensures every page gets a fresh view model with all its dependencies:
builder.Services.AddTransient<ItemDetailPage>();
builder.Services.AddTransient<ItemDetailViewModel>();
Keyed Services (.NET 8+)
If you have multiple implementations of the same interface, use keyed services:
builder.Services.AddKeyedSingleton<IStorageService, LocalStorageService>("local");
builder.Services.AddKeyedSingleton<IStorageService, CloudStorageService>("cloud");
Resolve with the [FromKeyedServices] attribute:
public class SyncViewModel(
[FromKeyedServices("local")] IStorageService local,
[FromKeyedServices("cloud")] IStorageService cloud)
{
// ...
}
Configuration
Bind configuration sections from appsettings.json or other sources:
builder.Configuration.AddJsonFile("appsettings.json");
builder.Services.Configure<ApiOptions>(
builder.Configuration.GetSection("Api"));
What to Register as What
- Singletons: Services that hold shared state or are expensive to create (database connections, settings, caches)
- Transient: View models and pages (each navigation should get a fresh instance)
- Scoped: Rarely used in MAUI. Consider it if you implement a custom scope per navigation session
The built-in container covers the vast majority of MAUI applications. You don't need Autofac, DryIoc, or any third-party container unless you have specific advanced requirements like property injection or named registrations beyond keyed services.