How to Fix Cannot Consume Scoped Service from Singleton?

When deploy .net core, we have found interesting exception

InvalidOperationException: Cannot consume scoped service from singleton.

I found it interesting because it’s actually the Service DI of ASP.NET Core trying to make sure you don’t trip yourself up. Although it’s not foolproof (They still give you enough rope to hang yourself), it’s actually trying to stop you making a classic DI scope mistake. I thought I would try and build up an example with code to first show them, then show you here! Of course, if you don’t care about any of that, there is a nice TL;DR right at the end of this post!

The Setup

So there is a little bit of code setup before we start explaining everything. The first thing we need is a “Child” service :

public class MyChildService
{
    public static int CreationCount { get; private set; }

    public MyChildService()
    {
        CreationCount++;
    }
}

The reason we have a property here called “CreationCount” is because later on we are going to test if a service is being created on a page refresh, or it’s re-using existing instances (Don’t worry, it’ll make sense!)

Next we are going to create *two* parent classes. I went with a Mother and Father class, for no other reason that the names seemed to fit (They are both parent classes). They also will have a creation count, and reference the child service.

public class MyFatherService
{
    public static int CreationCount { get; private set; }

    private readonly MyChildService _myChildService;

    public MyFatherService(MyChildService myChildService)
    {
        _myChildService = myChildService;
        CreationCount++;
    }
}

public class MyMotherService
{
    public static int CreationCount { get; private set; }

    private readonly MyChildService _myChildService;

    public MyMotherService(MyChildService myChildService)
    {
        _myChildService = myChildService;
        CreationCount++;
    }
}

ASPHostPortal

Finally, we should create a controller that simply outputs how many times the services have been created. This will help us later in understanding how our service collection is created and, in some circumstances, shared.

[Route("api/[controller]")]
public class HomeController : Controller
{
    private readonly MyFatherService _myFatherService;
    private readonly MyMotherService _myMotherService;

    public HomeController(MyFatherService myFatherService, MyMotherService myMotherService)
    {
        _myFatherService = myFatherService;
        _myMotherService = myMotherService;
    }

    [HttpGet]
    public string Get()
    {
        return $"Father Creation Count : {MyFatherService.CreationCount}. Mother Creation Count : {MyFatherService.CreationCount}. Child Creation Count : {MyChildService.CreationCount}";
    }
}

The Scoped Service Problem

The first thing we want to do, is add a few lines to the ConfigureServices method of our startup.cs. This first time around, all services will be singletons.

services.AddSingleton<MyFatherService>();
services.AddSingleton<MyMotherService>();
services.AddSingleton<MyChildService>();

Now, let’s load our page and refresh if a few times and see what the output is. The results of 3 page refreshes look like so :

Father Creation Count : 1. Mother Creation Count : 1. Child Creation Count : 1
Father Creation Count : 1. Mother Creation Count : 1. Child Creation Count : 1
Father Creation Count : 1. Mother Creation Count : 1. Child Creation Count : 1

So this makes sense. A singleton is one instance per application, so no matter how many times we fresh the page, it’s never going to create a new instance.

Let’s change things up, let’s make everything transient.

services.AddTransient<MyFatherService>();
services.AddTransient<MyMotherService>();
services.AddTransient<MyChildService>();

And then we load it up 3 times.

Father Creation Count : 1. Mother Creation Count : 1. Child Creation Count : 2
Father Creation Count : 2. Mother Creation Count : 2. Child Creation Count : 4
Father Creation Count : 3. Mother Creation Count : 3. Child Creation Count : 6

It’s what we would expect. The “parent” classes go up by 1 each time. The child actually goes up by 2 because it’s being “requested” twice per page load. One for the “mother” and one for the “father”.

Let’s instead make everything scoped. Scoped means that a new instance will be created essentially per page load (Atleast that’s the “scope” within ASP.NET Core).

services.AddScoped<MyFatherService>();
services.AddScoped<MyMotherService>();
services.AddScoped<MyChildService>();

