Skip to content

[API Proposal] Add IServiceScope<T> to promote type-safe scoped resolution and reduce service locator usage #116285

Open
@dombrovsky

Description

@dombrovsky

Background and motivation

Currently, IServiceScope exposes IServiceProvider, which encourages retrieving arbitrary services by type — a form of the service locator antipattern. This makes testing, reasoning about dependencies, and composition harder.

By introducing IServiceScope<TDependency>, we can:

  • Narrow the scope to a single, expected dependency.
  • Encourage better dependency design and inversion of control.
  • Improve discoverability and safety (e.g., GetRequiredService() will always return a TDependency).
  • Maintain compatibility with existing IServiceScope.

This API may provide a simple, opt-in mechanism to move away from open-ended service resolution and guide developers toward cleaner, more maintainable DI practice.

API Proposal

public interface IServiceProvider<out TDependency>
    where TDependency : class
{
    TDependency GetRequiredService();
    TDependency? GetService();
    IEnumerable<TDependency> GetServices();
}

public interface IServiceScope<out TDependency> : IServiceProvider<TDependency>, IDisposable
    where TDependency : class
{
}

public interface IAsyncServiceScope<out TDependency> : IServiceProvider<TDependency>, IAsyncDisposable
    where TDependency : class
{
}

public interface IServiceScopeFactory<out TDependency>
    where TDependency : class
{
    IServiceScope<TDependency> CreateScope();
    IAsyncServiceScope<TDependency> CreateAsyncScope();
}

Optional extension methods:

public static class ServiceProviderTypedServiceScopeExtensions
{
    public static IServiceScope<T> CreateScope<T>(this IServiceProvider serviceProvider)
        where T : class =>
        serviceProvider.GetRequiredService<IServiceScopeFactory<T>>().CreateScope();

    public static IAsyncServiceScope<T> CreateAsyncScope<T>(this IServiceProvider serviceProvider)
        where T : class =>
        serviceProvider.GetRequiredService<IServiceScopeFactory<T>>().CreateAsyncScope();
}

API Usage

public class FooWorker : BackgroundService
{
    private readonly IServiceScopeFactory<FooService> _fooServiceScopeFactory;

    // Inject IServiceScopeFactory<FooService> instead of IServiceScopeFactory
    public FooWorker(IServiceScopeFactory<FooService> fooServiceScopeFactory)
    {
        _fooServiceScopeFactory = fooServiceScopeFactory;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            using var scope = _fooServiceScopeFactory.CreateScope();

            var fooService = scope.GetRequiredService();  // only instance of FooService is possible to resolve from created scope.

            await fooService.DoWorkAsync(stoppingToken);

            await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
        }
    }
}

Alternative Designs

No response

Risks

  1. Redundancy and API Duplication
    IServiceScope<T> introduces a parallel abstraction to IServiceScope, potentially confusing developers with multiple similar-sounding scope APIs.
    Risk: Developers may not understand when to use one over the other, leading to inconsistent codebases.

  2. Fragmented Dependency Resolution
    The existing IServiceScope provides access to all services; IServiceScope restricts access to only one type (or service graph rooted at one type).
    Harder to resolve additional services in the same scope if needed, possibly leading to multiple overlapping scopes and duplicated lifetimes.

  3. Potential ambiguity in Scope Ownership.
    Developers might assume IServiceScope<T> only disposes T, but in reality it also disposes all scoped dependencies created during resolution of T. Could lead to incorrect assumptions about object lifetimes and side effects.

  4. Discoverability and Learning Curve
    IServiceScope is widely documented and familiar in ASP.NET Core and .NET applications.
    Adding IServiceScope<T> increases the surface area and learning burden, especially for new developers.

  5. Reduced Flexibility
    Typed scopes enforce a compile-time contract on what can be resolved, improving safety—but potentially reducing adaptability in dynamic scenarios.
    Developers might find it harder to support conditional logic or resolve supporting services that weren’t originally anticipated in the scope design.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions