How do you resolve scoped services in a background service?

Published: Monday 21 July 2025

Creating a scope in dependency injection happens per HTTP request. But what if you are running a background service?

The problem with scoped and transient service lifetimes

If you were to inject a scoped service lifetime instance into a background service using the constructor's parameters, you'll get the following AggregateException:

Cannot consume scoped service '{Type}' from singleton 

This is because background services behave like a singleton service lifetime. You can't inject a scoped lifetime into a singleton as it would not know what scope it belonged to.

It's possible to inject a transient service lifetime into a singleton. However, it would be behave exactly like a singleton. This is because the transient service lifetime would only be injected once when the background service is initialised.

So how do you get around this issue?

Creating a scope

Creating a scope involves injecting the IServiceScopeFactory type into your service and invoking the CreateScope method.

It's also possible to create a scope with the IServiceProvider type. But you can't do this with any singleton service lifetimes like a background service because the IServiceProvider type is a scoped service lifetime.

Here's how you do it for a background service:

// Worker.cs
public class Worker : BackgroundService
{
	private readonly IServiceScopeFactory _serviceScopeFactory;

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

	protected override async Task ExecuteAsync(CancellationToken cancellationToken)
	{
		while (!cancellationToken.IsCancellationRequested)
		{            
			using (var scope = _serviceScopeFactory.CreateScope())
			{
			}
		}
	}
}

It's important to use the using keyword when creating a scope. The using keyword will dispose the scope when it comes out of the block statement. This will ensure that any necessary resources are cleaned up.

To improve code readability you can create a new scope without providing curly braces. In this instance, the scope will be disposed at the end of the block statement.

// Worker.cs
public class Worker : BackgroundService
{
	private readonly IServiceScopeFactory _serviceScopeFactory;

	public Worker(IServiceScopeFactory serviceScopeFactory,
	{
		_serviceScopeFactory = serviceScopeFactory;
	}

	protected override async Task ExecuteAsync(CancellationToken cancellationToken)
	{
		while (!cancellationToken.IsCancellationRequested)
		{            
			using (var scope = _serviceScopeFactory.CreateScope());
			// Scope will be disposed off at this point
		}
	}
}

It's also possible to create an async scope. This is helpful when any of your classes implement the IAsyncDisposable interface. This allows you to dispose services using async methods.

// Worker.cs
public class Worker : BackgroundService
{
	private readonly IServiceScopeFactory _serviceScopeFactory;

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

	protected override async Task ExecuteAsync(CancellationToken cancellationToken)
	{
		while (!cancellationToken.IsCancellationRequested)
		{            
			await using (var scope = _serviceScopeFactory.CreateAsyncScope()) {
			
			}			
		}
	}
}

You can also use it without the curly braces.

// Worker.cs
public class Worker : BackgroundService
{
	private readonly IServiceScopeFactory _serviceScopeFactory;

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

	protected override async Task ExecuteAsync(CancellationToken cancellationToken)
	{
		while (!cancellationToken.IsCancellationRequested)
		{            
			await using (var scope = _serviceScopeFactory.CreateAsyncScope());
			// Scope is disposed here
		}
	}
}

How to use services in a custom scope

Now that you know the different ways to create a custom scope, it's time to resolve the service so you can use it.

When you create a new scope, it creates an IServiceScope instance. Within that is a ServiceProvider property with a IServiceProvider type. This allows you to use the GetService or GetRequiredService methods to resolve services. Read our article to learn the differences between these methods.

We have these classes created and the services registered in Program.cs:

// ICategoryScopedService.cs
public interface ICategoryScopedService
{
	DateTime Datestamp { get; }
}

// CategoryScopedService.cs
public class CategoryScopedService : ICategoryScopedService
{
	public DateTime Datestamp { get; }

	public CategoryScopedService()
	{
		Datestamp = DateTime.UtcNow;
	}
}

// ICategoryTransientService.cs
public interface ICategoryTransientService
{
	DateTime Datestamp { get; }
}

// CategoryTransientService.cs
public class CategoryTransientService : ICategoryTransientService
{
	public DateTime Datestamp { get; }

	public CategoryTransientService()
	{
		Datestamp = DateTime.UtcNow;
	}
}

// Program.cs
builder.Services.AddScoped<ICategoryScopedService, CategoryScopedService>();
builder.Services.AddTransient<ICategoryTransientService, CategoryTransientService>();

And this is how we use them in a background service:

// Worker.cs
public class Worker : BackgroundService
{
	private readonly IServiceScopeFactory _serviceScopeFactory;

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

