Description
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
-
Redundancy and API Duplication
IServiceScope<T>
introduces a parallel abstraction toIServiceScope
, 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. -
Fragmented Dependency Resolution
The existingIServiceScope
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. -
Potential ambiguity in Scope Ownership.
Developers might assumeIServiceScope<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. -
Discoverability and Learning Curve
IServiceScope
is widely documented and familiar in ASP.NET Core and .NET applications.
AddingIServiceScope<T>
increases the surface area and learning burden, especially for new developers. -
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.