Friday, 15 June 2018

InvalidOperationException: Cannot resolve scoped service from root provider.

Dotnet Core 2 and a DI error that kind of makes sense in my head but I couldn't see why I was getting it.

I am using a Controller with an Encryptor injected into the constructor and saw the above message about the encryptor, which is registered as Scoped. My understanding was that Controllers were created in scope so there should be no problem. Like many times I don't understand, I realise I need to read things more carefully!

I thought the error call-stack was only framework code but there was one line that wasn't and it was my code, except it wasn't the controller, it was a library and in the library, I had the following code:

services.AddSingleton(c => new ClientUtilities(
                readonlyDatabase,
                writableDatabase,
                sharedRedis,
                c.GetRequiredService(),
                c.GetRequiredService>()
            ));

Since the encryption provider is scoped, I am not allowed to do this. Now that I realised the actual mistake I made, it made more sense so I thought I would explain it here.

Dependency Injection takes a little while to fully appreciate but most frameworks will allow you to register services in a couple of different ways. In Dotnet core, the built-in DI framework has the following types:


  1. Singleton
  2. Scoped
  3. Transient
1 and 3 are the easiest to understand. 

When you register a Singleton, the first time you resolve the service, it will create a single instance and this will live forever! It will be shared by any subsequent calls to resolve the service. The only exception to this description is that you can register a Singleton (and only a Singleton) using an already created object, in which case, obviously, it is already created and not instantiated at resolve time.

A transient registration means that you will get a new instance every time you call resolve.

A scoped registration means that you will always get the same instance returned within a single scope. You can set these scopes up yourself but an easy to understand example is the request on a web application which automatically creates a scope, meaning that within a single request, you will always get the same instance of a service registered with AddScoped().

The general choice of which to use is related to performance. If you have a utility service that simply performs some static functionality, it would be wasteful to create a new instance of it every time it was needed. In general, you should prefer to register Singletons where possible. The problem with the Singleton occurs if there are member variables i.e. the object has state. If the object has state then multiple threads accessing it would screw things up. You can either make the class thread-safe and keep it as a Singleton or otherwise decide that you should use a Scoped or Transient pattern instead.

Since Transient is the least well performing method, you should reserve this for small, lightweight objects that have no state that needs sharing.

Using Scoped objects can be helpful because you can share stuff only between the same request in a web application, which will only be single-threaded (unless you create more threads yourself), so it doesn't need to be thread-safe, but you can keep the state.

Another use of Scoped is where your service has a resource that itself is not thread safe and you don't want to create a new resource in every single method that needs it, each time the method is called.

The problem you have, however, is that a Singleton cannot use a Scoped service as a dependency. This is for two reasons. The first is philosophical: If you have a single instance of a Singleton, then it doesn't make sense for it to reference something that is designed to have multiple instances since the scope could change at random times from the Singletons view of the world. The second is practical: The system can create instances of Singletons as early as it can in the application's lifecycle. If there is no request, there would be no scope to resolve for the Scoped() service so it can't work.

I believe that you will only get the above exception if the Singleton is resolved early on and otherwise you would get unexpected behaviour later in the lifecycle but don't quote me on that!
Post a Comment