4

I have a .Net 6 Core Minimal API set up to use SeriLog to log to MS SQL Server. In my class library I have managed to get logging working with SeriLog ONLY IF I modify the constructors in my class libraries. I am trying to avoid modifying the constructors of my class library classes or methods.

In my experience with console apps, if I set up SeriLog in my main Program.cs, then I can use logging in any class in my class library without passing the logger to the constructors. So, I can just use Log.Information("my message") anywhere in the class library and it works. I am trying to achieve the same with my Program.cs in a .Net 6 minimal API project.

I sense it should be possible to do from looking at other questions on this topic. In particular this one in which the answer stated that:

You don't have to do anything in your class library. Only the main application has a composition root (earliest point in an application lifecycle you can set up your object graph).

So following from the above, in my API Program.CS, I have this code (I have indicated what works and what does not in the comments):

    //Configure SeriLog
    builder.Logging.ClearProviders();
    var appSettings = new ConfigurationBuilder()
       .SetBasePath(Directory.GetCurrentDirectory())
       .AddJsonFile("appsettings.json")
       .Build();
    
    var logDB = 
    builder.Configuration.GetSection("ConnectionStrings:Default").Value;
    var sinkOpts = new MSSqlServerSinkOptions { TableName = "Logs" };
    var columnOptions = new ColumnOptions();
    
    var logger = new LoggerConfiguration()
       .MinimumLevel.Override("Microsoft", 
    Serilog.Events.LogEventLevel.Information)
      .WriteTo.MSSqlServer(
          connectionString: logDB,        
          sinkOptions: sinkOpts,
          columnOptions: columnOptions,
          appConfiguration: appSettings
        ).CreateLogger();
    
    builder.Logging.AddSerilog(logger);

    //Depency injection for Class Library method 
    //LoggerTestClass is a class in my Class Library project
    builder.Services.AddScoped<ILoggerTestClass, LoggerTestClass>();

    var app = builder.Build();
    app.ConfigureTestAPI();

The Method "ConfigureTestAPI()" is in a Extension Class that is shown below:

    public static class API_Test
    {

    public static void ConfigureTestAPI(this WebApplication app) 
    //Extension method for app
    {               
        app.MapGet("/test/", GetTest);
        
    }

    private static async Task<IResult> GetTest(int id, 
    ILogger<LoggerTestClass> logger, ILoggerTestClass testClass)
    {
        try
        {
            try
            {
                //This works
                logger.LogInformation("Starting test now");  

                //This does NOT work
                Log.Information("Using Log. directly"); 

                
                testClass.Test();  //Call to class library method

                logger.LogInformation("Test finished");  //Also works
                return Results.Ok("OK");
            }
            catch (Exception ex)
            {
                return Results.Problem(ex.Message);
            }
        }
        catch (Exception ex)
        {
            return Results.Problem(ex.Message);
        }
     }
    }

And finally here is the class with the test method in my class library:

        namespace TestingLib.Testing;
    public class LoggerTestClass : ILoggerTestClass
    {
    
        private Microsoft.Extensions.Logging.ILogger _logger;
    
        public LoggerTestClass(ILogger<LoggerTestClass> logger)
        {
            _logger = logger;
        }
    
        public void Test()
        {
            try
            {
    
               //Does not work
               Log.Information("Test logging from class library using Log.");
    
               //Does not work
               Log.Logger.Information("In Test Class in DLL. Trying loging with [Log.Logger.Information]");
    
               //This works
               _logger.LogInformation("In Test Class in DLL. Trying loging with [_logger.LogInformation]");
            }
            catch (Exception ex)
            {
                Log.Error("An error in class library");
            }
        }
    
    }

TarHalda
  • 1,050
  • 1
  • 9
  • 27
Fritz45
  • 752
  • 1
  • 10
  • 23

2 Answers2

5

I found the problem was a single line of code that was lacking in my API Program.cs file. I needed to add: "Log.Logger = logger;" after setting up the SeriLog logger.

The code for the working solution is below.

In my .NET6 API Program.cs file I have this:

var builder = WebApplication.CreateBuilder(args);

//Configure SeriLog
builder.Logging.ClearProviders();
var appSettings = new ConfigurationBuilder()
    .SetBasePath(Directory.GetCurrentDirectory())
    .AddJsonFile("appsettings.json")
    .Build();

var logDB = builder.Configuration.GetSection("ConnectionStrings:Default").Value;
var sinkOpts = new MSSqlServerSinkOptions { TableName = "Logs" };
var columnOptions = new ColumnOptions
{
    AdditionalColumns = new Collection<SqlColumn>
    {
        new SqlColumn("UserID", SqlDbType.Int),
        new SqlColumn("RunTag", SqlDbType.NVarChar),
        new SqlColumn("CustomType", SqlDbType.NVarChar)
    }
};

var logger = new LoggerConfiguration()
    .MinimumLevel.Override("Microsoft", Serilog.Events.LogEventLevel.Information)
    .WriteTo.MSSqlServer(
        connectionString: logDB,
        sinkOptions: sinkOpts,
        columnOptions: columnOptions,
        appConfiguration: appSettings
    ).CreateLogger();

builder.Logging.AddSerilog(logger);

//NOTE that this is not needed
//builder.Services.AddScoped<TestingLib.Testing.ILoggerTestClass, TestingLib.Testing.LoggerTestClass>();

And then lower down in this file I have this:

Log.Logger = logger; //This was the line I was missing!

var app = builder.Build();

My API Get method that calls into the Class Library now looks like this (if compared to the original one in the question, you will see there is no longer a need to pass ILogger around):

private static async Task<IResult> GetTest(int id)
    {
        try
        {
            try
            {
                string runTag = "In API GetTest(id)";
                string custom = "Calling from API method";                
                Log.Information("{Message}-{RunTag}-{CustomType}", "Message logging in API method", runTag, custom);
                
                //Call to Class Library Method - no need to pass ILogger
                LoggerTestClass testClass = new LoggerTestClass();
                testClass.Test();
               
                return Results.Ok("OK");
            }
            catch (Exception ex)
            {
                return Results.Problem(ex.Message);
            }
        }
        catch (Exception ex)
        {
            return Results.Problem(ex.Message);
        }
    }

And finally, here is the full Test Class in my class library. The class library contains a reference to SeriLog:

using Serilog;

namespace TestingLib.Testing;
public class LoggerTestClass //: ILoggerTestClass
{

    public LoggerTestClass()
    {
        //No need to pass in ILogger to class
    }

    public void Test()
    {
        try
        {
            string runTag = "In Class Library Method)";
            string custom = "Calling class library method Test()";
            Log.Information("{Message}-{RunTag}-{CustomType}", "Message logging in Class Library", runTag, custom);            
        }
        catch (Exception ex)
        {
            Log.Error("An error in class library");
        }
    }

}

I found this works perfectly for my project. Loggging from the class library goes directly into my SQL database with the custom columns etc.

Fritz45
  • 752
  • 1
  • 10
  • 23
1

I would recommend to use Two-stage initialization: https://github.com/serilog/serilog-aspnetcore#two-stage-initialization

This has the benefit of catching and reporting exceptions thrown during set-up of the ASP.NET Core host

So init Log.Logger as early as possible not before the builder.Build()

// usings here

var logger = new LoggerConfiguration()
    .WriteTo.Console(LogEventLevel.Information)
    .CreateLogger();

Log.Logger = logger;

var builder = WebApplication.CreateBuilder(args);

builder.Logging.ClearProviders();
builder.Logging.AddSerilog(logger);
builder.Host.UseSerilog(logger);

SerjG
  • 3,325
  • 3
  • 30
  • 30