extensions icon indicating copy to clipboard operation
extensions copied to clipboard

[API Proposal]: IServiceProvider<TService> and IServiceScopeFactory<TService>

Open weitzhandler opened this issue 6 months ago • 1 comments

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:

  1. It hides the list of dependencies this service is acutally consuming as those dependencies are not explicit in its constructor
  2. 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.
  3. 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

weitzhandler avatar Jun 11 '25 01:06 weitzhandler