maui-dependency-injection

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Dependency Injection in .NET MAUI

.NET MAUI中的依赖注入

.NET MAUI uses the same
Microsoft.Extensions.DependencyInjection
container as ASP.NET Core. All service registration happens in
MauiProgram.CreateMauiApp()
on
builder.Services
. The container is built once at startup and is immutable thereafter.
.NET MAUI使用和ASP.NET Core相同的
Microsoft.Extensions.DependencyInjection
容器。所有服务注册都在
MauiProgram.CreateMauiApp()
方法的
builder.Services
对象上完成。容器会在应用启动时构建一次,之后处于不可修改状态。

When to Use

适用场景

  • Registering services, ViewModels, and Pages in
    MauiProgram.cs
  • Choosing between
    AddSingleton
    ,
    AddTransient
    , and
    AddScoped
  • Wiring constructor injection for Pages and ViewModels
  • Leveraging Shell navigation to auto-resolve DI-registered Pages
  • Registering platform-specific service implementations with
    #if
    directives
  • Designing interfaces for testable service layers
  • MauiProgram.cs
    中注册服务、ViewModel和页面
  • 为服务选择
    AddSingleton
    AddTransient
    AddScoped
    生命周期
  • 为页面和ViewModel配置构造函数注入
  • 利用Shell导航自动解析已在DI中注册的页面
  • 通过
    #if
    指令注册平台专属的服务实现
  • 为可测试的服务层设计接口

When Not to Use

不适用场景

  • XAML data-binding syntax or compiled bindings — use the maui-data-binding skill
  • Shell route registration and query parameters — use the maui-shell-navigation skill
  • Mocking frameworks or test runners — use standard .NET testing tools (xUnit, NUnit, MSTest) and mocking libraries (NSubstitute, Moq)
  • XAML数据绑定语法或编译绑定——请使用maui-data-binding技能
  • Shell路由注册和查询参数——请使用maui-shell-navigation技能
  • 模拟框架或测试运行器——请使用标准.NET测试工具(xUnit、NUnit、MSTest)和模拟库(NSubstitute、Moq)

Inputs

前置输入

  • A .NET MAUI project with a
    MauiProgram.cs
    file
  • Knowledge of which services, ViewModels, and Pages need registration
  • Target platforms (Android, iOS, Mac Catalyst, Windows) for conditional registrations
  • 包含
    MauiProgram.cs
    文件的.NET MAUI项目
  • 明确需要注册的服务、ViewModel和页面清单
  • 条件注册对应的目标平台(Android、iOS、Mac Catalyst、Windows)

Workflow

操作流程

  1. Identify all services, ViewModels, and Pages that need to participate in dependency injection.
  2. Choose the correct lifetime for each type —
    AddSingleton
    for shared services,
    AddTransient
    for Pages and ViewModels.
  3. Register all types in
    MauiProgram.CreateMauiApp()
    on
    builder.Services
    , grouping by category (services, HTTP, ViewModels, Pages).
  4. Register Pages as Shell routes in
    AppShell.xaml.cs
    so Shell navigation auto-resolves the full dependency graph.
  5. Wire each Page to its ViewModel via constructor injection, assigning the ViewModel as
    BindingContext
    .
  6. Add platform-specific registrations with
    #if
    directives, ensuring every target platform is covered or has a fallback.
  7. Verify resolution works by running the app and confirming no
    null
    dependencies or missing-registration exceptions at runtime.

  1. 梳理所有需要接入依赖注入的服务、ViewModel和页面。
  2. 为每个类型选择正确的生命周期——共享服务使用
    AddSingleton
    ,页面和ViewModel使用
    AddTransient
  3. MauiProgram.CreateMauiApp()
    builder.Services
    上注册所有类型,按类别(服务、HTTP、ViewModel、页面)分组管理。
  4. AppShell.xaml.cs
    中将页面注册为Shell路由,这样Shell导航可以自动解析完整的依赖图。
  5. 通过构造函数注入将页面和对应的ViewModel关联,将ViewModel赋值为页面的
    BindingContext
  6. 使用
    #if
    指令添加平台专属注册,确保覆盖所有目标平台或提供降级实现。
  7. 运行应用验证依赖解析正常,确认运行时无
    null
    依赖或注册缺失异常。

Lifetime Selection

生命周期选择

