Dependency Injection in .NET MAUI
MAUI ships with the same Microsoft.Extensions.DependencyInjection container used by ASP.NET Core, exposed through builder.Services in MauiProgram.cs. Rather than a page instantiating its own HttpClient or SQLiteAsyncConnection, it declares what it needs in its constructor and the container supplies a ready-made instance — this keeps classes focused on their own logic, makes unit testing possible by swapping in fakes, and centralizes lifetime decisions (singleton vs transient) in one place instead of scattering new calls throughout the codebase.
Cricket analogy: It's like a franchise cricket team not scouting and signing every net bowler itself for every session — a central team-ops staff supplies the resources a squad needs, exactly as the DI container supplies dependencies to a class instead of it self-provisioning.
Registering Services in MauiProgram.cs
Registration happens once, at startup, with three lifetimes: AddSingleton creates one instance for the app's entire lifetime (good for a database connection or an auth-token store), AddTransient creates a new instance every time it's requested (good for lightweight, stateless services or view models bound one-per-page), and AddScoped behaves like transient in most MAUI apps since there's no per-request scope the way ASP.NET Core has one per HTTP request — it's mostly relevant if you're sharing code with a Blazor Hybrid component tree that does define scopes.
Cricket analogy: It's like deciding whether a team keeps one permanent head coach for the whole season (singleton) versus bringing in a fresh specialist net-bowler for each individual session (transient) — the lifetime you pick depends on whether state should persist or reset.
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder.UseMauiApp<App>();
// Singleton: one instance for the app's lifetime
builder.Services.AddSingleton<DatabaseService>();
builder.Services.AddSingleton<IAuthTokenStore, AuthTokenStore>();
// Transient: a new instance every time it's requested
builder.Services.AddTransient<ProductsViewModel>();
builder.Services.AddTransient<ProductsPage>();
return builder.Build();
}
}
// Page constructor receives its view model via DI
public partial class ProductsPage : ContentPage
{
public ProductsPage(ProductsViewModel viewModel)
{
InitializeComponent();
BindingContext = viewModel;
}
}Constructor Injection in Pages and ViewModels
For DI to hand a Page its view model automatically, the page itself must be resolved through the container rather than constructed with new — which means navigation needs to go through IServiceProvider or, in Shell apps, through registered routes (Routing.RegisterRoute) so Shell's own navigation resolves pages via DI under the hood. A common bug is a page built with a parameterless constructor in XAML's implicit style while also declaring a constructor that expects a view model — only one path can be active, and mixing them causes MissingMethodException at navigation time.
Cricket analogy: It's like a cricket board making sure every replacement player is called up through the official selection panel rather than a captain grabbing a random net bowler off the street — pages must be resolved through the container, not hand-built with new.
In a Shell app, register routes with Routing.RegisterRoute<ProductDetailPage, ProductDetailViewModel>(nameof(ProductDetailPage)) so GoToAsync resolves the page (and its constructor-injected view model) through the DI container automatically.
If a XAML page has both an implicit parameterless constructor and a DI constructor taking a view model, only register and navigate to it via the container — instantiating it directly elsewhere (e.g. new ProductsPage()) will throw because the required constructor argument won't be supplied.
- MAUI uses the same Microsoft.Extensions.DependencyInjection container as ASP.NET Core, configured in MauiProgram.cs via builder.Services.
- AddSingleton creates one instance for the app's lifetime; AddTransient creates a new instance on every resolution.
- AddScoped behaves like Transient in most MAUI apps since there's no per-request scope, but matters in Blazor Hybrid component trees.
- Pages and view models should both be registered so constructor injection wires a page's BindingContext automatically.
- In Shell apps, register navigable pages with Routing.RegisterRoute so GoToAsync resolves them (and their view models) via DI.
- Never mix a parameterless constructor with a DI constructor on the same page — resolve consistently through the container.
- Centralizing service lifetimes in MauiProgram.cs makes classes testable by swapping in fakes instead of hardcoding concrete dependencies.
Practice what you learned
1. Which lifetime should typically be used for a shared SQLiteAsyncConnection wrapper in a MAUI app?
2. What is the effect of registering a view model with AddTransient?
3. What must a Shell-based MAUI app do so that GoToAsync resolves a page's constructor-injected view model correctly?
4. What commonly causes a MissingMethodException when navigating to a MAUI page?
Was this page helpful?
You May Also Like
Consuming REST APIs
Learn how to call web APIs from a .NET MAUI app using IHttpClientFactory, System.Text.Json, and connectivity-aware error handling.
Local Storage with SQLite
Persist structured data on-device in a MAUI app using sqlite-net-pcl, async CRUD operations, and simple schema migrations.
MAUI Blazor Hybrid
Learn how BlazorWebView embeds Razor components inside a native MAUI app, sharing UI code between web and native targets.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics