Skip to content

Dependency injection: document that AddScoped has no natural scope boundary in .NET MAUI #3249

@PureWeen

Description

@PureWeen

Summary

The dependency injection documentation does not clearly warn that AddScoped has no natural scope boundary in .NET MAUI (for non-Blazor apps). Developers with ASP.NET Core or Blazor experience expect AddScoped to provide per-request or per-page instances, but in MAUI there is no built-in scope — a scoped service registered this way effectively behaves like a singleton within the app lifetime unless you manually create and manage IServiceScope.

Why it matters

This is a subtle but impactful correctness bug:

  • In ASP.NET Core, AddScoped creates one instance per HTTP request
  • In Blazor, AddScoped creates one instance per circuit
  • In .NET MAUI (non-Blazor), there is no equivalent built-in scope boundary — a scoped service resolves to the same instance across the entire app unless the developer explicitly creates a scope with IServiceScopeFactory

A developer who registers a unit-of-work or database context as AddScoped expecting per-navigation isolation will get a shared instance instead, leading to stale state, cross-page data contamination, and hard-to-reproduce bugs.

What should be documented

Add a note to the service lifetime table and/or the AddScoped entry in the dependency injection docs:

Note for .NET MAUI (non-Blazor) apps: AddScoped has no natural scope boundary. Unlike ASP.NET Core (per-request) or Blazor (per-circuit), MAUI does not automatically create scopes during navigation. A scoped service will behave like a singleton for the lifetime of the app unless you explicitly create a scope using IServiceScopeFactory.CreateScope(). For per-navigation isolation, prefer AddTransient, or manually manage scopes:

// Explicitly create a scope (advanced pattern)
using var scope = serviceProvider.GetRequiredService<IServiceScopeFactory>().CreateScope();
var unitOfWork = scope.ServiceProvider.GetRequiredService<IUnitOfWork>();
await unitOfWork.SaveChangesAsync();
// scope is disposed here, releasing scoped services

For most MAUI use cases:

  • Use AddTransient for ViewModels and Pages (fresh instance per navigation)
  • Use AddSingleton for shared services (database connections, settings, caches)
  • Reserve AddScoped for manually managed scope patterns only

Suggested location

docs/fundamentals/dependency-injection.md — in the service lifetime table or as a callout after the lifetime guidance section

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions