I have a .NET5 WorkerService project that contains many BackgroundService
classes and they all have their own ILogger<T>
construction parameters. I want to use Autofac to register a different SerilogProvider
for each ILogger<T>
, and automatically configure the File sink related to the BackgroundService
class name for it.
For example:
This is a BackgroundService
.
public class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;
public Worker(ILogger<Worker> logger)
{
_logger = logger;
}
}
I hope to automatically complete the work equivalent to the following code:
Host.CreateDefaultBuilder(args)
.UseServiceProviderFactory(new AutofacServiceProviderFactory())
.ConfigureContainer<ContainerBuilder>(builder =>
{
var logger = new LoggerConfiguration()
.WriteTo.File(nameof(Worker)).CreateLogger();
builder.Register((c, p) =>
new LoggerFactory(new ILoggerProvider[]
{
new SerilogLoggerProvider(logger)
})).As<ILoggerFactory>();
builder.RegisterGeneric(typeof(Logger<>))
.As(typeof(ILogger<>)).SingleInstance();
})
.ConfigureServices((hostContext, services) =>
{
services.AddHostedService<Worker>();
});
Of course, there is more than one BackgroundService
. So is there a best practice?
There are a couple of ways you could do this.
The first thing you could do is use scoped services like it shows in the docs. You can register things in a child lifetime scope that only apply to that scope.
Minor disclaimer - I'm not running all this through a compiler and testing it out first. I may have some typos or something you'll have to tweak.
In the docs they split the "thing that does work" and the BackgroundService
apart. You'd have to do the same. In the docs, the "thing that does work" (where you'd have your separate logger) is the ScopedProcessingService
.
internal interface IScopedProcessingService
{
Task DoWork(CancellationToken stoppingToken);
}
internal class ScopedProcessingService : IScopedProcessingService
{
private int executionCount = 0;
private readonly ILogger _logger;
// This will be the special per-service logger you want.
public ScopedProcessingService(ILogger<ScopedProcessingService> logger)
{
_logger = logger;
}
public async Task DoWork(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
executionCount++;
_logger.LogInformation(
"Scoped Processing Service is working. Count: {Count}", executionCount);
await Task.Delay(10000, stoppingToken);
}
}
}
// This is the background service itself - you won't do the work
// in here because you want the logging to come out of your special
// logger.
public class ConsumeScopedServiceHostedService : BackgroundService
{
private readonly ILogger<ConsumeScopedServiceHostedService> _logger;
// This is not going to get the special logger.
public ConsumeScopedServiceHostedService(IServiceProvider services,
ILogger<ConsumeScopedServiceHostedService> logger)
{
Services = services;
_logger = logger;
}
public IServiceProvider Services { get; }
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation(
"Consume Scoped Service Hosted Service running.");
await DoWork(stoppingToken);
}
private async Task DoWork(CancellationToken stoppingToken)
{
_logger.LogInformation(
"Consume Scoped Service Hosted Service is working.");
// Here's the good bit - you need to register your special
// logger just for this lifetime scope.
//
// First, get the lifetime scope that resolved this BackgroundService.
var parent = Services.GetRequiredService<ILifetimeScope>();
// Now create a child scope and register your special logger.
// All the other stuff - registering the generic types and
// things that are NOT specific to this special logger factory -
// can go in the main container that you configure during
// HostBuilder.ConfigureContainer like you already have.
using (var scope = parent.BeginLifetimeScope(b =>
var logger = new LoggerConfiguration()
.WriteTo
.File(nameof(ConsumeScopedServiceHostedService))
.CreateLogger();
b.Register((c, p) =>
new LoggerFactory(new ILoggerProvider[]
{
new SerilogLoggerProvider(logger)
})).As<ILoggerFactory>();
))
{
// This is how the scoped service will get the
// special logger - resolve from the scope.
var scopedProcessingService =
scope.Resolve<IScopedProcessingService>();
await scopedProcessingService.DoWork(stoppingToken);
}
}
public override async Task StopAsync(CancellationToken stoppingToken)
{
_logger.LogInformation(
"Consume Scoped Service Hosted Service is stopping.");
await base.StopAsync(stoppingToken);
}
}
You could make that whole BeginLifetimeScope
call into an extension method so basically it could all happen at once and your code would be nice and clean.
The second thing you could do is use keyed services. A keyed service helps you identify a specific service registration with a key. You could combine that with attribute filters to inject a specific ILoggerFactory
(not the actual generic ILogger<T>
but the factory that can create it).
First, you'd probably want to make a method for the registrations.
public static void RegisterLoggerFactory(
this ContainerBuilder builder,
string name)
{
var logger = new LoggerConfiguration()
.WriteTo
.File(name)
.CreateLogger();
// The factory can be a single, named instance -
// the logger instances are the transient things.
builder.Register((c, p) =>
new LoggerFactory(new ILoggerProvider[]
{
new SerilogLoggerProvider(logger)
}))
.Keyed<ILoggerFactory>(name)
.SingleInstance();
}
Now when you set up your container, you'll have to do that for all the background services.
You also need to register the service using Autofac, not using the AddHostedService
extension. From the code for AddHostedService
all it's doing is registering it as a singleton IHostedService
. We need to use Autofac so we can tell it to use the keyed attribute.
Host.CreateDefaultBuilder(args)
.UseServiceProviderFactory(new AutofacServiceProviderFactory())
.ConfigureContainer<ContainerBuilder>(builder =>
{
// We're not doing anything with ILogger<T> because
// we'll get that from the factory.
builder.RegisterLoggerFactory(nameof(Worker));
// Register the service using Autofac. Make sure
// to enable attribute filtering. You might even be
// able to make another extension method to make this
// cleaner or less repetitious.
builder.RegisterType<Worker>()
.As<IHostedService>()
.SingleInstance()
.WithAttributeFiltering();
})
.ConfigureServices((hostContext, services) =>
{
// Not doing this anymore!
// services.AddHostedService<Worker>();
});
In each service, you need to add an attribute to the constructor so it knows which logger factory to use. For your ILogger<T>
you'll get that from the factory.
public class Worker : BackgroundService
{
// It'll be a non-generic logger but that's OK, it'll be right.
private readonly ILogger _logger;
public Worker([KeyFilter(nameof(Worker))] ILoggerFactory loggerFactory)
{
// loggerFactory is the keyed one that was registered
// special for this service.
_logger = loggerFactory.CreateLogger(nameof(Worker))
}
}
Again, you could refactor some of that, like you could move the ILoggerFactory.CreateLogger
into the Autofac registration and have a keyed ILogger
instead of a keyed ILoggerFactory
if you wanted. You could do all sorts of stuff to refactor and remove the duplicate code. But this is the gist - using keyed services with the filter attribute.
Either way here would work. If it was me, I feel like the keyed service way of things is probably the easiest and doesn't require so much change of the original code.