LifetimeWhen to UseTypical Types
AddSingleton<T>()
Shared state, expensive to create, app-wide config
HttpClient
factory, settings service, database connection
AddTransient<T>()
Lightweight, stateless, or needs a fresh instance per usePages, ViewModels, per-call API wrappers
AddScoped<T>()
Per-scope lifetime with manually created
IServiceScope
Scoped unit-of-work (rare in MAUI)
Key rule: Register Pages and ViewModels as Transient. Register shared services as Singleton.
⚠️ Avoid
AddScoped
unless you manually manage
IServiceScope
.
MAUI has no built-in request scope like ASP.NET Core. A Scoped registration without an explicit scope silently behaves as a Singleton, leading to subtle bugs.

生命周期适用场景典型类型
AddSingleton<T>()
共享状态、创建成本高、应用全局配置
HttpClient
工厂、设置服务、数据库连接
AddTransient<T>()
轻量、无状态、每次使用都需要全新实例页面、ViewModel、单次调用的API封装
AddScoped<T>()
配合手动创建的
IServiceScope
实现按 scope 生命周期
按scope的工作单元(MAUI中很少使用)
核心规则: 页面和ViewModel注册为Transient,共享服务注册为Singleton
⚠️ 除非手动管理
IServiceScope
,否则避免使用
AddScoped
MAUI没有ASP.NET Core内置的请求scope,没有显式scope的Scoped注册会静默表现为Singleton,引发难以定位的bug。

Registration Pattern in MauiProgram.cs

MauiProgram.cs中的注册模式

csharp
public static MauiApp CreateMauiApp()
{
    var builder = MauiApp.CreateBuilder();
    builder.UseMauiApp<App>();

    // Services — Singleton for shared state
    builder.Services.AddSingleton<IDataService, DataService>();
    builder.Services.AddSingleton<ISettingsService, SettingsService>();

    // HTTP — use typed or named clients via IHttpClientFactory
    // Requires NuGet: Microsoft.Extensions.Http
    builder.Services.AddHttpClient<IApiClient, ApiClient>();

    // ViewModels — Transient for fresh state per navigation
    builder.Services.AddTransient<MainViewModel>();
    builder.Services.AddTransient<DetailViewModel>();

    // Pages — Transient so constructor injection fires each time
    builder.Services.AddTransient<MainPage>();
    builder.Services.AddTransient<DetailPage>();

    return builder.Build();
}

csharp
public static MauiApp CreateMauiApp()
{
    var builder = MauiApp.CreateBuilder();
    builder.UseMauiApp<App>();

    // 服务 —— 共享状态用Singleton
    builder.Services.AddSingleton<IDataService, DataService>();
    builder.Services.AddSingleton<ISettingsService, SettingsService>();

    // HTTP —— 通过IHttpClientFactory使用类型化或命名客户端
    // 需要安装NuGet包:Microsoft.Extensions.Http
    builder.Services.AddHttpClient<IApiClient, ApiClient>();

    // ViewModel —— 每次导航使用全新状态用Transient
    builder.Services.AddTransient<MainViewModel>();
    builder.Services.AddTransient<DetailViewModel>();

    // 页面 —— Transient确保每次触发构造函数注入
    builder.Services.AddTransient<MainPage>();
    builder.Services.AddTransient<DetailPage>();

    return builder.Build();
}

Constructor Injection

构造函数注入

Inject dependencies through constructor parameters. The container resolves them automatically when the type is itself resolved from DI.
csharp
public class MainViewModel
{
    private readonly IDataService _dataService;

    public MainViewModel(IDataService dataService)
    {
        _dataService = dataService;
    }

    public async Task LoadAsync() => Items = await _dataService.GetItemsAsync();
}
通过构造函数参数注入依赖,当类型本身从DI中解析时,容器会自动完成依赖的解析。
csharp
public class MainViewModel
{
    private readonly IDataService _dataService;

    public MainViewModel(IDataService dataService)
    {
        _dataService = dataService;
    }

    public async Task LoadAsync() => Items = await _dataService.GetItemsAsync();
}

ViewModel → Page Wiring

ViewModel与页面关联

Register both Page and ViewModel. Inject the ViewModel into the Page and assign it as
BindingContext
:
csharp
public partial class MainPage : ContentPage
{
    public MainPage(MainViewModel viewModel)
    {
        InitializeComponent();
        BindingContext = viewModel;
    }
}

同时注册页面和ViewModel,将ViewModel注入到页面中并赋值为
BindingContext
csharp
public partial class MainPage : ContentPage
{
    public MainPage(MainViewModel viewModel)
    {
        InitializeComponent();
        BindingContext = viewModel;
    }
}

Shell Navigation Auto-Resolution

Shell导航自动解析

