23
\$\begingroup\$

I want to use IServiceScopeFactory in a way, which is not the Service Locator anti-pattern.

Therefore I thought about making a generic one, which supports explicitly only the given service.

I have made these interfaces:

public interface IServiceScopeFactory<T> where T : class
{
    IServiceScope<T> CreateScope();
}

public interface IServiceScope<T> : IDisposable where T : class
{
    T Service { get; }
}

And these implementations:

public class ServiceScopeFactory<T> : IServiceScopeFactory<T> where T:class
{
    private readonly IServiceScopeFactory _serviceScopeFactory;

    public ServiceScopeFactory(IServiceScopeFactory serviceScopeFactory)
    {
        _serviceScopeFactory = serviceScopeFactory;
    }

    public IServiceScope<T> CreateScope()
    {
        return new ServiceScope<T>(_serviceScopeFactory.CreateScope());
    }
}

public class ServiceScope<T> : IServiceScope<T> where T : class
{
    private readonly IServiceScope _scope;
    private T _service;

    public ServiceScope(IServiceScope scope)
    {
        _scope = scope;
    }

    public void Dispose()
    {
        _scope?.Dispose();
    }

    public T Service => _service ?? (_service = _scope.ServiceProvider.GetRequiredService<T>());
}

Usage is for example:

public class Foo
{
    private readonly IServiceScopeFactory<ConmaniaDbContext> _dbContextFactory;

    public Foo(IServiceScopeFactory<ConmaniaDbContext> dbContextFactory)
    {
        _dbContextFactory = dbContextFactory;
    }

    public void Bar()
    {
        using (var scope = _dbContextFactory.CreateScope())
        {
            var dbContext = scope.Service
            //use service
        }
    }
}

For registering it, I have made these extension methods:

public static void AddServiceScopeFactory<T>(this IServiceCollection serviceCollection) where T : class
{
    serviceCollection.AddTransient<IServiceScopeFactory<T>, ServiceScopeFactory<T>>();
}

The registration is like that:

services.AddScoped<ConmaniaDbContext, ConmaniaDbContext>(); //Scoped Service
services.AddServiceScopeFactory<ConmaniaDbContext>();
services.AddSingleton<Foo, Foo>(); //Singleton Service

My base problem was that I need the Scoped Service DbContext in a Singleton service.

Is this a good way for dependency injection with a custom scope or is this just a wrapper around the Service Locator pattern? Is there anything you would improve? Or any obviously problems I might get with this implementation?

To clarify, this is where I am coming from:

public class Foo
{
    private readonly IServiceScopeFactory _scopeFactory;

    public Foo(IServiceScopeFactory scopeFactory)
    {
        _scopeFactory = scopeFactory;
    }

    public void Bar()
    {
        using (var scope = _scopeFactory.CreateScope())
        {
            var dbContext = scope.ServiceProvider.GetRequiredService<ConmaniaDbContext>()
            //use service
        }
    }
}

But this feels like the Service Locator Pattern, or am I wrong? The problem is here, that the class constructor doesn't know which dependencies the class needs.

The problem I am facing is that IServiceScopeFactory feels like the Service Locator Pattern. Therefore I created a IServiceScopeFactory<T>.

Since the whole Dependency Injection Pattern is relative new to me, I want to know if this is a suitable way of doing it, or some misconception.

\$\endgroup\$
7
  • 1
    \$\begingroup\$ How much of this code is real? \$\endgroup\$
    – t3chb0t
    Commented Jan 19, 2018 at 17:07
  • 1
    \$\begingroup\$ Everything but Foo class. The Bar method would for example query onetime the database to get some informations. @t3chb0t \$\endgroup\$ Commented Jan 19, 2018 at 17:09
  • 2
    \$\begingroup\$ Disposing the scope will automatically dispose any services acquired from it, so you don't need to put the dbContext in a using block. \$\endgroup\$
    – Brad M
    Commented Jan 19, 2018 at 19:04
  • 1
    \$\begingroup\$ Injecting factories can sometimes be misused like a service locator. It is most times best to keep it simple and use Explicit dependency principle and just inject what is actually needed by the dependent class. In this case the factory is misleading as the class does not really need it. It really wants a ConmaniaDbContext which is what should have been injected. \$\endgroup\$
    – Nkosi
    Commented Jan 20, 2018 at 12:55
  • 2
    \$\begingroup\$ The problem I am facing, is that ConmaniaDbContext is registered Scoped while the Service, that needs to use it, is registered as Singleton. This is the a part that I can not change. A Singleton can not consume Scoped or Transient services. @Nkosi \$\endgroup\$ Commented Jan 20, 2018 at 14:22

2 Answers 2

12
\$\begingroup\$

I'm of the opinion that this is awesome, and I have a couple of suggestions to add. It's not "Service Locator" because you're explicitly constraining to the exact type that can be resolved---the explicit dependency is right up there in the constructor.

