Do you use .NET (formerly .NET Core)? If so, you’re probably familiar with the built-in .NET Core LoggerFactory which is in Microsoft.Extensions.Logging. Back when it was introduced, it created a lot of confusion around logging with ASP.NET Core. Several years latter, the dust has settled down, and .NET logging has become somewhat “boring”, which means predictable and consistent.
In this post, we’ll offer you a guide on .NET logging. These are the topics we’ll cover:
Let’s get started.
It is designed as a logging API that developers can use to capture built-in ASP.NET logging as well as for their own custom logging. The logging API supports multiple output providers and is extensible to potentially be able to send your application logging anywhere.
Other logging frameworks like NLog and Serilog have even written providers for it. So you can use the ILoggerFactory and it ends up working sort of like Common.Logging does as a facade above an actual logging library. By using it in this way, it also allows you to leverage all of the power of a library like NLog to overcome any limitations the built-in Microsoft.Extensions.Logging API may have.
Configuring the .NET logging facilities used to be way harder than it is today. In recent versions of .NET (6 and newer), the configuration of web apps has been greatly simplified. For starters, the Startup class is gone. You can have it back if you really want it but by default, it’s no longer there.
Instead, all configuration now lives in the Program.cs class.
Currently, if you start a new ASP.NET web API (making sure you don’t choose to use the minimal API format), your Program.cs class should look like the following:
var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddControllers(); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); var app = builder.Build(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); app.UseAuthorization(); app.MapControllers(); app.Run();
Right after the first line, we’ll add two more lines and that will be the start of our logging configuration:
builder.Logging.ClearProviders(); builder.Logging.AddConsole();
In the example code below, I am showing off 2 different ways to access the LoggerFactory from your MVC controller. Dependency injection can give you the factory or a logger either one.
public class ValuesController : Controller { private ILoggerFactory _Factory; private ILogger _Logger; //set by dependency injection public ValuesController(ILoggerFactory factory, ILogger logger) { _Factory = factory; _Logger = logger; } [HttpGet] public IEnumerable Get() { var loggerFromDI = _Factory.CreateLogger("Values"); _Logger.LogDebug("From direct dependency injection"); loggerFromDI.LogDebug("From dependency injection factory"); } }
OK, so this is where the new logging API quickly becomes a nightmare. Dependency injection works great for accessing it in your MVC controller. But…how do you do logging in a class library that is consumed by this MVC project?
1. You could pass your existing LoggerFactory into every object/method you call (which is a terrible idea).
2. You could create your own LoggerFactory within your class library
This is an option as long as you don’t use any providers like a File provider that can’t have multiple instances writing at the same time. If you are using a lot of different class libraries you would have a lot of LoggerFactory objects running around.
3. Create a centrally located static class or project to hold and wrap around the main LoggerFactory reference
I see this as the best solution here unless you aren’t using any providers that have concurrency issues.
My suggestion is to create a little static helper class that becomes the owner of the LoggerFactory. The class can look something like this below. You can then use this ApplicationLogging class in any code that you want to use logging from and not have to worry about recreating LoggerFactory objects over and over. After all, logging needs to be fast!
public class ApplicationLogging { private static ILoggerFactory _Factory = null; public static void ConfigureLogger(ILoggerFactory factory) { factory.AddDebug(LogLevel.None).AddStackify(); factory.AddFile("logFileFromHelper.log"); //serilog file extension } public static ILoggerFactory LoggerFactory { get { if (_Factory == null) { _Factory = new LoggerFactory(); ConfigureLogger(_Factory); } return _Factory; } set { _Factory = value; } } public static ILogger CreateLogger() => LoggerFactory.CreateLogger(); }
Both NLog and Serilog both have a provider that you can use to extend the functionality of the built-in logging API. They essentially redirect all of the logs being written to the new logging API to their libraries. This gives you all the power of their libraries for the output of your logs while your code is not tied to any particular library. This is similar to the benefit of using Common.Logging.
Logging is an essential part of most non-trivial applications. As such, it shouldn’t be hard to integrate it into your application. The idea behind .NET’s built-in logging capabilities represents exactly that: by making logging a first-class citizen of the framework, friction is greatly reduced.
Back in the day, a great option when it came to .NET logging was to simply use NLog or Serilog and don’t even worry about the new logging API. Even though the built-in logging capabilities are now easier than ever to use, the advice still remains. If you want to capture the built-in ASP.NET logging, you can plugin the NLog/Serilog provider and it will map those messages over. By doing it this way, you can use a different logging library directly and you don’t have to even think about LoggerFactory even existing.
If you would like to be a guest contributor to the Stackify blog please reach out to [email protected]