mcpField
M
maui-dependency-injection

maui-dependency-injection

github.com/davidortinauDevelopment17772 installsUpdated 17w ago
Repository

About maui-dependency-injection

This skill offers .NET MAUI developers guidance on dependency-injection lifetimes and registrations. It focuses on page and ViewModel registration, XAML initialization timing, and common platform-specific service pitfalls.

Key features

  1. 01Choose lifetimes based on state and use
  2. 02Register pages and ViewModels as transient
  3. 03Defer service resolution to CreateWindow
  4. 04Avoid overusing the service locator
  5. 05Check platform-specific registration coverage

Use cases

  1. 01Set ViewModel lifetimes for page navigation
  2. 02Troubleshoot pages missing injected dependencies
  3. 03Handle XAML resources parsed before DI is ready
  4. 04Prevent missing services from platform conditionals

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/davidortinau/maui-skills --skill maui-dependency-injection

After installing, type/maui-dependency-injectionin Claude Code to confirm it loaded.

Bundled filesreferences/dependency-injection-api.md

Details

From the project README

Dependency Injection in .NET MAUI

Lifetime Decision Framework

QuestionAnswer → Lifetime
Does it hold shared state or is expensive to create?AddSingleton
Is it stateless, lightweight, or per-request?AddTransient
Do you manage IServiceScope yourself?AddScoped

⚠️ Avoid AddScoped in MAUI — there is no built-in scope per page. Using it without manually creating IServiceScope gives you singleton behaviour silently, which is confusing and error-prone.

Singleton traps
// ❌ ViewModel registered as Singleton — stale data across navigations
builder.Services.AddSingleton<DetailViewModel>();

// ✅ ViewModels are Transient — fresh instance each navigation
builder.Services.AddTransient<DetailViewModel>();

Register Pages and ViewModels as Transient. Register services that hold shared state as Singleton (e.g., IDataService, HttpClient factory).


Gotcha: XAML Resource Parsing vs. DI Timing

XAML resources (App.xaml styles, converters) are parsed during InitializeComponent() — before the DI container is fully available. If a resource or converter needs a service, resolve it in CreateWindow(), not in the constructor.

// ❌ Resolving services during XAML parse — container may not be ready
public App(IDataService data)
{
    InitializeComponent(); // XAML parses here
    _data = data;          // may fail for types not yet resolved
}

// ✅ Defer service resolution to 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
        var mainPage = _services.GetRequiredService<MainPage>();
        return new Window(new AppShell());
    }
}

Gotcha: Unregistered Page Silently Skips DI

If a Page is used in Shell XAML (<ShellContent ContentTemplate="...">) but not registered in builder.Services, MAUI instantiates it with the parameterless constructor. Dependencies are silently null — no exception.

// ❌ Page not registered — constructor injection silently skipped
// builder.Services.AddTransient<DetailPage>(); // missing!

// ✅ Always register pages that need injection
builder.Services.AddTransient<DetailPage>();
builder.Services.AddTransient<DetailViewModel>();

Anti-Pattern: Service Locator Overuse

// ❌ Service locator scattered through code — hard to test, hides dependencies
public void DoWork()
{
    var service = this.Handler.MauiContext.Services.GetService<IDataService>();
    service.Load();
}

// ✅ Constructor injection — explicit, testable
public class MyViewModel(IDataService dataService)
{
    public void DoWork() => dataService.Load();
}

Use explicit resolution (Handler.MauiContext.Services) only when constructor injection is genuinely unavailable (e.g., inside a custom handler or platform callback).


Platform-Specific Registration Pitfall

When using #if directives for platform services, ensure the interface is always registered — otherwise consumers on untargeted platforms get a runtime null.

// ❌ No registration on Windows — GetService returns null
#if ANDROID
builder.Services.AddSingleton<INotificationService, AndroidNotificationService>();
#elif IOS || MACCATALYST
builder.Services.AddSingleton<INotificationService, AppleNotificationService>();
#endif

// ✅ Cover all platforms or provide a no-op fallback
#if ANDROID
builder.Services.AddSingleton<INotificationService, AndroidNotificationService>();
#elif IOS || MACCATALYST
builder.Services.AddSingleton<INotificationService, AppleNotificationService>();
#elif WINDOWS
builder.Services.AddSingleton<INotificationService, WindowsNotificationService>();
#endif

Checklist

  • Every Page and ViewModel that needs injection is registered in MauiProgram.cs
  • Pages/ViewModels are AddTransient; shared services are AddSingleton
  • Constructor injection used everywhere possible; service locator only as last resort
  • Interfaces defined for any service you need to mock in tests
  • Platform-specific #if registrations cover all target platforms (or provide fallback)
  • Late-bound services resolved in CreateWindow(), not during XAML parse
  • AddScoped only used when you manually manage IServiceScope

Comments

Markdown supported