Search code examples
c#asp.net-core.net-coredependency-injectionserilog

Register open generic Serilog logger via factory


One can resolve a framework logger with category by idiomatic .NET code:

using Microsoft.Extensions.Logging;

public class Foo
{
  private readonly ILogger<Foo> _logger;

  public Foo(ILogger<Foo> logger) =>
    _logger = logger ?? throw new ArgumentNullException(nameof(logger));
}

With Serilog it's more complicated:

using Serilog;

public class Foo
{
  private readonly ILogger _logger;

  public Foo(ILogger logger) =>
    _logger = logger?.ForContext<Foo>() ?? throw new ArgumentNullException(nameof(logger));
}

It's easy to forget the logger?.ForContext<Foo>() and it' a bit of a nuisance too.

I'd like to be able to do this:

using Serilog;

public class Foo
{
  private readonly ILogger _logger;

  public Foo(ILogger<T> logger) =>
    _logger = logger ?? throw new ArgumentNullException(nameof(logger));
}

Unfortunately it's currently impossible to register an open generic type using a factory.

So any variation of this won't work:

// ILogger`T.cs
using Serilog;
public interface ILogger<T> : ILogger { }

// Program.cs
builder.Services.AddTransient(
  typeof(Serilog.ILogger<>), 
  sp => sp.GetRequiredService<Serilog.ILogger>().ForContext<>()
);

Is there some way to do this?

(Without reflection, as in a non-trivial system one expects a very large number of logger resolutions per second. And without the Serilog.Log static class, as I prefer an idiomatic .NET approach.)


Solution

  • It is possible. You need to recreate what MS does with their built-in logger. We need a generic interface to keep the generic type parameter.

    public interface ILogger<out TCategoryName> : ILogger
    {
    }
    

    An interface needs an implementation. This essentially is a wrapper over Serilog main logger.

    public class SerilogCategoryLogger<TCategoryName> : ILogger<TCategoryName>
    {
        private readonly ILogger categoryLogger;
    
        public SerilogCategoryLogger(ILogger logger)
        {
            ArgumentNullException.ThrowIfNull(logger);
    
            categoryLogger = logger.ForContext<TCategoryName>();
        }
    
        public void Write(LogEvent logEvent) => categoryLogger.Write(logEvent);
    }
    

    And service registration bits:

    services.AddSingleton(Log.Logger);
    services.AddSingleton(typeof(ILogger<>), typeof(SerilogCategoryLogger<>));
    

    Notice that this approach requires an already initialized Serilog logger instance. So usually it would look like shown below or standard UseSerilog() called on IHostBuilder in Program.cs.

    services.AddLogging(b => b.AddSerilog(Log.Logger));
    

    Make sure you use Serilog not MS namespace for logging, otherwise you'll end up with errors.

    Simple console app demo showcasing the wrapper:

    public static class Program
    {
        static void Main()
        {
            Log.Logger = new LoggerConfiguration()
                .MinimumLevel.Information()
                .Enrich.FromLogContext()
                .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] [{SourceContext}] {Message:lj}{NewLine}{Exception})")
                .CreateLogger();
    
            var services = new ServiceCollection();
            services.AddLogging(b => b.AddSerilog(Log.Logger));
            services.AddSingleton(Log.Logger);
            services.AddSingleton(typeof(ILogger<>), typeof(SerilogCategoryLogger<>));
            services.AddTransient<TestService>();
            using var serviceProvider = services.BuildServiceProvider();
            var testService = serviceProvider.GetRequiredService<TestService>();
            testService.DoWork();
        }
    }
    
    public interface ILogger<out TCategoryName> : ILogger
    {
    }
    
    public class SerilogCategoryLogger<TCategoryName> : ILogger<TCategoryName>
    {
        private readonly ILogger categoryLogger;
    
        public SerilogCategoryLogger(ILogger logger)
        {
            ArgumentNullException.ThrowIfNull(logger);
    
            categoryLogger = logger.ForContext<TCategoryName>();
        }
    
        public void Write(LogEvent logEvent) => categoryLogger.Write(logEvent);
    }
    
    public class SerilogTestService(ILogger<SerilogTestService> logger)
    {
        public void DoWork() => logger.Information("It is working.");
    }
    

    Final note

    In my personal opinion, I do not recommend this solution unless you have good reasons. As I stated before, there is a reason why logging in ASP.NET Core is done over a common and agreed-upon level of abstraction. Benefits from this abstracted architecture usually outweigh using an explicit logging provider approach, mostly because anyone familiar with the ASP.NET Core tech stack can walk into your source code and understand the logging parts.