And now let’s refresh out page 3 times.

Father Creation Count : 1. Mother Creation Count : 1. Child Creation Count : 1
Father Creation Count : 2. Mother Creation Count : 2. Child Creation Count : 2
Father Creation Count : 3. Mother Creation Count : 3. Child Creation Count : 3

OK this makes sense. Each page load, it creates a new instance for each. But unlike the transient example, our child creation only gets created once per page load even though two different services require it as a dependency.

Right, so we have everything as scoped. Let’s try something. Let’s say that we decide that our parents layer should be singleton. There could be a few reasons we decide to do this. There could be some sort of “cache” that we want to use. It might have to hold state etc. The one reason we don’t want to make it singleton is for performance. Very very rarely do I actually see any benefit from changing services to singletons. And the eventual screw ups because you are making things un-intuitively singletons is probably worse in the long run.

We change our ConfigureServices method to :

services.AddSingleton<MyFatherService>();
services.AddSingleton<MyMotherService>();
services.AddScoped<MyChildService>();

We hit run and….

InvalidOperationException: Cannot consume scoped service 'ScopedServiceIssue.MyChildService' from singleton 'ScopedServiceIssue.MyFatherService'.

So because our ChildService is scoped, but the FatherService is singleton, it’s not going to allow us to run. So why?

If we think back to how our creation counts worked. When we have a scoped instance, each time we load the page, a new instance of our ChildService is created and inserted in the parent service. Whereas when we do a singleton, it keeps the exact same instance (Including the same child services). When we make the parent service a singleton, that means that the child service is unable to be created per page load. ASP.NET Core is essentially stopping us from falling in this trap of thinking that a child service would be created per page request, when in reality if the parent is a singleton it’s unable to be done. This is why the exception is thrown.

What’s the fix? Well either the parent needs to be scoped or transient scope. Or the child needs to be a singleton itself. The other option is to use a service locator pattern but this is not really recommended. Typically when you make a class/interface singleton, it’s done for a very good reason. If you don’t have a reason, make it transient.

The Singleton Transient Trap

The interesting thing about ASP.NET Core catching you from making a mistake when a scoped instance is within a singleton, is that the same “problem” will arise from a transient within a singleton too.

So to refresh your memory. When we have all our services as transient :

services.AddTransient<MyFatherService>();
services.AddTransient<MyMotherService>();
services.AddTransient<MyChildService>();

We load the page 3 times, and we see that we create a new instance every single time the class is requested.

Father Creation Count : 1. Mother Creation Count : 1. Child Creation Count : 2
Father Creation Count : 2. Mother Creation Count : 2. Child Creation Count : 4
Father Creation Count : 3. Mother Creation Count : 3. Child Creation Count : 6

So let’s try something else. Let’s make the parent classes singleton, but leave the child as a transient.

services.AddSingleton<MyFatherService>();
services.AddSingleton<MyMotherService>();
services.AddTransient<MyChildService>();

So we load the page 3 times. And no exception but…

Father Creation Count : 1. Mother Creation Count : 1. Child Creation Count : 2
Father Creation Count : 1. Mother Creation Count : 1. Child Creation Count : 2
Father Creation Count : 1. Mother Creation Count : 1. Child Creation Count : 2

This is pretty close as having the child as scoped with the parents singletons. It’s still going to give us some unintended behaviour because the transient child is not going to be created “everytime” as we might first think. Sure, it will be created everytime it’s “requested”, but that will only be twice (Once for each of the parents).

I think the logic behind this not throwing an exception, but scoped will, is that transient is “everytime this service is requested, create a new instance”, so technically this is correct behaviour (Even though it’s likely to cause issues). Whereas a “scoped” instance in ASP.NET Core is “a new instance per page request” which cannot be fulfilled when the parent is singleton.

Still, you should avoid this situation as much as possible. It’s highly unlikely that having a parent as a singleton and the child as transient is the behaviour you are looking for.

Related Posts

Leave a Reply

Your email address will not be published. Required fields are marked *