Skip to main content
skill CX Codex

maui-dependency-injection — Claude Code skill: DI setup guide

Guides dependency injection setup in .NET MAUI projects. It configures service lifetimes, constructor injection, and platform-specific registrations in MauiProgram.cs.

Install into Claude Code · Skills CLI (Vercel) All methods and platforms ↓
npx skills add dotnet/skills --skill maui-dependency-injection -a claude-code
Open on GitHub

What maui-dependency-injection does

The skill instructs the agent on managing Microsoft.Extensions.DependencyInjection inside MauiProgram.CreateMauiApp(). It enforces clear lifetime boundaries: AddTransient for Pages and ViewModels to guarantee fresh state on navigation, and AddSingleton for shared resources like database connections or HTTP handlers. The container builds once during startup and remains immutable.

It actively warns against reaching for AddScoped. Because .NET MAUI lacks ASP.NET Core request pipelines, scoped services default to a single window scope or behave like singletons from the root container. If transactional operations or isolated database contexts are needed, the skill advises explicit calls to IServiceScopeFactory.CreateScope() or AddDbContextFactory.

Omitting a target platform inside conditional #if blocks causes missing-registration crashes at runtime. The skill specifically keeps focus on dependency registration; data-binding markup and route declarations are delegated to separate specialized skills.

What it helps with

  • register ViewModels and Pages in MauiProgram.CreateMauiApp()
  • pick between AddTransient and AddSingleton for app components
  • inject service dependencies via Page and ViewModel constructors
  • resolve pages automatically with Shell navigation routes
  • configure conditional platform services using if directives

Example prompts for the agent

  • Register DetailViewModel and DetailPage in MauiProgram.cs
  • Configure constructor injection for SettingsPage with ISettingsService
  • How should I register platform-specific audio players in .NET MAUI DI?

How to install maui-dependency-injection

1Skills CLI (Vercel)

npx skills add dotnet/skills --skill maui-dependency-injection -a claude-code

2Manually: put the files in place

~/.claude/skills/maui-dependency-injection/SKILL.md

Copy the whole skill folder; for one project use .claude/skills/maui-dependency-injection/

Step by step, with commands for each platform: How to install skills in Claude Code: five ways

Questions about maui-dependency-injection

How to install maui-dependency-injection in Claude Code?

Run npx skills add dotnet/skills --skill maui-dependency-injection -a claude-code in your terminal. This saves the skill configuration to ~/.claude/skills/maui-dependency-injection/SKILL.md.

What does the maui-dependency-injection skill do?

It teaches AI coding assistants how to register services and ViewModels in .NET MAUI. It covers lifetime selection, Shell dependency resolution, and multi-platform conditional registrations.

Why should AddScoped be avoided in .NET MAUI?

MAUI does not have an HTTP request lifecycle. A scoped service attaches to the window lifetime or behaves as a singleton when resolved from the root provider, failing to give fresh instances across navigation.

Source text

SKILL.md · MIT
---
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).
license: MIT
---

# Dependency Injection in .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.

## 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

## 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)

## 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

## Rules That Change the Answer

| Situation | Do this | Why |
|---|---|---|
| Registering a Page or ViewModel | Prefer `AddTransient` | A fresh instance per navigation avoids stale state, and a Singleton page cannot be re-added to the visual tree after it is removed. Singleton is defensible for a genuinely single-instance page (e.g. a root tab you want to keep warm) |
| Registering shared/expensive state | `AddSingleton` | One instance app-wide (settings, DB connection, `HttpClient` handler) |
| Tempted to use `AddScoped` | Use `AddTransient` (or `AddSingleton` if sharing is intended) | MAUI has **no** built-in request scope like ASP.NET Core's HTTP pipeline. MAUI does create one `IServiceScope` per window, so a Scoped service lives as long as that window — and resolved from the root provider it behaves like a Singleton. Neither gives you per-navigation freshness |
| Navigating to a DI-registered page | Register the page **and** its ViewModel, then `Routing.RegisterRoute` | `Shell.Current.GoToAsync` resolves the page through DI and injects its constructor dependencies |
| Platform-specific implementation | `#if` per platform **with every platform covered** | A missing platform branch leaves the service unregistered and throws at resolution time |

**Do not** introduce DI into a project that isn't using it, swap a working service
lifetime, or add an interface purely for symmetry — only when the user asked or it
fixes a real defect.

**Answer narrowly, but completely.** When you recommend a lifetime change, show the
registration code, and give the realistic alternatives rather than a single verdict —
for a unit-of-work or `DbContext` question that means `AddTransient`, an explicit
`IServiceScopeFactory.CreateScope()`, **and** the factory pattern
(`AddDbContextFactory`), with a note on when each fits. A one-line prescription is
usually a worse answer than a short menu with trade-offs.

```csharp
// Explicit scope when you genuinely need unit-of-work semantics
using var scope = scopeFactory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<MyDbContext>();
```

## 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.

---

## Lifetime Selection

