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");
}
}
}
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.
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);
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With