'Should I take ILogger, ILogger<T>, ILoggerFactory or ILoggerProvider for a library?
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.
Solution 1:[1]
Definition
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.
Dependency Injection
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.
Solution 2:[2]
Those are all valid except for ILoggerProvider
. ILogger
and ILogger<T>
are what you're supposed to use for logging. To get an ILogger
, you use an ILoggerFactory
. ILogger<T>
is a shortcut to get a logger for a particular category (shortcut for the type as the category).
When you use the ILogger
to perform logging, each registered ILoggerProvider
gets a chance to handle that log message. It's not really valid for consuming code to call into the ILoggerProvider
directly.
Solution 3:[3]
The ILogger<T>
was the actual one that is made for DI. The ILogger<T>
came in order to help implement the factory pattern much more easily, instead of you writing on your own all the DI and Factory logic, that was one of the smartest decisions in ASP.NET Core
You can choose between:
ILogger<T>
if you have a need to use factory and DI patterns in your code or you could use the ILogger
, to implement simple logging with no DI needed.
Given that, the ILoggerProvider
is just a bridge to handle each of the registered log's messages. There is no need to use it, as it does not effect anything that you should intervene in code. It listens to the registered ILoggerProvider
and handles the messages. That's about it.
Solution 4:[4]
When writing a library, ILoggerFactory
or ILoggerFactory<T>
is the way to go.
Why?
As a library author, you may care about:
- The content of a message
- The severity of a message
- The category/class/grouping of a message
You may not care about:
- Which logging library a consumer uses
- Whether a logging library is provided at all
When I write libraries:
I write classes in such a way that I control the content and severity of messages (and sometimes the category) while allowing the consumer to choose whatever logging implementation they desire or none at all if they so choose.
Examples
Non-generic class
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
public class MyClass
{
private readonly ILogger _logger;
public MyClass(
..., /* required deps */
..., /* other optional deps */
ILoggerFactory? loggerFactory)
{
_logger = loggerFactory?.CreateLogger<MyClass>()
?? NullLoggerFactory.Instance.CreateLogger<MyClass>();
}
}
Generic class
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
public class MyClass<T>
{
private readonly ILogger<T> _logger;
public MyClass<T>(
..., /* required deps */
..., /* other optional deps */
ILoggerFactory? loggerFactory)
{
_logger = loggerFactory?.CreateLogger<T>()
?? NullLoggerFactory.Instance.CreateLogger<T>();
}
}
Now you can:
- Use the full MS ILogger interface to do all your logging, not caring if there really is a logger at all
- Substitute the generic
CreateLogger<T>()
for the non-genericCreateLogger("")
if you need to control the category.
For the grumbles:
- Yes, you could use
ILogger
, orILogger<T>
in the constructor if you don't care about the category, but I'm proposing this as the most universal/generic way, that gives you the most options without stepping on the consumer. - The consumer can still override the category with the configuration of the log factory or their logger implementation.
- You're not necessarily initializing anything by accepting a log factory, It's up to the DI container configuration/consumer
- The null logger doesn't count as overhead in my book since we're using a single instance
- The consumer can pass in a NullLoggerFactory, if they want to
- And if you're really overkill, you can have a library configuration setting that (with a modification to the constructor) will enable/disable logging for the library (conditionally force the NullLogger)
Solution 5:[5]
Sticking to the question, I believe ILogger<T>
is the right option, considering downside of other options:
- Injecting
ILoggerFactory
force your user to give away the control of the mutable global logger factory to your class library. Moreover, by acceptingILoggerFactory
your class now can write to log with any arbitrary category name withCreateLogger
method. WhileILoggerFactory
is usually available as a singleton in DI container, I as a user would doubt why any library would need to use it. - While the method
ILoggerProvider.CreateLogger
looks like it, it is not intended for injection. It is used withILoggerFactory.AddProvider
so the factory can create aggregatedILogger
that writes to multipleILogger
created from each registered providers. This is clear when you inspect the implementation ofLoggerFactory.CreateLogger
- Accepting
ILogger
also looks like the way to go, but it is impossible with .NET Core DI. This actually sounds like the reason why they needed to provideILogger<T>
at the first place.
So after all, we have no better choice than ILogger<T>
, if we were to choose from those classes.
Another approach would be to inject something else that wraps non-generic ILogger
, which in this case should be non-generic one. The idea is that by wrapping it with your own class, you take full control of how user could configure it.
Solution 6:[6]
For library design, good approach would be:
Do not force consumers to inject logger to your classes. Simply create another constructor passing NullLoggerFactory.
class MyClass { private readonly ILoggerFactory _loggerFactory; public MyClass():this(NullLoggerFactory.Instance) { } public MyClass(ILoggerFactory loggerFactory) { this._loggerFactory = loggerFactory ?? NullLoggerFactory.Instance; } }
Limit number of categories which you use when you create loggers to allow consumers configure logs filtering easily.
this._loggerFactory.CreateLogger(Consts.CategoryName)
Solution 7:[7]
The default approach is meant to be ILogger<T>
. This means that in the log the logs from the specific class will be clearly visible because they will include the full class name as the context. For example if the full name of your class is MyLibrary.MyClass
you will get this in the log entries created by this class. For example:
MyLibrary.MyClass:Information: My information log
You should use the ILoggerFactory
if you want to specify your own context. For example that all the logs from your library have the same log context instead every class. For example:
loggerFactory.CreateLogger("MyLibrary");
And then the log will look like this:
MyLibrary:Information: My information log
If you do that in all classes then the context will be just MyLibrary for all classes. I imagine you would want to do that for a library if you don't want to expose the inner class structure in the logs.
Regarding the optional logging. I think you should always require the ILogger or ILoggerFactory in the constructor and leave it to the consumer of the library to turn it off or provide a Logger that does nothing in the dependency injection if they don't want logging. It is very easy to turn of the logging for a specific context in the configuration. For example:
{
"Logging": {
"LogLevel": {
"Default": "Warning",
"MyLibrary": "None"
}
}
}
Solution 8:[8]
I would prefer to keep it simple and inject the non generic ILogger
This seems to be non-default behavior - but is easily wired up with the following:
services.AddTransient(s => s.GetRequiredService<ILoggerFactory>().CreateLogger(""));
Solution 9:[9]
This (injecting a ILogger into a constructor and calling base that needs ILogger) is only possible because ILogger<T>
is covariant and is just a wrapperclass that has a dependency on the LoggerFactory
. If it wasn't covariant you would definitely be using the ILoggerFactory
or ILogger
. But ILogger
should be discarded because you could be logging to any category and you would loose all context regarding the logging. I think ILoggerFactory
would be the best way to go and then use CreateLogger<T>()
to create a ILogger<T>
inside you class. This way you really have a nice solution because as a developer you would really like to align the category with your classes to jump right to the code and not to some unrelated derived class. (You could add linesnumbers.) You can also let your derived classes use the logger that is defined by the baseclass but then also where to start looking for the source code? Besides this I can imagine you might also have any other ILogger
with special purpose category (sub)names in the same class. Nothing is preventing you from have multiple ILogger's in such a case ILoggerFactory
just looks cleaner.
My preferred solution is to inject ILoggerFactory
call CreatLogger<T>
where T
is the current class and assign it to a private readonly ILogger<T> logger
Or in case you already inject IServiceProvider
you can call serviceProvider.GetService<ILogger<T>>();
Note that injecting the IServiceprovider
is the service-locator pattern and is considered an antipattern. Injecting the ILoggerFactory
is also a variation of the service-locator pattern.
Solution 10:[10]
This added, because a lot of other search results link here.
If you have a ILoggerFactory
and you need to provide an ILogger<Whatever>
, this is the way to create it: new Logger<Whatever>(myLoggerFactory)
Solution 11:[11]
I've used this simple technique to inject Ilogger into my legacy classes that require a basic ILogger
services.AddSingleton<ILogger>(provider => provider.GetService<ILogger<Startup>>());
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow