This may be somewhat related to Pass ILogger or ILoggerFactory to constructors in AspNet Core?, however this is specifically about Library Design, not about how the actual application that uses those libraries implement its logging.
I am writing a .net Standard 2.0 Library that will be installed via Nuget, and to allow people using that Library to get some debug info, I'm depending on Microsoft.Extensions.Logging.Abstractions to allow a standardized Logger to be injected.
However, I'm seeing multiple interfaces, and sample code on the web sometimes uses ILoggerFactory
and creates a logger in the ctor of the class. There's also ILoggerProvider
which looks like a read-only version of the Factory, but implementations may or may not implement both interfaces, so I'd have to pick. (Factory seems more common than Provider).
Some code I've seen uses the non-generic ILogger
interface and might even share one instance of the same logger, and some take an ILogger<T>
in their ctor and expect the DI container to support open generic types or explicit registration of each and every ILogger<T>
variation my library uses.
Right now, I do think that ILogger<T>
is the right approach, and maybe a ctor that doesn't take that argument and just passes a Null Logger instead. That way, if no logging is needed, none is used. However, some DI containers pick the largest ctor and thus would fail anyway.
I'm curious of what I'm supposed to be doing here to create the least amount of headache for users, while still allowing proper logging support if desired.
ILogger: is responsible to write a log message of a given Log Level. ILoggerFactory: you can register one or more ILoggerProvider s with the factory, which in turn uses all of them to create an instance of ILogger . ILoggerFactory holds a collection of ILoggerProviders .
The responsibility of the ILogger interface is to write a log message of a given log level and create logging scopes. The interface itself only exposes some generic log methods which are then used by “external” extension methods like LogInformation or LogError .
ILoggerFactory is a factory interface that we can use to create instances of the ILogger type and register logging providers. It acts as a wrapper for all the logger providers registered to it and a logger it creates can write to all the logger providers at once.
But to my surprise, Serilog is using it's own ILogger interface - bummer!
We have 3 interfaces: ILogger
, ILoggerProvider
and ILoggerFactory
. Let's look at the source code to find out their responsibilities:
ILogger: is responsible to write a log message of a given Log Level.
ILoggerProvider: is responsible to create an instance of ILogger
(you are not supposed to use ILoggerProvider
directly to create a logger)
ILoggerFactory: you can register one or more ILoggerProvider
s with the factory, which in turn uses all of them to create an instance of ILogger
. ILoggerFactory
holds a collection of ILoggerProviders
.
In the example below, we are registering 2 providers (console and file) with the factory. When we create a logger, the factory uses both of these providers to create an instance of Logger
:
ILoggerFactory factory = new LoggerFactory().AddConsole(); // add console provider
factory.AddProvider(new LoggerFileProvider("c:\\log.txt")); // add file provider
Logger logger = factory.CreateLogger(); // creates a console logger and a file logger
So the logger itself, is maintaining a collection of ILogger
s, and it writes the log message to all of them. Looking at Logger source code we can confirm that Logger
has an array of ILoggers
(i.e. LoggerInformation[]
), and at the same time it is implementing ILogger
interface.
MS documentation provides 2 methods for injecting a logger:
1. Injecting the factory:
public TodoController(ITodoRepository todoRepository, ILoggerFactory logger) { _todoRepository = todoRepository; _logger = logger.CreateLogger("TodoApi.Controllers.TodoController"); }
creates a Logger with Category = TodoApi.Controllers.TodoController.
2. Injecting a generic
ILogger<T>
:public TodoController(ITodoRepository todoRepository, ILogger<TodoController> logger) { _todoRepository = todoRepository; _logger = logger; }
creates a logger with Category = fully qualified type name of TodoController
In my opinion, what makes the documentation confusing is that it does not mention anything about injecting a non-generic, ILogger
. In the same example above, we are injecting a non-generic ITodoRepository
and yet, it does not explain why we are not doing the same for ILogger
.
According to Mark Seemann:
An Injection Constructor should do no more than receiving the dependencies.
Injecting a factory into the Controller is not a good approach, because it is not Controller's responsibility to initialize the Logger (violation of SRP). At the same time injecting a generic ILogger<T>
adds unnecessary noise. See Simple Injector's blog for more details: What’s wrong with the ASP.NET Core DI abstraction?
What should be injected (at least according to the article above) is a non-generic ILogger
, but then, that's not something that Microsoft's Built-in DI Container can do, and you need to use a 3rd party DI Library. These two documents explain how you can use 3rd party libraries with .NET Core.
This is another article by Nikola Malovic, in which he explains his 5 laws of IoC.
Nikola’s 4th law of IoC
Every constructor of a class being resolved should not have any implementation other than accepting a set of its own dependencies.
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