(I wish they'd bake this into .Net Core.)

  1. Don't store the service-object
    • I think it would be better to replace T Service with T GetRequiredService(). If you're resolving a Transient service, it would be useful. Additionally, it makes for fewer changes when someone switches away from the built-in IServiceScopeFactory.
  2. Finish implementing IDispose
  3. Go Singleton
    • I see no reason to register as Scoped, considering what this does. It's using a Singleton, so make it Singleton.

Here's my tweaked version

public interface IServiceScopeFactory<T> where T : class
{
    IServiceScope<T> CreateScope();
}

public interface IServiceScope<T> : IDisposable where T : class
{
    T GetRequiredService();
    T GetService();
    IEnumerable<T> GetServices();
}


public class ServiceScopeFactory<T> : IServiceScopeFactory<T> where T : class
{
    private readonly IServiceScopeFactory _serviceScopeFactory;

    public ServiceScopeFactory(IServiceScopeFactory serviceScopeFactory) => _serviceScopeFactory = serviceScopeFactory;

    public IServiceScope<T> CreateScope() => new ServiceScope<T>(_serviceScopeFactory.CreateScope());
}

public class ServiceScope<T> : IServiceScope<T> where T : class
{
    readonly IServiceScope _scope;

    public ServiceScope(IServiceScope scope) => _scope = scope;

    public T GetRequiredService() => _scope.ServiceProvider.GetRequiredService<T>();

    public T GetService() => _scope.ServiceProvider.GetService<T>();

    public IEnumerable<T> GetServices() => _scope.ServiceProvider.GetServices<T>();


    #region IDisposable/Dispose methods ( https://stackoverflow.com/a/538238/530545 )
    bool _disposed = false;

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool calledFromCodeNotTheGarbageCollector)
    {
        if (_disposed)
            return;
        if (calledFromCodeNotTheGarbageCollector)
        {
            // dispose of manged resources in here
            _scope?.Dispose();
        }
        _disposed = true;
    }

    ~ServiceScope() { Dispose(false); }
    #endregion

}

You'd register this like so:

services.AddSingleton(typeof(IServiceScopeFactory<>), typeof(ServiceScopeFactory<>));

And to work with your example, you'd use it like so (assuming you registered your ConmaniaDbContext):

public class Foo
{
    readonly IServiceScopeFactory<ConmaniaDbContext> _dbCtxFactory;

    public Foo(IServiceScopeFactory<ConmaniaDbContext> dbCtxFactory) => _dbCtxFactory = dbCtxFactory;

    public void Bar()
    {
        using var scope = _dbCtxFactory.CreateScope();
        var db = scope.GetRequiredService();
        //use service
    }
}

FYI/Backstory

FYI, I'm using this in a .Net Core 3.x app that's leveraging Gql.Net. This pattern makes it so I can actually use EFC. Gql.Net's field resolvers run in parallel, so if you don't do something like this, you cannot use your constructor-injected DbContext in more than one field, or you wind up getting this EFC exception (which is not unique to Gql.Net):

[InvalidOperationException] A second operation started on this context before a previous operation completed. This is usually caused by different threads using the same instance of DbContext. For more information on how to avoid threading issues with DbContext, see https://go.microsoft.com/fwlink/?linkid=2097913.

I've confirmed this solves that "parallel execution in the same Http request" issue for me. I can now run multiple, concurrent tasks against the database from the same instance of a scoped service class. It's because with this solution, instead of all threads using the same DbContext that was injected into the constructor, they now each resolve their own, scoped DbContext instance when they need it.

\$\endgroup\$
10
  • \$\begingroup\$ How do you mock dbContext in this approach for testing? \$\endgroup\$ Commented May 1, 2020 at 4:32
  • 1
    \$\begingroup\$ The same way you would decide to mock it had you directly injected it in the constructor. \$\endgroup\$
    – Granger
    Commented May 2, 2020 at 5:05
  • \$\begingroup\$ How do you inject the IServiceScopeFactory into the Foo service? \$\endgroup\$
    – mjwrazor
    Commented Jan 12, 2022 at 22:27
  • \$\begingroup\$ @mjwrazor - In the example above, it's constructor-injected, and later used in the Bar() method. \$\endgroup\$
    – Granger
    Commented Jan 13, 2022 at 0:07
  • \$\begingroup\$ @Granger that doesn't answer the question. For example I implemented this and to do the injection for Foo() I did something like this. services.AddSingleton<ITicketStore>(provider => new Foo(provider.GetRequiredService<IServiceScopeFactory<DbContext>>() )); Or is there a better way. \$\endgroup\$
    – mjwrazor
    Commented Jan 13, 2022 at 15:20
5
\$\begingroup\$

At first, I liked your approach quite a bit.

However, I think there's a smell here that's being ignored.

In limiting the IServiceScopeFactory<T>/IServiceScope<T> to a single type (instead of multiple types like the non-generic version does) you are establishing a new scope for each type.

This approach will work if you have a single type that needs to be injected and used across the code path, but if you have more than one type (and that's very possible), you are now establishing multiple scopes for a single code path, which is a problem.

This will cause an error if the multiple injected types use shared scoped types; multiple scoped instances will be created.

To get around this, you shouldn't use constructor injection for this. Rather, you should resolve the dependencies whose lifetimes conflict with the injectee at the call site, and pass them in as parameters.

If you can't do this (say, this is a hosted service, or something of that nature) then while it would be a code smell, I think you should stick with the non-generic IServiceScopeFactory and stick with constructing a single scope when necessary and resolve your types from the scopes created from the factory.

\$\endgroup\$
1
  • 1
    \$\begingroup\$ Yeah you are absolutely right. Good pointer. I didn't realised this, because I always use some "root service", when creating a new Scope. This way, all required Dependencies are injectet within the same scope into the requested "root service". It wasn't meant to get multiple different scopes for one unit of work. Only, "Create me this service with a seperate scope". \$\endgroup\$ Commented Aug 7, 2021 at 1:19

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.