Search code examples
c#.net-coreautofacserilogasp.net-core-hosted-services

How to override Serilog logger in Autofac child scope?


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"        
      }
    ]
  }
}

Solution

  • 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.