When a Page is registered in DI and as a Shell route, Shell resolves it (and its full dependency graph) automatically on navigation:
csharp
// MauiProgram.cs
builder.Services.AddTransient<DetailPage>();
builder.Services.AddTransient<DetailViewModel>();

// AppShell.xaml.cs
Routing.RegisterRoute(nameof(DetailPage), typeof(DetailPage));

// Navigate — DI resolves DetailPage + DetailViewModel
await Shell.Current.GoToAsync(nameof(DetailPage));

当页面同时在DI和Shell路由中注册时,Shell会在导航时自动解析页面及其完整依赖图:
csharp
// MauiProgram.cs
builder.Services.AddTransient<DetailPage>();
builder.Services.AddTransient<DetailViewModel>();

// AppShell.xaml.cs
Routing.RegisterRoute(nameof(DetailPage), typeof(DetailPage));

// 执行导航 —— DI自动解析DetailPage + DetailViewModel
await Shell.Current.GoToAsync(nameof(DetailPage));

Platform-Specific Registration

平台专属注册

Use preprocessor directives to register platform implementations. Always cover every target platform or provide a no-op fallback to avoid runtime
null
.
csharp
#if ANDROID
builder.Services.AddSingleton<INotificationService, AndroidNotificationService>();
#elif IOS || MACCATALYST
builder.Services.AddSingleton<INotificationService, AppleNotificationService>();
#elif WINDOWS
builder.Services.AddSingleton<INotificationService, WindowsNotificationService>();
#else
builder.Services.AddSingleton<INotificationService, NoOpNotificationService>();
#endif

使用预处理指令注册平台特定的实现,务必覆盖所有目标平台或提供空实现降级,避免运行时出现
null
csharp
#if ANDROID
builder.Services.AddSingleton<INotificationService, AndroidNotificationService>();
#elif IOS || MACCATALYST
builder.Services.AddSingleton<INotificationService, AppleNotificationService>();
#elif WINDOWS
builder.Services.AddSingleton<INotificationService, WindowsNotificationService>();
#else
builder.Services.AddSingleton<INotificationService, NoOpNotificationService>();
#endif

Explicit Resolution (Last Resort)

显式解析(万不得已时使用)

Prefer constructor injection. Use explicit resolution only where injection is genuinely unavailable (custom handlers, platform callbacks):
csharp
// From any Element with a Handler
var service = this.Handler.MauiContext.Services.GetService<IDataService>();
For dynamic resolution, inject
IServiceProvider
:
csharp
public class NavigationService(IServiceProvider serviceProvider)
{
    public T ResolvePage<T>() where T : Page
        => serviceProvider.GetRequiredService<T>();
}

优先使用构造函数注入,仅在确实无法使用注入的场景(自定义处理器、平台回调)中使用显式解析:
csharp
// 从任意带Handler的Element中获取服务
var service = this.Handler.MauiContext.Services.GetService<IDataService>();
如果需要动态解析,可以注入
IServiceProvider
csharp
public class NavigationService(IServiceProvider serviceProvider)
{
    public T ResolvePage<T>() where T : Page
        => serviceProvider.GetRequiredService<T>();
}

Interface-First Pattern for Testability

面向接口的可测试性模式

Define interfaces for every service so implementations can be swapped in tests:
csharp
public interface IDataService
{
    Task<List<Item>> GetItemsAsync();
}

// Production registration
builder.Services.AddSingleton<IDataService, DataService>();

// Test registration — swap without touching production code
var services = new ServiceCollection();
services.AddSingleton<IDataService, FakeDataService>();

为每个服务定义接口,这样测试时可以替换实现:
csharp
public interface IDataService
{
    Task<List<Item>> GetItemsAsync();
}

// 生产环境注册
builder.Services.AddSingleton<IDataService, DataService>();

// 测试环境注册 —— 无需修改生产代码即可替换实现
var services = new ServiceCollection();
services.AddSingleton<IDataService, FakeDataService>();

Common Pitfalls

常见陷阱

1. Singleton ViewModels Cause Stale Data

1. Singleton类型的ViewModel导致数据过期

csharp
// ❌ ViewModel keeps stale state across navigations
builder.Services.AddSingleton<DetailViewModel>();

// ✅ Fresh instance each navigation
builder.Services.AddTransient<DetailViewModel>();
csharp
// ❌ ViewModel会在多次导航间保留过期状态
builder.Services.AddSingleton<DetailViewModel>();

// ✅ 每次导航生成全新实例
builder.Services.AddTransient<DetailViewModel>();

2. Unregistered Page Silently Skips Injection

2. 未注册的页面会静默跳过注入

