I have spent some months writing code with ASP.NET Core MVC. Now I am having a go at worker services but came across some issues with Dependency Injection.
I can register a service in Program.cs and use it in Worker.cs (see below). My problem comes up when you need the service deep down in the class structures like this:
Of course, I could pass the service down the whole hierarchy but in this case, I do not see the value of services. It seems like I can use Dependency Injection only on the level of the worker service. In MVC you can simply register a service and pass it to a Controller’s constructor as an argument. The advantage here was that you never had to call the constructor explicitly just like one the level of Worker.cs. I came about this problem while trying to access connection strings and settings but I guess the same applies to the logger.
Program.cs
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseWindowsService()
.ConfigureServices((hostContext, services) =>
{
services.AddHostedService<Worker>();
services.AddTransient<ITestService,TestService>();
});
Worker.cs
public class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;
public readonly ITestService TestServ;
public Worker(ILogger<Worker> logger, ITestService test)
{
_logger = logger;
TestServ = test;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
//this works
_logger.LogInformation("TestService: " + TestServ.GetData());
//is this necessary?
TestClassServ test = new TestClassServ(TestServ);
_logger.LogInformation("TestService: " + test.GetData());
await Task.Delay(1000, stoppingToken);
}
}
}
Class and Service
public interface ITestService
{
string GetData();
}
public class TestService : ITestService
{
public const string teststring = "service";
public string GetData()
{
return teststring;
}
}
public class TestClassServ
{
public ITestService TestServ { get; set; }
public TestClassServ(ITestService testServ)
{
TestServ = testServ;
}
public string GetData()
{
return TestServ.GetData();
}
}
After weeks of googling and trying, I have decided to ask you for help:
It seems like I can use Dependency Injection only on the level of the worker service.
The framework is certainly able to create worker classes that contain deep object graphs, so it's certainly possible to inject dependencies many levels deep; even when working with worker classes.
But there's a catch here, which is that Worker classes are resolved just once by the framework, basically making them Singletons. This means that, even though you can inject dependencies into them, its direct and indirect dependencies should not be Scoped dependencies.
I assume that this is what you are struggling with, because in most cases, for a worker to do anything useful, it needs to talk to the database. This typically means using Entity Framework, and its DbContext
is always Scoped. Injecting it in a Singleton is a really bad idea. The DbContext
becomes a Captive Dependency.
What this means is that, instead of injecting dependencies into the worker's constructor, you will have to resolve them from the container during the lifetime of the application. This couples the Worker to the DI Container, but that is not a problem when you make the Worker part of your Composition Root.
This does require, though, that you create a new scope before resolving services. Here's an example:
public class Worker : BackgroundService
{
// NOTE: Logger can be safely injected; loggers are always singletons
private readonly ILogger<Worker> _logger;
private readonly IServiceProvider _provider;
public Worker(ILogger<Worker> logger, IServiceProvider provider)
{
_logger = logger;
_provider = provider;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await using (var scope = _provider.CreateScope())
{
var service = scope.GetRequiredService<ITestService>();
service.GoDoSomethingUseful();
}
await Task.Delay(1000, stoppingToken);
}
}
}
Is my problem rooted in the architecture of the program?
No, not as far as your code examples show. As long as you keep access to the DI Container (or its IServiceProvider
abstraction) inside your Composition Root, you're fine.