Search code examples
.net-coredependency-injectionworker

Use custom DI provider in DotNetCore 3.1 Worker Service?


Does anyone know how to set up a custom DI provider (such as Castle Windsor) in a DotNetCore Worker Service? I've seen info on how to do it for ASP.NET apps, but all the examples talk about modifying Startup.cs, which does not exist in the Worker Service template.


Solution

  • Using custom dependency injection container in a .NET Core Worker project

    Castle Windsor doesn't support recent .NET Core versions, so I'll use Autofac as an example.

    When you start a new worker project, you'll have a Program class like this

    public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }
    
        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureServices((hostContext, services) =>
                {
                    services.AddHostedService<Worker>();
                });
    }
    

    Host.CreateDefaultBuilder gives you an IHostBuilder, which has necessary methods to extend or replace the DI container. We're using Autofac, so let's install it:

    dotnet add package Autofac.Extensions.DependencyInjection
    

    Then introduce it to our host. Here ContainerBuilder comes from Autofac assembly.

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            // use autofac
            .UseServiceProviderFactory(new AutofacServiceProviderFactory())
            // configure autofac di container
            .ConfigureContainer<ContainerBuilder>((hostContext, container) =>
            {
                // hostcontext gives you access to config & environment
                // hostContext.Configuration.GetSection("ApiKeys");
                // hostContext.HostingEnvironment.IsDevelopment()
    
                // auto register all classes in the assembly
                container
                    .RegisterAssemblyTypes(typeof(Program).Assembly)
                    .AsImplementedInterfaces();
            })
            // configure microsoft di container
            .ConfigureServices((hostContext, services) =>
            {
                // let autofac register this service
                // services.AddHostedService<Worker>();
            });
    

    Now let's create a service. IJob interface here is unnecessary, it's just there to show Autofac's capabilities:

    internal interface IJob
    {
        Task ExecuteAsync(CancellationToken cancellationToken = default);
    }
    
    internal class WarmUpCaches : IJob
    {
        private readonly ILogger<WarmUpCaches> _logger;
    
        public WarmUpCaches(ILogger<WarmUpCaches> logger)
        {
            _logger = logger;
        }
    
        public async Task ExecuteAsync(CancellationToken cancellationToken = default)
        {
            _logger.LogInformation("Warming up caches");
            var steps = 5;
            while (!cancellationToken.IsCancellationRequested && --steps > 0)
            {
                _logger.LogInformation("working...");
                await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
            }
    
            _logger.LogInformation("Done");
        }
    }
    

    Now let's use this service inside the background service:

    class Worker : BackgroundService
    {
        private readonly ILogger<Worker> _logger;
        private readonly IJob _job;
    
        public Worker(ILogger<Worker> logger, IJob job)
        {
            _logger = logger;
            _job = job;
        }
    
        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            await _job.ExecuteAsync(stoppingToken);
        }
    }
    

    When we run the app, it logs:

    info: CustomDiExample.WarmUpCaches[0]
          Warming up caches
    info: CustomDiExample.WarmUpCaches[0]
          working...
    info: Microsoft.Hosting.Lifetime[0]
          Application started. Press Ctrl+C to shut down.
    info: Microsoft.Hosting.Lifetime[0]
          Hosting environment: Development
    info: Microsoft.Hosting.Lifetime[0]
          Content root path: C:\...\CustomDiExample
    info: CustomDiExample.WarmUpCaches[0]
          working...
    info: CustomDiExample.WarmUpCaches[0]
          working...
    info: CustomDiExample.WarmUpCaches[0]
          working...
    info: CustomDiExample.WarmUpCaches[0]
          Done
    

    Consuming services without using a background service

    If the application doesn't need to keep working, we can do without a BackgroundService.

    We need to modify the Main method and expose the IHost instance. We can resolve any service from the container using IHost.Services.

    public static async Task Main(string[] args)
    {
        var host = CreateHostBuilder(args).Build();
    
        // stop the application with Ctrl+C, SIGINT, SIGTERM
        var lifetime = host.Services.GetRequiredService<IHostApplicationLifetime>();
        var cancellationToken = lifetime.ApplicationStopping;
    
        var job = host.Services.GetRequiredService<IJob>();
        await job.ExecuteAsync(cancellationToken);
    }
    

    References