I work a lot with legacy applications, upgrading the internals to use the latest architectural approaches. I often encounter older systems that do not have an IoC container, so adding one is the first thing I do. Along with that effort goes the conversion of some random classes into services and loading those services into the container.
The first service I always tackle in these conversions is the logger. It’s typically simple with no business logic, and nicely demonstrates the idea of how to resolve a service from a container (for those team members who are unfamiliar with IoC). It’s also a good first step in getting all the proper references into the projects of a solution so they can use the container. In general, adding a container and converting the logger to use it fits nicely into a single sprint.
A pattern I keep seeing, and one that makes these first few steps of a conversion much more difficult, is using a static variable as a handle to get to the logger. I’ve seen this pattern in many of the instructional articles on the various loggers. This is a bad practice for several reasons.
Static logger instances are difficult to mock in a unit test. This means your logger is always writing logs even during unit tests when they are probably not needed. This slows down the tests and eats up disk space unnecessarily.
Since you cannot mock the logger, this also means you cannot write tests to ensure an error log is written in appropriate situations. Using Mock.Verify() is a great way of ensuring errors are logged properly.
Static loggers cannot be replaced at runtime to allow injection of different loggers. This can be especially important if you are releasing a library for others to use. Define a standard logger interface and log everything to that. You can provide your own built-in logger, but also allow the user to replace that with their own preference.
Most of all, using the default static logger implementation provided by the logging vendor locks you into their interface. This means you cannot hide or change the surface area of the logger. Changing loggers becomes a MUCH bigger effort if the syntax of the new logger changes.
Use a Log Wrapper
A much better alternative is to create a logging interface that does things the way you like, then a wrapper class around your logger which translates those log calls into the proper calls for your logger.
Create an interface with the logging method signatures you like. Then add a class that implements that interface, and make the class wrap your favorite logger. The wrapping class can just pass through any log method calls to your favorite logger so you still get the advantage of using it. However, you still have the option of easily ripping that logger out and replacing it with something else.
It also opens up the possibility of adding more than one logger. The class that implements the interface can easily make calls to 2 real loggers for a single message, or even change its behavior depending on the environment it is living in.
With the advent of Blazor, this logger wrapper approach is even more important. C# code can be shared between the server side and the front end. If you implement a logger that can only run on the server, not in WASM, then your shared objects will break when trying to log data from the browser. Implementing a logger wrapper with an interface allows you to provide different loggers on the front end (i.e. Console.WriteLine(…)) and on the back end, but using the same class.
I used to use Log4Net quite a bit and have that logger in many systems I have built. I am moving to use Serilog more due to its support for writing json objects. I have created wrappers for each of these loggers which share the same interface. This lets me quickly swap one for the other if I edit an older system with Log4Net and want to migrate it to Serilog.
If you are interested and want to save yourself some typing, you can find these wrappers here.