	protected override async Task ExecuteAsync(
		CancellationToken cancellationToken)
	{
		while (!cancellationToken.IsCancellationRequested)
		{
			using var scope = _serviceScopeFactory.CreateScope();

			var scoped = scope.ServiceProvider.GetRequiredService<ICategoryScopedService>();

			var transient = scope.ServiceProvider.GetRequiredService<ICategoryTransientService>();

			Console.WriteLine(scoped.Datestamp.ToString("HH:mm:ss.ffffff"));
			Console.WriteLine(transient.Datestamp.ToString("HH:mm:ss.ffffff"));

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

Using keyed services

You can also resolve keyed services in an almost identical method. Here we have created a CategoryComputersStorageService class and added it as a keyed service using the transient service lifetime:

// ICategoryStorageService.cs
public interface ICategoryStorageService
{
	DateTime Datestamp { get; }
}

// CategoryComputersStorageService.cs
public class CategoryComputersStorageService : ICategoryStorageService
{
	public DateTime Datestamp { get; }

	public CategoryComputersStorageService()
	{
		Datestamp = DateTime.UtcNow;
	}
}

// Program.cs
builder.Services.AddKeyedTransient<ICategoryStorageService, CategoryComputersStorageService>("computers");

We then either call the GetKeyedService or GetRequiredKeyedService from the IServiceScope ServiceProvider property to resolve it, remembering to include the key that it corresponds to:

// Worker.cs
public class Worker : BackgroundService
{
	private readonly IServiceScopeFactory _serviceScopeFactory;

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

	protected override async Task ExecuteAsync(
		CancellationToken cancellationToken)
	{
		while (!cancellationToken.IsCancellationRequested)
		{
			using var scope = _serviceScopeFactory.CreateScope();

			var keyed = scope.ServiceProvider.GetRequiredKeyedService<ICategoryStorageService>("computers");

			Console.WriteLine(keyed.Datestamp.ToString("HH:mm:ss.ffffff"));

			await Task.Delay(TimeSpan.FromSeconds(1));

		}
	}        
}

Multithreading

It's highly recommended to create separate scopes with multithreading. If you start sharing scopes across multiple threads, there is a high possibility that the scope may have been disposed when it is called. It also presents an issue with scoped service lifetime methods that are not thread safe.

In this example, we are calling the Task.WhenAll method to call the DIAsyncNewScope method three times asynchronously. As the DIAsyncNewScope method creates a new scope every time it runs, we won't be presented with non-thread safe methods or scope sharing problems:

[Route("api/[controller]")]
[ApiController]
public class WebApiController : ControllerBase
{
	private readonly IServiceScopeFactory _serviceScopeFactory;

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

	[HttpGet("multiple-threads-unique-scope")]
	public async Task<IActionResult> MultipleThreadsUniqueScope()
	{
		await Task.WhenAll([
			DIAsyncNewScope(1),
			DIAsyncNewScope(2),
			DIAsyncNewScope(3)
		]);

		return NoContent();
	}


	private Task DIAsyncNewScope(int task)
	{
		using var scope = _serviceScopeFactory.CreateScope();

		var scoped = scope.ServiceProvider.GetRequiredService<ICategoryScopedService>();

		Console.WriteLine();
		Console.WriteLine($"WORKER - ASYNC - TASK {task}");
		Console.WriteLine($"Datestamp = {scoped.Datestamp:HH:mm:ss.ffffff}");

		return Task.CompletedTask;
	}
}

Problems may occurs when we start sharing the scope. Below, we've created a new DIAsyncSharedScope method with some slight modifications to the DIAsyncNewScope method. We are passing in an parameter with the IServiceScope type. That means that we could use the same scope every time this method is called. We are also adding a random delay before resolving the ICategoryScopedService instance from that scope.

We've also modified the web API endpoint to create a new scope and pass it in to each of the threads that we are running. We've also changed the call to Task.WhenAny. This means that the NoContent method will return when any of these tasks are completed and will dispose of the scope.

[Route("api/[controller]")]
[ApiController]
public class WebApiController : ControllerBase
{
	private readonly IServiceScopeFactory _serviceScopeFactory;

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

	...

	[HttpGet("multiple-threads-shared-scope")]
	public async Task<IActionResult> MultipleThreadsSharedScope()
	{
		using var scope = _serviceScopeFactory.CreateScope();

		await Task.WhenAny([
			DIAsyncSharedScope(1, scope),
			DIAsyncSharedScope(2, scope),
			DIAsyncSharedScope(3, scope)
		]);

		return NoContent();
	}

	...

	private async Task DIAsyncSharedScope(int task, IServiceScope scope)
	{
		var random = new Random();
		var secs = random.Next(1, 5);

		await Task.Delay(TimeSpan.FromSeconds(secs));

		var scoped = scope.ServiceProvider.GetRequiredService<ICategoryScopedService>();

		Console.WriteLine();
		Console.WriteLine($"WORKER - ASYNC - TASK {task}");
		Console.WriteLine($"Datestamp = {scoped.Datestamp:HH:mm:ss.ffffff}");
	}
}

When calling the MultipleThreadsSharedScope web API method, it's likely that only one of the tasks will have any console output. This is because the scope would have been disposed before any of the other threads are able to resolve the ICategoryScopedService instance.

Watch our video

Watch our video where we talk you through how to create a custom scope and how they are used in background services and multithreading:

And if you want to create a new scope in a background task or multithreading, you can download the code example to try it out for yourself.