ASP.NET Core – Dependency inject a background service into the controllers

Let’s say you have a background service called DatabaseLoggerService. It runs as a hosted background service and logs messages to the database. It has the following definition:

public class DatabaseLoggerService : BackgroundService, ILoggerService
Code language: C# (cs)

You want your controllers to use this for logging. They don’t need to know about the concrete DatabaseLoggerService class, and they don’t need to know they are actually using a background service. Therefore, you’ll want them to depend on ILoggerService.

First, constructor inject ILoggerService into your controllers, like this:

[Route("[controller]")] [ApiController] public class RecipesController : ControllerBase { private readonly ILoggerService Logger; public RecipesController(ILoggerService logger) { Logger = logger; } //rest of controller }
Code language: C# (cs)

Then, in Startup.ConfigureServices(), you’ll need to register DatabaseLoggerService as both an ILoggerService and as a HostedService, like this:

public class Startup { //rest of class public void ConfigureServices(IServiceCollection services) { //rest of method var loggerService = new DatabaseLoggerService(); services.AddSingleton<ILoggerService>(loggerService); services.AddHostedService(_ => loggerService); } }
Code language: C# (cs)

Note: AddHostedService() expects an IHostedService, so you can’t use serviceProvider.GetService<ILoggerService>(). This is why you have to actually create the DatabaseLoggerService instance and register it like this.

When a request comes into the controller, the framework will inject the DatabaseLoggerService singleton into the controller, because it was registered as an ILoggerService. The controller can then use the logging functionality without needing to know anything about the concrete implementation or the fact that it’s a background service.

If your background service needs dependencies resolved

The approach shown above is fine if the background service doesn’t have any dependencies it needs resolved using ServiceProvider. However, let’s say your background service depends on IHostApplicationLifetime. You can use the following approach to register the background service as both a singleton and hosted service while resolving its dependencies:

public class Startup { //rest of class public void ConfigureServices(IServiceCollection services) { //rest of method services.AddSingleton<ILoggerService>(sp => { var hostAppLifetime = sp.GetService<IHostApplicationLifetime>(); return new DatabaseLoggerService(hostAppLifetime); }); services.AddHostedService(sp => sp.GetService<ILoggerService>() as DatabaseLoggerService); } }
Code language: C# (cs)

The difference with this approach and one shown at the beginning of the article is in AddHostedService() it’s resolving ILoggerService and casting it to a DatabaseLoggerService. That’s a bit unsafe, but we know for sure that the ILoggerService we registered is definitely a DatabaseLoggerService, so it’s fine.

Why not inject IHostedService or the concrete class?

You’ve probably heard the saying: “Code against interfaces, not implementations.

Does the controller need to know that it’s using a background service that’s logging to the database? In most cases, no. It only needs to know that it’s using a logger. In other words, the controller should code against the ILoggerService, not the concrete DatabaseLoggerService class.

If you pass in the concrete background service class, then the controller would have access to the background service methods, such as StopAsync(). In most cases, this would be undesirable:

[Route("[controller]")] [ApiController] public class RecipesController : ControllerBase { private readonly DatabaseLoggerService Logger; public RecipesController(DatabaseLoggerService logger) { Logger = logger; } [HttpGet("{id}")] public async Task<IActionResult> Get(int id) { await Logger.StopAsync(HttpContext.RequestAborted); //rest of method } }
Code language: C# (cs)

Furthermore, injecting interfaces makes the code easier to unit test because you mock out the interface.

Leave a Comment