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
的每个作用域生命周期
作用域工作单元(在MAUI中很少使用)
核心规则: 将页面和ViewModel注册为 Transient。将共享服务注册为 Singleton
⚠️ 除非手动管理
IServiceScope
,否则避免使用
AddScoped
MAUI没有像ASP.NET Core那样内置的请求作用域。没有显式作用域的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. 未手动管理作用域时使用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行为。除非显式管理作用域,否则请改用
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解析期间执行
  • AddScoped
    仅与手动创建的
    IServiceScope
    一起使用

References

参考资料