| Lifetime | When to Use | Typical 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 use | Pages, ViewModels, per-call API wrappers |
| `AddScoped<T>()` | Per-window lifetime, or a manually created `IServiceScope` | Scoped unit-of-work (rare in MAUI) |

**Key rule:** Register Pages and ViewModels as **Transient** by default. Register shared services as **Singleton**.

> ⚠️ **Avoid `AddScoped` unless you manually manage `IServiceScope`.** MAUI has no built-in request scope like ASP.NET Core. MAUI creates one `IServiceScope` per window, so a Scoped service lives as long as that window; resolved from the root provider it silently behaves as a Singleton. Neither gives per-navigation freshness.

---

## Registration Pattern in 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();
}
```

---

## 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();
}
```

### ViewModel → Page Wiring

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;
    }
}
```

---

## Shell Navigation Auto-Resolution

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));
```

### Passing parameters to a DI-resolved ViewModel

DI supplies the ViewModel's *dependencies*; navigation parameters arrive separately.
Don't try to inject them through the constructor — implement `IQueryAttributable`
on the ViewModel so it receives both:

```csharp
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 applies query attributes to the page **and** its `BindingContext`, so the
ViewModel receives them without any wiring in the page.

---

## 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
```

---

## 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>();
}
```

---

## 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>();
```

---

## Common Pitfalls

### 1. Singleton ViewModels Cause Stale Data

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

// ✅ Fresh instance each navigation
builder.Services.AddTransient<DetailViewModel>();
```

### 2. ContentTemplate Pages Are Not Created Through DI

Pages declared in Shell XAML via `<ShellContent ContentTemplate="{DataTemplate views:DetailPage}">` are instantiated with `Activator.CreateInstance` (`ElementTemplate.cs`), **not** through the service provider. Constructor injection does not run on that path: if the page's only constructor takes dependencies, you get a `MissingMethodException` — not a silently `null` dependency.

Pages reached through `Routing.RegisterRoute` + `GoToAsync` are different: they go through `ActivatorUtilities.GetServiceOrCreateInstance` (`Routing.cs`), which injects registered dependencies even if the page type itself was never registered, and **throws** if a required dependency cannot be resolved.

```csharp
// Registering the page and its dependencies keeps both paths working
builder.Services.AddTransient<DetailPage>();
builder.Services.AddTransient<DetailViewModel>();
```

If you need DI for a tab/flyout page, give it a parameterless constructor that resolves what it needs, or navigate to it by route instead of embedding it in `ContentTemplate`.

### 3. XAML Resource Parsing vs. DI Timing

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);
    }
}
```

### 4. Service Locator Anti-Pattern

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

// ✅ Constructor injection — explicit and testable
public class MyViewModel(IDataService dataService) { }
```

### 5. Missing Platform in Conditional Registration

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.

### 6. AddScoped Without Manual Scope

See the rule table above: `AddScoped` gives you either window lifetime or Singleton behaviour, never per-navigation freshness. Use `AddTransient` or `AddSingleton` unless you explicitly create and manage an `IServiceScope`.

---

## 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` used only when window lifetime is intended, or alongside a manually created `IServiceScope`

## References

- [Dependency injection in .NET MAUI](https://learn.microsoft.com/dotnet/maui/fundamentals/dependency-injection)
- [.NET dependency injection fundamentals](https://learn.microsoft.com/dotnet/core/extensions/dependency-injection)

© dotnet · MIT · dotnet/skills

Similar skills

Everything for Mobile →

From the same set101

mtp-hot-reload Sets up a persistent Microsoft Testing Platform console host to apply code edits and rerun .NET tests immediately. CX Codex +1 3,400installs template-validation Validates custom dotnet new template configurations before publishing. It spots syntax issues, missing properties, and parameter bugs that cause silent failures. CX Codex +1 3,200installs maui-theming A skill that walks the agent through light and dark themes in .NET MAUI: AppThemeBinding, ResourceDictionary swapping, system theme detection. CX Codex +1 3,200installs maui-data-binding A skill that gives your coding agent the rules for XAML and C# data bindings in .NET MAUI, from compiled bindings to change notification. CX Codex +1 3,200installs migrate-mstest-v3-to-v4 Walks a test project from MSTest v3 to v4: fixes source breaks, explains behavior changes, and sorts out test discovery in CI. CX Codex +1 3,200installs migrate-mstest-v1v2-to-v3 A skill that walks an MSTest v1 or v2 project over to v3: it classifies the workspace, fixes build breaks and swaps .testsettings for .runsettings. CX Codex +1 3,200installs template-authoring Creates and validates custom dotnet new templates from existing .NET projects. It generates .template.config/template.json while keeping your original build settings intact. CX Codex +1 3,200installs migrate-vstest-to-mtp Walks a .NET test solution from VSTest to Microsoft.Testing.Platform: edits Directory.Build.props, global.json and the dotnet test arguments. CX Codex +1 3,200installs

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor