I have a basic .NET Core Generic Host that uses Serilog and Autofac.
public class Program
{
public static async Task Main(string[] args)
{
var configuration = CreateConfiguration(args);
Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(configuration)
.CreateLogger();
try
{
Log.Information("Starting host...");
await new HostBuilder()
.UseServiceProviderFactory(new AutofacServiceProviderFactory())
.ConfigureHostConfiguration(cb => cb.AddConfiguration(configuration))
.ConfigureServices((ctx, sc) => {
sc.AddLogging(lb => lb.AddSerilog());
})
.ConfigureContainer<ContainerBuilder>(cb => {
cb.RegisterModule(new AppModule());
})
.RunConsoleAsync();
}
catch (Exception ex) {
Log.Fatal(ex, "Host terminated unexpectedly");
}
finally {
Log.CloseAndFlush();
}
}
private static IConfiguration CreateConfiguration(IReadOnlyCollection<string> args)
{
return new ConfigurationBuilder().SetBasePath(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location))
.AddJsonFile("appsettings.json", false, true)
.AddUserSecrets<Program>(true)
.AddCommandLine(args.ToArray())
.Build();
}
}
AppModule
is an Autofac module where I register a IHostedService
internal class AppModule : Module
{
protected override void Load(ContainerBuilder builder)
{
builder.Register(
ctx => {
var c = ctx.Resolve<IComponentContext>();
return new AutofacHostedServiceScopeOwner(
() => c.Resolve<ILifetimeScope>()
.BeginLifetimeScope(b => b.Register(x => Log.Logger.ForContext("name", "value")).As<Serilog.ILogger>()),
scope => new MessageProcessingService(
scope.Resolve<ILogger<MessageProcessingService>>()
)
);
}
)
.As<IHostedService>();
}
}
This part looks complicated, but what I am trying to accomplish is to run MessageProcessingService
(an IHostedService
implementation) in an Autofac child scope so that I can override the resolved logger within the entire MessageProcessingService
graph with some context specific information. There will be multiple MessageProcessingService
instances and each requires a logger with unique context so that I can correlate log output.
I use AutofacHostedServiceScopeOwner
to wrap MessageProcessingService
in a child scope. There is probably a better way to do this, but it's the best I could come up with.
public class AutofacHostedServiceScopeOwner : IHostedService, IDisposable
{
private readonly Func<ILifetimeScope> _scopeFactory;
private readonly Func<ILifetimeScope, IHostedService> _serviceFactory;
private ILifetimeScope _currentScope;
private IHostedService _hostedService;
public AutofacHostedServiceScopeOwner(Func<ILifetimeScope> scopeFactory, Func<ILifetimeScope, IHostedService> serviceFactory) {
_scopeFactory = scopeFactory;
_serviceFactory = serviceFactory;
}
public async Task StartAsync(CancellationToken cancellationToken) {
_currentScope = _scopeFactory();
_hostedService = _serviceFactory(_currentScope);
await _hostedService.StartAsync(cancellationToken);
}
public async Task StopAsync(CancellationToken cancellationToken) {
if (_hostedService != null)
await _hostedService.StopAsync(cancellationToken);
_currentScope?.Dispose();
}
public void Dispose() {
_currentScope?.Dispose();
}
}
This is all working correctly, except the resolved logger does not contain the expected context properties. This makes me think that my override logic is wrong, but I'm not sure what else to try.
c.Resolve<ILifetimeScope>()
.BeginLifetimeScope(
b => b.Register(x => Log.Logger.ForContext("name", "value")).As<Serilog.ILogger>()
)
appsettings.json
{
"Serilog": {
"Enrich": [ "FromLogContext" ],
"MinimumLevel": "Debug",
"WriteTo": [
{
"Name": "Console"
}
]
}
}
I was able to figure this out after hours of banging my head against the wall.
builder.Register(
ctx => {
var c = ctx.Resolve<IComponentContext>();
return new AutofacHostedServiceScopeOwner(
() => c.Resolve<ILifetimeScope>()
.BeginLifetimeScope(
b => {
b.RegisterGeneric(typeof(Logger<>)).As(typeof(ILogger<>));
b.Register(
x => new SerilogLoggerFactory(
Log.Logger.ForContext("name", "value")
)
)
.As<ILoggerFactory>()
.InstancePerLifetimeScope();
}
),
scope => new MessageProcessingService(
scope.Resolve<ILogger<MessageProcessingService>>()
)
);
}
)
.As<IHostedService>();
The reason this works is because in Program.cs
the logger configuration call sc.AddLogging(lb => lb.AddSerilog())
registers ILogger<>
and ILoggerFactory
as singletons under the covers. The lb.AddSerilog()
call registers a Serilog provider which is used by the ILoggerFactory
, but I was not able to find any way to override the provider specifically so I replaced them all.
Hopefully this helps someone.