Using scoped services inside singletons

Using scoped services inside singletons

ยท

4 min read

Disclaimer

I would like to preface this discussion by stating that trying to use scoped services inside of singletons (or using any short-lived service inside of a longer-lived service for that matter!) is generally a bad idea. There's a reason why most dependency injection (DI) containers try to stop you from doing it. Trying to use scoped services inside of singletons can lead to what's known as Captive Dependencies, which can cause all sorts of nasty bugs and memory leaks.

If you find yourself in a situation where you are trying to inject a scoped service into a singleton, that's generally a code smell and you should seriously consider refactoring your services to avoid that dependency.

Nevertheless, there are sometimes legitimate reasons to use scoped services inside singletons. If you believe this to be true of your use case, then please read on.

Introduction

Have you ever tried injecting a service into another service, but get the following exception?:

InvalidOperationException: Cannot resolve scoped service 'IMyScopedService' from root provider.

If so, this is a good indication that you might be trying to inject a scoped service into a singleton service. The exception appears because the DI container is trying to protect you from Captive Dependencies (see the disclaimer above). While it is generally good that the DI container tries to stop you from doing such things, it is sometimes necessary to do so.

I recently came across this when trying to create a hosted service in ASP.NET Core. A hosted service allows you to create long running background task, and it essentially behaves like a singleton service (with a few minor caveats).

Some of my background tasks need to make use of other services, most of which are transient/scoped. However, when I tried injecting these services into the consructor of my hosted service, I got an exception similar to the one above.

The Problem

The ASP.NET Core DI container has a root IServiceProvider which is used to resolve singleton services. For scoped services, the container must first create a new scope, and each scope will have it's own IServiceProvider. Scoped services can only be accessed from the IServiceProvider within their own scope, and not from the root IServiceProvider.

ASP.NET Core DI container scopes

The Solution

To be able to use scoped services within a singleton, you must create a scope manually. A new scope can be created by injecting an IServiceScopeFactory into your singleton service (the IServiceScopeFactory is itself a singleton, which is why this works). The IServiceScopeFactory has a CreateScope method, which is used for creating new scope instances.

public class MySingletonService
{
  private readonly IServiceScopeFactory _serviceScopeFactory;

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

  public void Execute()
  {
    using (var scope = _serviceScopeFactory.CreateScope())
    {
      var myScopedService = scope.ServiceProvider.GetService<IMyScopedService>();

      myScopedService.DoSomething();
    }
  }
}

The created scope has it's own IServiceProvider, which you can access to resolve your scoped services.

It is important to make sure that the scope only exists for as long as is necessary, and that it is properly disposed of once you have finished with it. This is to avoid any issues of captive dependencies (as discussed at the start of this article. Therefore, I would recommend:

  • Only define the scope within the method that you intend to use it. It might be tempting to assign it to a field for reuse elsewhere in the singleton service, but again this will lead to captive dependencies.
  • Wrap the scope in a using statement. This will ensure that the scope is properly disposed of once you have finished with it.

Conclusion

While trying to resolve scoped services within a singleton can often be a sign that your code needs refactoring, sometimes it is still necessary to do so.

Scoped services can be used in singletons by creating a scope and using the IServiceProvider of the scope. However, it is important to make sure that the scope is cleaned up once it has been used to prevent captive dependencies.

If you found this article useful, please like it and share it. For more content like this, please follow this blog and follow me on Twitter. If you want, you can also buy me a coffee ! ๐Ÿ˜Š

Did you find this article valuable?

Support Sam Walpole by becoming a sponsor. Any amount is appreciated!