extensions
extensions copied to clipboard
[API Proposal]: IServiceProvider<TService> and IServiceScopeFactory<TService>
Background and motivation
We often have singleton services that need to create a service which we would currently achieve by using a dependency on the main IServiceProvider or IServiceScopeFactory.
Problem
The problems with this design are:
- It hides the list of dependencies this service is acutally consuming as those dependencies are not explicit in its constructor
- When testing the dependent service, mocking the service provider is loose and not explicit as we can't see what service are actually going to be requested just by looking at its constructor.
- It gives the dependent service unlimited access to any service when in essence it should only access a specific service.
Proposed solution
Please enable providing an explicit service and a explicit scoped-service factory, which its purpose is limited to an explicit service only.
API Proposal
Contracts
public interface IServiceProvider<TService>
where TService : class
{
TService? GetService();
}
public interface IServiceScopeFactory<TService>
where TService : class
{
IServiceScope<TService> CreateScope();
}
public interface IServiceScope<TService> : IDisposable
where TService : class
{
// Perhaps we don't need this property and can have a GetService() directly
// or an extension method
IServiceProvider<TService> ServiceProvider { get; }
}
Implementation
public class ServiceProvider<TService>(IServiceProvider serviceProvider)
: IServiceProvider<TService>
where TService : class
{
public TService? GetService() = serviceProvider.GetService<TService>();
}
public class ServiceScopeFactory<TService>(IServiceScopeFactory scopeFactory)
: IServiceScopeFactory<TService>
where TService : class
{
public IServiceScope<TService> CreateScope()
{
var scope = scopeFactory.CreateScope()
var service = new ServiceScope<TService>(scope);
return service;
}
private class ServiceScope(IServiceScope scope) : IScopedService<TService>
{
public IServiceProvider<TService> ServiceProvider { get; } =
scope.GetRequiredService<IServiceProvider<TService>();
public async ValueTask Dispose() => scope.Dispose();
}
}
Registrations
serviceProvider.AddTransient(typeof(IServiceProvider<>), typeof(ServiceProvider<>));
serviceProvider.AddTransient(typeof(IServiceScopeFactory<>), typeof(ServiceScopeFactory<>));
API Usage
public class DownloaderService(
IServiceScopeFactory<IEntityBusinessService> entityBusinessServiceScopeFactory,
IServiceProvider<ITransientService> transientServiceProvider)
{
public async Task DoWrok(CancellationToken ct = default)
{
await using var scope = entityBusinessServiceScopeFactory.CreateScoped();
var entityBusinessService = entityBusinessServiceScopeFactory.GetService();
// Do stuff with entity business service
var myTransientService = transientServiceFactory.GetService();
// Do stuff with transient service
}
}
Additional context
To easily mock these service I can then use:
Declaration
public class ServiceProviderMock<TService>(MockBehavior mockBehavior)
: Mock<TService>(mockBehavior)
where TService : class
{
public new IServiceProvider<TService> Object => field ??= new ServiceProvider(base.Object);
}
public class ServiceScopeFactoryMock<TService>(MockBehavior mockBehavior)
: Mock<TService>(mockBehavior)
where TService : class
{
public new IServiceScopeFactory<TService> Object => field ??= new ServiceScopeFactory(base.Object);
private class ServiceScopeFactory(TService service) : IServiceScopeFactory<TService>
{
public IServiceScope<TService> CreateScope() => new ServiceScope(service);
private class ServiceScope(TService service) : IServiceScope<TService>
{
public IServiceProvider<TService> ServiceProvider => field ??= new ServiceProvider<TService>(service);
}
}
}
internal class ServiceProvider(TService service) : IServiceProvider<TService>
{
public TService? GetService() => service;
}
Usage
Using the above examples we can mock the service in the following manner:
[Fact]
public async Task Should_download_data_when_requested_from_channel()
{
// arrange
var entityBusinessServiceMock = new ServiceScopeFactoryMock<IEntityBusinessService>(MockBehavior.Strict);
entityBusinessServiceMock
.Setup(entityBusinessService => entityBusinessService.GetEntities(It.IsAny<CancellationToken>())
.ReturnsAsync( .. test data .. );
var transientServiceProviderMock = new ServiceProviderMock<ITransientService>(MockBehavior.Strict);
transientServiceMock
.Setup(transientService => transientService.DoWork())
.Returns( .. test data .. );
var downloaderServiceSut =
new DownloaderService(
entityBusinessServiceMock.Object,
transientServiceProviderMock.Object);
// act
// assert
}
Alternative Designs
No response
Risks
No response