If a Page appears in Shell XAML via
<ShellContent ContentTemplate="...">
but is not registered in
builder.Services
, MAUI creates it with the parameterless constructor. Dependencies are silently
null
— no exception is thrown.
csharp
// ❌ Missing — injection silently skipped
// builder.Services.AddTransient<DetailPage>();

// ✅ Always register pages that need injection
builder.Services.AddTransient<DetailPage>();
builder.Services.AddTransient<DetailViewModel>();
如果页面通过
<ShellContent ContentTemplate="...">
在Shell XAML中声明,但没有
builder.Services
中注册,MAUI会使用无参构造函数创建实例,依赖会静默为
null
,不会抛出异常。
csharp
// ❌ 缺失注册 —— 注入会被静默跳过
// builder.Services.AddTransient<DetailPage>();

// ✅ 需要注入的页面必须注册
builder.Services.AddTransient<DetailPage>();
builder.Services.AddTransient<DetailViewModel>();

3. XAML Resource Parsing vs. DI Timing

3. XAML资源解析与DI的时序冲突

XAML resources in
App.xaml
are parsed during
InitializeComponent()
— before the container is fully available. Defer service-dependent work to
CreateWindow()
:
csharp
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
        // Requires: builder.Services.AddTransient<AppShell>() in MauiProgram.cs
        var appShell = _services.GetRequiredService<AppShell>();
        return new Window(appShell);
    }
}
App.xaml
中的XAML资源会在
InitializeComponent()
执行期间解析,此时容器还未完全就绪。需要依赖服务的逻辑请延迟到
CreateWindow()
中执行:
csharp
public partial class App : Application
{
    private readonly IServiceProvider _services;

    public App(IServiceProvider services)
    {
        _services = services;
        InitializeComponent();
    }

    protected override Window CreateWindow(IActivationState? activationState)
    {
        // 此时容器已完全构建,可以安全调用
        // 需要提前在MauiProgram.cs中添加:builder.Services.AddTransient<AppShell>()
        var appShell = _services.GetRequiredService<AppShell>();
        return new Window(appShell);
    }
}

4. Service Locator Anti-Pattern

4. 服务定位器反模式

csharp
// ❌ Hides dependencies, hard to test
var svc = this.Handler.MauiContext.Services.GetService<IDataService>();

// ✅ Constructor injection — explicit and testable
public class MyViewModel(IDataService dataService) { }
csharp
// ❌ 隐藏依赖,难以测试
var svc = this.Handler.MauiContext.Services.GetService<IDataService>();

// ✅ 构造函数注入 —— 显式且易测试
public class MyViewModel(IDataService dataService) { }

5. Missing Platform in Conditional Registration

5. 条件注册遗漏平台

Forgetting a platform in
#if
blocks means
GetService<T>()
returns
null
at runtime on that platform. Always include an
#else
fallback or cover every target.
#if
代码块中遗漏平台会导致对应平台上
GetService<T>()
运行时返回
null
,务必添加
#else
降级逻辑或覆盖所有目标平台。

6. AddScoped Without Manual Scope

6. 未手动管理scope却使用AddScoped

AddScoped
in MAUI without creating
IServiceScope
manually gives Singleton behavior silently. Use
AddTransient
or
AddSingleton
instead unless you explicitly manage scopes.

MAUI中未手动创建
IServiceScope
AddScoped
注册会静默表现为Singleton行为,除非明确管理scope,否则请使用
AddTransient
AddSingleton

Checklist

检查清单

  • Every Page and ViewModel that needs injection is registered in
    MauiProgram.cs
  • Pages and ViewModels use
    AddTransient
    ; shared services use
    AddSingleton
  • Constructor injection used everywhere possible; service locator only as last resort
  • Interfaces defined for services that need test substitution
  • Platform-specific
    #if
    registrations cover all target platforms or include a fallback
  • Service-dependent work deferred to
    CreateWindow()
    , not run during XAML parse
  • AddScoped
    only used alongside manually created
    IServiceScope
  • 所有需要注入的页面和ViewModel都已在
    MauiProgram.cs
    中注册
  • 页面和ViewModel使用
    AddTransient
    注册;共享服务使用
    AddSingleton
    注册
  • 尽可能使用构造函数注入,仅万不得已时使用服务定位器
  • 需要测试替换的服务都已定义接口
  • 平台专属
    #if
    注册覆盖了所有目标平台或包含降级实现
  • 依赖服务的逻辑延迟到
    CreateWindow()
    中执行,不在XAML解析阶段运行
  • 仅在搭配手动创建的
    IServiceScope
    时使用
    AddScoped

References

参考资料