{
  "schema": "https://ai-atoms.com/schemas/skill-v1.json",
  "type": "skill",
  "id": "skill/maui-dependency-injection",
  "version": "1.0.1",
  "name": "maui-dependency-injection",
  "description": "Guidance for configuring dependency injection in .NET MAUI apps — service registration in MauiProgram.cs, lifetime selection (Singleton / Transient / Scoped), constructor injection, Shell navigation auto-resolution, platform-specific registrations, and testability patterns. USE FOR: \"dependency injection\", \"DI setup\", \"AddSingleton\", \"AddTransient\", \"AddScoped\", \"service registration\", \"constructor injection\", \"IServiceProvider\", \"MauiProgram DI\", \"register services\", \"BindingContext injection\". DO NOT USE FOR: data binding (use maui-data-binding), Shell route configuration (use maui-shell-navigation), unit-test mocking frameworks (use standard xUnit and NSubstitute patterns).",
  "system_prompt_fragment": "# Dependency Injection in .NET MAUI\n\n.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.\n\n## When to Use\n\n- Registering services, ViewModels, and Pages in `MauiProgram.cs`\n- Choosing between `AddSingleton`, `AddTransient`, and `AddScoped`\n- Wiring constructor injection for Pages and ViewModels\n- Leveraging Shell navigation to auto-resolve DI-registered Pages\n- Registering platform-specific service implementations with `#if` directives\n- Designing interfaces for testable service layers\n\n## When Not to Use\n\n- XAML data-binding syntax or compiled bindings — use the **maui-data-binding** skill\n- Shell route registration and query parameters — use the **maui-shell-navigation** skill\n- Mocking frameworks or test runners — use standard .NET testing tools (xUnit, NUnit, MSTest) and mocking libraries (NSubstitute, Moq)\n\n## Inputs\n\n- A .NET MAUI project with a `MauiProgram.cs` file\n- Knowledge of which services, ViewModels, and Pages need registration\n- Target platforms (Android, iOS, Mac Catalyst, Windows) for conditional registrations\n\n## Workflow\n\n1. Identify all services, ViewModels, and Pages that need to participate in dependency injection.\n2. Choose the correct lifetime for each type — `AddSingleton` for shared services, `AddTransient` for Pages and ViewModels.\n3. Register all types in `MauiProgram.CreateMauiApp()` on `builder.Services`, grouping by category (services, HTTP, ViewModels, Pages).\n4. Register Pages as Shell routes in `AppShell.xaml.cs` so Shell navigation auto-resolves the full dependency graph.\n5. Wire each Page to its ViewModel via constructor injection, assigning the ViewModel as `BindingContext`.\n6. Add platform-specific registrations with `#if` directives, ensuring every target platform is covered or has a fallback.\n7. Verify resolution works by running the app and confirming no `null` dependencies or missing-registration exceptions at runtime.\n\n---\n\n## Lifetime Selection\n\n| Lifetime | When to Use | Typical Types |\n|---|---|---|\n| `AddSingleton<T>()` | Shared state, expensive to create, app-wide config | `HttpClient` factory, settings service, database connection |\n| `AddTransient<T>()` | Lightweight, stateless, or needs a fresh instance per use | Pages, ViewModels, per-call API wrappers |\n| `AddScoped<T>()` | Per-scope lifetime with manually created `IServiceScope` | Scoped unit-of-work (rare in MAUI) |\n\n**Key rule:** Register Pages and ViewModels as **Transient**. Register shared services as **Singleton**.\n\n> ⚠️ **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.\n\n---\n\n## Registration Pattern in MauiProgram.cs\n\n```csharp\npublic static MauiApp CreateMauiApp()\n{\n    var builder = MauiApp.CreateBuilder();\n    builder.UseMauiApp<App>();\n\n    // Services — Singleton for shared state\n    builder.Services.AddSingleton<IDataService, DataService>();\n    builder.Services.AddSingleton<ISettingsService, SettingsService>();\n\n    // HTTP — use typed or named clients via IHttpClientFactory\n    // Requires NuGet: Microsoft.Extensions.Http\n    builder.Services.AddHttpClient<IApiClient, ApiClient>();\n\n    // ViewModels — Transient for fresh state per navigation\n    builder.Services.AddTransient<MainViewModel>();\n    builder.Services.AddTransient<DetailViewModel>();\n\n    // Pages — Transient so constructor injection fires each time\n    builder.Services.AddTransient<MainPage>();\n    builder.Services.AddTransient<DetailPage>();\n\n    return builder.Build();\n}\n```\n\n---\n\n## Constructor Injection\n\nInject dependencies through constructor parameters. The container resolves them automatically when the type is itself resolved from DI.\n\n```csharp\npublic class MainViewModel\n{\n    private readonly IDataService _dataService;\n\n    public MainViewModel(IDataService dataService)\n    {\n        _dataService = dataService;\n    }\n\n    public async Task LoadAsync() => Items = await _dataService.GetItemsAsync();\n}\n```\n\n### ViewModel → Page Wiring\n\nRegister both Page and ViewModel. Inject the ViewModel into the Page and assign it as `BindingContext`:\n\n```csharp\npublic partial class MainPage : ContentPage\n{\n    public MainPage(MainViewModel viewModel)\n    {\n        InitializeComponent();\n        BindingContext = viewModel;\n    }\n}\n```\n\n---\n\n## Shell Navigation Auto-Resolution\n\nWhen a Page is registered in DI **and** as a Shell route, Shell resolves it (and its full dependency graph) automatically on navigation:\n\n```csharp\n// MauiProgram.cs\nbuilder.Services.AddTransient<DetailPage>();\nbuilder.Services.AddTransient<DetailViewModel>();\n\n// AppShell.xaml.cs\nRouting.RegisterRoute(nameof(DetailPage), typeof(DetailPage));\n\n// Navigate — DI resolves DetailPage + DetailViewModel\nawait Shell.Current.GoToAsync(nameof(DetailPage));\n```\n\n---\n\n## Platform-Specific Registration\n\nUse preprocessor directives to register platform implementations. Always cover every target platform or provide a no-op fallback to avoid runtime `null`.\n\n```csharp\n#if ANDROID\nbuilder.Services.AddSingleton<INotificationService, AndroidNotificationService>();\n#elif IOS || MACCATALYST\nbuilder.Services.AddSingleton<INotificationService, AppleNotificationService>();\n#elif WINDOWS\nbuilder.Services.AddSingleton<INotificationService, WindowsNotificationService>();\n#else\nbuilder.Services.AddSingleton<INotificationService, NoOpNotificationService>();\n#endif\n```\n\n---\n\n## Explicit Resolution (Last Resort)\n\nPrefer constructor injection. Use explicit resolution only where injection is genuinely unavailable (custom handlers, platform callbacks):\n\n```csharp\n// From any Element with a Handler\nvar service = this.Handler.MauiContext.Services.GetService<IDataService>();\n```\n\nFor dynamic resolution, inject `IServiceProvider`:\n\n```csharp\npublic class NavigationService(IServiceProvider serviceProvider)\n{\n    public T ResolvePage<T>() where T : Page\n        => serviceProvider.GetRequiredService<T>();\n}\n```\n\n---\n\n## Interface-First Pattern for Testability\n\nDefine interfaces for every service so implementations can be swapped in tests:\n\n```csharp\npublic interface IDataService\n{\n    Task<List<Item>> GetItemsAsync();\n}\n\n// Production registration\nbuilder.Services.AddSingleton<IDataService, DataService>();\n\n// Test registration — swap without touching production code\nvar services = new ServiceCollection();\nservices.AddSingleton<IDataService, FakeDataService>();\n```\n\n---\n\n## Common Pitfalls\n\n### 1. Singleton ViewModels Cause Stale Data\n\n```csharp\n// ❌ ViewModel keeps stale state across navigations\nbuilder.Services.AddSingleton<DetailViewModel>();\n\n// ✅ Fresh instance each navigation\nbuilder.Services.AddTransient<DetailViewModel>();\n```\n\n### 2. Unregistered Page Silently Skips Injection\n\nIf 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.\n\n```csharp\n// ❌ Missing — injection silently skipped\n// builder.Services.AddTransient<DetailPage>();\n\n// ✅ Always register pages that need injection\nbuilder.Services.AddTransient<DetailPage>();\nbuilder.Services.AddTransient<DetailViewModel>();\n```\n\n### 3. XAML Resource Parsing vs. DI Timing\n\nXAML resources in `App.xaml` are parsed during `InitializeComponent()` — before the container is fully available. Defer service-dependent work to `CreateWindow()`:\n\n```csharp\npublic partial class App : Application\n{\n    private readonly IServiceProvider _services;\n\n    public App(IServiceProvider services)\n    {\n        _services = services;\n        InitializeComponent();\n    }\n\n    protected override Window CreateWindow(IActivationState? activationState)\n    {\n        // Safe — container is fully built\n        // Requires: builder.Services.AddTransient<AppShell>() in MauiProgram.cs\n        var appShell = _services.GetRequiredService<AppShell>();\n        return new Window(appShell);\n    }\n}\n```\n\n### 4. Service Locator Anti-Pattern\n\n```csharp\n// ❌ Hides dependencies, hard to test\nvar svc = this.Handler.MauiContext.Services.GetService<IDataService>();\n\n// ✅ Constructor injection — explicit and testable\npublic class MyViewModel(IDataService dataService) { }\n```\n\n### 5. Missing Platform in Conditional Registration\n\nForgetting a platform in `#if` blocks means `GetService<T>()` returns `null` at runtime on that platform. Always include an `#else` fallback or cover every target.\n\n### 6. AddScoped Without Manual Scope\n\n`AddScoped` in MAUI without creating `IServiceScope` manually gives Singleton behavior silently. Use `AddTransient` or `AddSingleton` instead unless you explicitly manage scopes.\n\n---\n\n## Checklist\n\n- [ ] Every Page and ViewModel that needs injection is registered in `MauiProgram.cs`\n- [ ] Pages and ViewModels use `AddTransient`; shared services use `AddSingleton`\n- [ ] Constructor injection used everywhere possible; service locator only as last resort\n- [ ] Interfaces defined for services that need test substitution\n- [ ] Platform-specific `#if` registrations cover all target platforms or include a fallback\n- [ ] Service-dependent work deferred to `CreateWindow()`, not run during XAML parse\n- [ ] `AddScoped` only used alongside manually created `IServiceScope`\n\n## References\n\n- [Dependency injection in .NET MAUI](https://learn.microsoft.com/dotnet/maui/fundamentals/dependency-injection)\n- [.NET dependency injection fundamentals](https://learn.microsoft.com/dotnet/core/extensions/dependency-injection)",
  "applicable_domains": [
    "code",
    "dotnet",
    "engineering"
  ],
  "invocation": [
    "/maui-dependency-injection"
  ],
  "tags": [
    "dotnet-maui",
    "dotnet",
    "csharp",
    "microsoft"
  ],
  "authored_by": "anthropics",
  "source_url": "https://github.com/dotnet/skills/blob/main/plugins/dotnet-maui/skills/maui-dependency-injection/SKILL.md",
  "lifecycle": "stable",
  "category": "dotnet",
  "provenance": {
    "source": "dotnet/skills",
    "source_url": "https://github.com/dotnet/skills/blob/main/plugins/dotnet-maui/skills/maui-dependency-injection/SKILL.md",
    "author": "Microsoft / .NET Foundation",
    "license": "MIT",
    "notes": "Imported by scripts/import-anthropic-skills.py."
  }
}