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:

MauiProgram.cs
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:

Constructor Injection

Pages and view models receive their dependencies through constructors:

Example.cs
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:

Example.cs
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:

Example.cs
// 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:

Example.cs
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:

Example.cs
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:

Example.cs
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:

Example.cs
builder.Services.AddTransient<ItemDetailPage>();
builder.Services.AddTransient<ItemDetailViewModel>();

Keyed Services (.NET 8+)

If you have multiple implementations of the same interface, use keyed services:

Example.cs
builder.Services.AddKeyedSingleton<IStorageService, LocalStorageService>("local");
builder.Services.AddKeyedSingleton<IStorageService, CloudStorageService>("cloud");

Resolve with the [FromKeyedServices] attribute:

Example.cs
public class SyncViewModel(
    [FromKeyedServices("local")] IStorageService local,
    [FromKeyedServices("cloud")] IStorageService cloud)
{
    // ...
}

Configuration

Bind configuration sections from appsettings.json or other sources:

Example.cs
builder.Configuration.AddJsonFile("appsettings.json");
builder.Services.Configure<ApiOptions>(
    builder.Configuration.GetSection("Api"));

What to Register as What

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.