maui-dependency-injection について
.NET MAUI 開発者向けに、サービス登録やライフタイムからコンストラクターインジェクション、Shell ナビゲーションまでを案内するスキルです。プラットフォーム別登録や解決時の注意点も扱います。
主な機能
- 01MauiProgram でサービス、ページ、ViewModel を登録
- 02Singleton、Transient、Scoped の選択
- 03コンストラクターによる依存性注入
- 04Shell ルート経由でページを解決
- 05対象プラットフォームごとの実装登録
活用シーン
- 01MAUI ページと ViewModel の注入設定
- 02共有サービスのライフタイム選択
- 03Shell ナビゲーションで依存ページを生成
- 04プラットフォーム別サービス実装の登録
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ dotnet/ skills --skill maui-dependency-injectionインストール後、Claude Code で/maui-dependency-injectionを入力して読み込まれたか確認します。
同梱ファイルreferences/dependency-injection-api.md
詳細
.NET MAUI の依存性注入
.NET MAUI は、ASP.NET Core と同じ Microsoft.Extensions.DependencyInjection コンテナーを使用します。すべてのサービス登録は、builder.Services を使って MauiProgram.CreateMauiApp() で行います。コンテナーは起動時に一度だけ構築され、その後は変更できません。
使用する場面
MauiProgram.csでサービス、ViewModel、Page を登録するAddSingleton、AddTransient、AddScopedのいずれを使うか選ぶ- Page と ViewModel のコンストラクターインジェクションを設定する
- Shell ナビゲーションを活用して、DI に登録された Page を自動解決する
#ifディレクティブを使って、プラットフォーム固有のサービス実装を登録する- テスト可能なサービスレイヤーのためのインターフェイスを設計する
使用しない場面
- XAML のデータバインディング構文やコンパイル済みバインディング — maui-data-binding スキルを使用してください
- Shell のルート登録やクエリパラメーター — maui-shell-navigation スキルを使用してください
- モックフレームワークやテストランナー — 標準の .NET テストツール(xUnit、NUnit、MSTest)とモックライブラリ(NSubstitute、Moq)を使用してください
入力
MauiProgram.csファイルを含む .NET MAUI プロジェクト- 登録が必要なサービス、ViewModel、Page に関する知識
- 条件付き登録の対象となるプラットフォーム(Android、iOS、Mac Catalyst、Windows)
回答を左右するルール
| 状況 | 対応 | 理由 |
|---|---|---|
| Page または ViewModel を登録する | AddTransient を優先する | ナビゲーションごとに新しいインスタンスを使うことで古い状態を避けられます。また、Singleton の Page はビジュアルツリーから削除された後、再追加できません。本当に単一インスタンスにする Page(例:状態を維持したいルートタブ)であれば、Singleton も妥当です |
| 共有状態や生成コストの高い状態を登録する | AddSingleton を使う | アプリ全体でインスタンスを1つだけ使います(設定、DB 接続、HttpClient ハンドラーなど) |
AddScoped を使いたくなった場合 | AddTransient を使います(共有が意図された動作なら AddSingleton を使います) | MAUI には、ASP.NET Core の HTTP パイプラインのような組み込みのリクエストスコープはありません。MAUI はウィンドウごとに1つの IServiceScope を作成するため、Scoped サービスはそのウィンドウが存続する間有効です。また、ルートプロバイダーから解決すると Singleton のように動作します。どちらもナビゲーションごとに新しいインスタンスを提供するものではありません |
| DI に登録された Page に移動する | Page とその ViewModel を登録し、Routing.RegisterRoute を呼び出す | Shell.Current.GoToAsync は DI を通じて Page を解決し、コンストラクターの依存関係を注入します |
| プラットフォーム固有の実装 | すべてのプラットフォームを網羅したうえで、プラットフォームごとに #if を使う | いずれかのプラットフォームの分岐が欠けていると、そのサービスは登録されず、解決時に例外がスローされます |
DI を使っていないプロジェクトに DI を導入したり、問題なく動作しているサービスのライフタイムを変更したり、単に統一感を持たせるためだけにインターフェイスを追加したりしないでください。ユーザーから依頼された場合、または実際の不具合を解決する場合に限ってください。
回答は要点を絞りつつ、必要な内容を漏らさないでください。 ライフタイムの変更を提案する場合は、登録コードを示し、現実的な選択肢を1つだけでなく提示してください。ユニットオブワークや DbContext に関する質問であれば、AddTransient、明示的な IServiceScopeFactory.CreateScope()、そしてファクトリーパターン(AddDbContextFactory)を取り上げ、それぞれが適している場面について説明してください。通常、1行で決めつけるよりも、トレードオフを添えた簡潔な選択肢の提示のほうが適切です。
// Explicit scope when you genuinely need unit-of-work semantics
using var scope = scopeFactory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<MyDbContext>();
ワークフロー
- 依存性注入に参加させる必要があるすべてのサービス、ViewModel、ページを特定します。
- 各型に適切なライフタイムを選択します。共有サービスには
AddSingleton、ページとViewModelにはAddTransientを使用します。 - すべての型を
MauiProgram.CreateMauiApp()のbuilder.Servicesに登録し、カテゴリ(サービス、HTTP、ViewModel、ページ)ごとにまとめます。 AppShell.xaml.csでページを Shell ルートとして登録し、Shell ナビゲーションで依存関係グラフ全体が自動的に解決されるようにします。- コンストラクターインジェクションを使って各ページを対応するViewModelに接続し、ViewModelを
BindingContextに設定します。 #ifディレクティブを使ってプラットフォーム固有の登録を追加し、すべての対象プラットフォームを網羅するか、フォールバックを用意します。- アプリを実行し、実行時に依存関係が
nullにならないこと、登録漏れの例外が発生しないことを確認して、解決が機能することを検証します。
ライフタイムの選択
| ライフタイム | 使用する場面 | 代表的な型 |
|---|---|---|
AddSingleton<T>() | 状態を共有する場合、生成コストが高い場合、アプリ全体の設定 | HttpClient ファクトリー、設定サービス、データベース接続 |
AddTransient<T>() | 軽量で状態を持たない場合、または使用ごとに新しいインスタンスが必要な場合 | ページ、ViewModel、呼び出しごとのAPIラッパー |
AddScoped<T>() | ウィンドウ単位のライフタイム、または手動で作成した IServiceScope | スコープ付きユニット・オブ・ワーク(MAUIではまれ) |
重要なルール: ページとViewModelは、デフォルトで Transient として登録します。共有サービスは Singleton として登録します。
⚠️
IServiceScopeを手動で管理しない限り、AddScopedの使用は避けてください。 MAUIにはASP.NET Coreのような組み込みのリクエストスコープがありません。MAUIはウィンドウごとに1つのIServiceScopeを作成するため、スコープ付きサービスはそのウィンドウが存続する間有効です。ルートプロバイダーから解決すると、暗黙的にSingletonとして動作します。どちらの場合も、ナビゲーションごとに新しいインスタンスが作られるわけではありません。
MauiProgram.csでの登録パターン
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();
}
コンストラクターインジェクション
コンストラクターパラメーターを通じて依存関係を注入します。その型自体がDIから解決されるとき、コンテナーが依存関係を自動的に解決します。
public class MainViewModel
{
private readonly IDataService _dataService;
public MainViewModel(IDataService dataService)
{
_dataService = dataService;
}
public async Task LoadAsync() => Items = await _dataService.GetItemsAsync();
}
ViewModelからページへの接続
ページとViewModelの両方を登録します。ViewModelをページに注入し、BindingContext に設定します。
public partial class MainPage : ContentPage
{
public MainPage(MainViewModel viewModel)
{
InitializeComponent();
BindingContext = viewModel;
}
}
Shellナビゲーションによる自動解決
ページがDIに かつ Shellルートとして登録されている場合、ナビゲーション時にShellがページ(および依存関係グラフ全体)を自動的に解決します。
// 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で解決されるViewModelへのパラメーターの受け渡し
DIが提供するのはViewModelの 依存関係 です。ナビゲーションパラメーターは別途渡されます。
コンストラクターを通じて注入しようとせず、ViewModelに IQueryAttributable
を実装して、両方を受け取るようにします。
public class DetailViewModel : ObservableObject, IQueryAttributable
{
readonly IDataService _data; // ← injected by DI
public DetailViewModel(IDataService data) => _data = data;
public void ApplyQueryAttributes(IDictionary<string, object> query)
{
// ← supplied by navigation
if (query.TryGetValue("id", out var id))
LoadAsync(id.ToString()!);
}
}
// Navigate with a parameter — the page and its ViewModel still come from DI
await Shell.Current.GoToAsync($"{nameof(DetailPage)}?id={product.Id}");
Shellはページ および その BindingContext にクエリー属性を適用するため、
ページで特別な接続処理をしなくてもViewModelがパラメーターを受け取れます。
プラットフォーム固有の登録
プリプロセッサディレクティブを使って、プラットフォームごとの実装を登録します。実行時に null になるのを避けるため、すべての対象プラットフォームを必ず網羅するか、何もしないフォールバックを用意します。
#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
明示的な解決(最後の手段)
コンストラクターインジェクションを優先してください。明示的な解決は、カスタムハンドラーやプラットフォームコールバックなど、注入を本当に利用できない場合に限って使用します。
// From any Element with a Handler
var service = this.Handler.MauiContext.Services.GetService<IDataService>();
動的に解決する場合は、IServiceProvider を注入します。
public class NavigationService(IServiceProvider serviceProvider)
{
public T ResolvePage<T>() where T : Page
=> serviceProvider.GetRequiredService<T>();
}
テスト容易性を高めるインターフェース優先のパターン
テストで実装を差し替えられるよう、すべてのサービスにインターフェースを定義します。
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>();
よくある落とし穴
1. SingletonのViewModelは古いデータを引き起こします
// ❌ ViewModel keeps stale state across navigations
builder.Services.AddSingleton<DetailViewModel>();
// ✅ Fresh instance each navigation
builder.Services.AddTransient<DetailViewModel>();
2. ContentTemplate ページは DI 経由では作成されません
Shell XAML で <ShellContent ContentTemplate="{DataTemplate views:DetailPage}"> として宣言されたページは、サービスプロバイダー経由ではなく、Activator.CreateInstance(ElementTemplate.cs)でインスタンス化されます。この経路ではコンストラクターインジェクションは実行されません。ページのコンストラクターが依存関係を受け取るものだけの場合、依存関係が黙って null になるのではなく、MissingMethodException が発生します。
Routing.RegisterRoute と GoToAsync 経由で遷移するページは異なります。ActivatorUtilities.GetServiceOrCreateInstance(Routing.cs)を経由するため、ページの型自体が登録されていなくても、登録済みの依存関係が注入されます。また、必須の依存関係を解決できない場合は例外が発生します。
// Registering the page and its dependencies keeps both paths working
builder.Services.AddTransient<DetailPage>();
builder.Services.AddTransient<DetailViewModel>();
タブやフライアウトのページで DI が必要な場合は、必要なものを解決する引数なしコンストラクターを用意するか、ページを ContentTemplate に埋め込むのではなく、ルート経由で遷移してください。
3. XAML リソースの解析と DI のタイミング
App.xaml 内の XAML リソースは、コンテナーが完全に利用可能になる前に InitializeComponent() 中に解析されます。サービスに依存する処理は CreateWindow() まで遅らせてください。
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);
}
}
4. サービスロケーターのアンチパターン
// ❌ Hides dependencies, hard to test
var svc = this.Handler.MauiContext.Services.GetService<IDataService>();
// ✅ Constructor injection — explicit and testable
public class MyViewModel(IDataService dataService) { }
5. 条件付き登録でのプラットフォーム漏れ
#if ブロックでプラットフォームを指定し忘れると、そのプラットフォームでは実行時に GetService<T>() が null を返します。必ず #else のフォールバックを含めるか、すべてのターゲットを対象にしてください。
6. 手動でスコープを作成しない AddScoped
上のルール表を参照してください。AddScoped はウィンドウ単位の有効期間、または Singleton と同様の動作になり、ナビゲーションごとに新しいインスタンスが作られることはありません。IServiceScope を明示的に作成して管理する場合を除き、AddTransient または AddSingleton を使用してください。
チェックリスト
- 注入が必要なすべての Page と ViewModel が
MauiProgram.csに登録されている - Page と ViewModel には
AddTransient、共有サービスにはAddSingletonを使用している - 可能な限りすべての箇所でコンストラクターインジェクションを使用し、サービスロケーターは最後の手段としてのみ使用している
- テスト時に差し替える必要があるサービスにはインターフェイスを定義している
- プラットフォーム固有の
#if登録がすべてのターゲットプラットフォームを対象にしているか、フォールバックを含んでいる - サービスに依存する処理を XAML の解析中に実行せず、
CreateWindow()まで遅らせている -
AddScopedはウィンドウ単位の有効期間を意図する場合にのみ使用しているか、手動で作成したIServiceScopeと併用している