Search code examples
c#.netentity-frameworkdependency-injection

How should we use per-request DbContext instances in an IHostedService?


Keeping in mind the .NET dependency injection guidelines recommendations:

  • Avoid using the service locator pattern. For example, don't invoke GetService to obtain a service instance when you can use DI instead.
  • Another service locator variation to avoid is injecting a factory that resolves dependencies at run time. Both of these practices mix Inversion of Control strategies.

The .NET docs say we can consume a scoped service via IServiceProvider or IServiceScopeFactory as in here or here, e.g.

public class ConsumeScopedServiceHostedService: BackgroundService
{
   // ...
   public ScopedProcessingService(IServiceProvider services) { // ... }
   
   protected override async Task ExecuteAsync(CancellationToken stoppingToken)
   {
      using (var scope = _services.CreateScope())
      {
         var dbContext = scope.ServiceProvider.GetRequiredService<FooDbContext>();
         // ...
      }
}

Another way is some variation of a factory, e.g.

public class ConsumeFactoryHostedService: BackgroundService
{
   // ...
   public ConsumeFactoryHostedService(Func<FooDbContext> dbContextFactory) { // ... }

   protected override async Task ExecuteAsync(CancellationToken stoppingToken)
   {
      using (var dbContext = _dbContextFactory()) { // ... }
   }
}

Aren't both of these implementations against the previously mentioned recommendations though? It may be that for many cases "it doesn't matter" (especially for "simple" microservices), but I'm still interested in what is the "most correct" way—so, is there another pattern we should use? How should we use short-lived DbContext instances in an IHostedService?


Solution

  • Aren't both of these implementations against the previously mentioned recommendations though?

    No they're not, but to understand that we have to look at Mark Seemann's concept of the Composition Root and the Service Locator pattern.

    A Composition Root is

    is a single, logical location in an application where modules are composed together.

    In his book Dependency Injection in .NET, Mark describes the Composition Root pattern as a foundational concept of Dependency Injection. Although I read that book a few times, I'm more familiar with the second edition, Dependency Injection Principles, Practices, and Patterns, as I coauthored that edition. This is why, next, I'll quote solely from that edition.

    In the second edition, Mark and I mention that:

    If you use a DI Container, the Composition Root should be the only place where you use the DI Container. Using a DI Container outside the Composition Root leads to the Service Locator anti-pattern [§4.1]

    The Service Locator anti-pattern is what the Microsoft documentation recommends against, as do we in the book. The book defines Service Locator as:

    A Service Locator supplies application components outside the Composition Root with access to an unbounded set of Volatile Dependencies. [§5.2]

    Notice the cross reference back to the Composition Root. What this means is what was already mentioned above:

    Using a DI Container outside the Composition Root leads to the Service Locator anti-pattern [§4.1]

    The reason why the use of a DI Container inside the Composition Root is fine and is not considered to be an application of the Service Locator anti-pattern, is because in that case it doesn't exhibit the downsides of the Service Locator anti-pattern. In a sense behavior on the maintainability of the application is completely different compared to use outside the Composition Root. Or, as Mark Seemann elegantly described in the past: Service Locator is about the role it plays in the application, not the mechanics. Or, put differently, if you wouldn't even be able to use a DI Container inside your Composition Root, you'd end up with using no DI Container at all.

    So what this means for your particular case with your hosted service is the following:

    your hosted service is allows to depend on the IServiceProvider (or other container-specific interfaces)... as long as the hosted service is placed inside the Composition Root.

    We could, of course, try to place all of our application inside the Composition Root, but that wouldn't lead to a maintenance hell. The Composition Root should not contain any application or business logic; it should solely contain the infrastructure needed to bootstrap the application. In other words, it is allowed to tie dependencies together, manage their lifetime, define adapters to back-end systems, and implement cross-cutting concerns.

    This this all means is that, if placed inside the Composition Root, your hosted service should be as small as possible and only deal with infrastructure logic, making it a Humble Object. You can do that by extracting all application logic out of the hosted service and place it inside a separate class. That class itself might have many dependencies injected into its constructor, and those can be resolved by the DI Container.

    This way, the only thing the hosted service has to do is start a new IServiceScope, resolve that extracted class, invoke it, and dispose of the service scope. For instance:

    // This class is part of the Composition Root
    public class ConsumeScopedServiceHostedService : BackgroundService
    {
        public ScopedProcessingService(IServiceProvider services) ...
       
        protected override async Task ExecuteAsync(CancellationToken token)
        {
            using (var scope = _services.CreateScope())
            {
                // ExtractClassWithTheLogic is part of the application
                var service = scope.ServiceProvider
                    .GetRequiredService<ExtractClassWithTheLogic>();
             
                service.Run(token);
            }
        }
    }
    
    // Part of the application logic
    public sealed class ExtractClassWithTheLogic
    {
        public ExtractClassWithTheLogic(
            FooDbContext dbContext,
            /* other dependencies go here */)
        {
            ...
        }
    
        public void Run(CancellationToken cancellation) ...
    }