Why singleton hates scoped injection in .NET dependency injection

Published: Saturday 7 October 2023

When it comes to injecting services with dependency injection in .NET, most services can be injected regardless of their service time.

However, when we inject a scoped service into a service with a singleton lifetime, a runtime exception is thrown.

We'll explore why this is, when it becomes a problem and how we can go about fixing this.

The service lifetimes

When using dependency injection in .NET, there are three service lifetimes that we can choose from. These are:

  • Singleton - instance for the application's lifetime
  • Scoped - instance for the duration of the scope. An example is a HTTP request in an ASP.NET Core application
  • Transient - new instance everytime it's injected

Services with the scoped and transient lifetime can inject services regardless of their lifetime.

What happens when injecting through a service with the singleton lifetime?

Singleton and transient service lifetimes have no problem being injected into a service with a singleton lifetime. But when it comes to a scoped service lifetime, the following runtime exception is thrown:

Cannot consume scoped service

This is because there can be many scopes initialised at any one time in an application's lifetime. The application wouldn't know which scope to use.

When does this exception become a problem?

Entity Framework is a popular ORM for performing operations on a SQL Server database. However, it's default service lifetime is scoped. Although it's possible to change this, there are many reasons not to.

But this presents a problem when we use Entity Framework with a service with a singleton service lifetime. Examples of singleton service lifetimes include a background service that is used in a hosted service, or a SignalR hub.

How do we use a scoped service lifetime with a singleton?

We can inject theĀ IServiceProvider instance into the service and use either theĀ CreateScope or CreateAsyncScope method.

This allows us to create a custom scope and we can use it to resolve any services with a scoped service lifetime.

When creating a custom scope, it creates a separate IServiceProvider instance that we can use to resolve services. With this instance, we can either use the GetService or GetRequiredService method. The only difference is that GetService will return NULL if a service can't be injected, whereas the GetRequiredService will throw an exception.

Here's an example of how it can be done with a background hosted service. The IFrameworkService has been added with a scoped service lifetime to the IoC container. By resolving the service in the custom scope that we've created, it means we can use the methods within it.

public class Worker : BackgroundService
{
    private readonly ILogger<Worker> _logger;
    private readonly IServiceProvider _serviceProvider;

    public Worker(ILogger<Worker> logger, IServiceProvider serviceProvider)
    {
        _logger = logger;
        _serviceProvider = serviceProvider;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            await using (var scope = _serviceProvider.CreateAsyncScope())
            {
                var frameworkService = scope.ServiceProvider.GetRequiredService<IFrameworkService>();

                foreach (var framework in await frameworkService.GetAllFrameworksAsync())
                {
                    _logger.LogInformation(framework.FrameworkName);
                }
            }

            await Task.Delay(TimeSpan.FromMinutes(1));
        }
    }
}

Watch the video

Watch our video where we talk about what the difference dependency injection service lifetimes, why a singleton service can't inject a scoped and a working example and demonstration of how we can create a custom scope to fix it.

Dependency injection online course

If you want to learn more about dependency injection in .NET, check out our online course that explores the different service lifetimes, how to use them in an ASP.NET Core application and what common errors to look out for.