Should I take ILogger, ILogger<T>, ILoggerFactory or ILoggerProvider for a library?

56,973

Solution 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 ILoggerProviders 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 ILoggers, 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

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

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

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-generic CreateLogger("") if you need to control the category.

For the grumbles:

  • Yes, you could use ILogger, or ILogger<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

For library design, good approach would be:

  1. 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;
        }
    }
    
  2. Limit number of categories which you use when you create loggers to allow consumers configure logs filtering easily.

    this._loggerFactory.CreateLogger(Consts.CategoryName)
    
Share:
56,973

Related videos on Youtube

Michael Stum
Author by

Michael Stum

The same thing we do every night, Pinky. Try to take over the world! Full-Stack Developer on Stack Overflow Enterprise, working to make our little corner of the Internet better for all of us.

Updated on April 22, 2022

Comments

  • Michael Stum
    Michael Stum about 2 years

    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.

    • Hooman Bahreini
      Hooman Bahreini over 2 years
      It seems like Microsoft injects ILoggerFactory for Library design... however as I explained below I don't think injecting a factory is a good solution.
  • Michael Stum
    Michael Stum almost 6 years
    Thanks. This makes me think that an ILoggerFactory would be great as an easy way to wire up DI for consumers by only having 1 dependency ("Just give me a Factory and I'll create my own loggers"), but would prevent using an existing logger (unless the consumer uses some wrapper). Taking an ILogger - generic or not - allows the consumer to give me a logger they specially prepared, but makes the DI setup potentially a lot more complex (unless a DI container that supports Open Generics is used). Does that sound correct? In that case, I think I'll go with the factory.
  • Damien_The_Unbeliever
    Damien_The_Unbeliever almost 6 years
    @MichaelStum - I'm not sure I follow your logic here. You're expecting your consumers to use a DI system but then are wanting to take over and manually manage dependencies within your library? Why would that seem the right approach?
  • Michael Stum
    Michael Stum almost 6 years
    @Damien_The_Unbeliever That is a good point. Taking a factory seems odd. I think that instead of taking an ILogger<T>, I'll take an ILogger<MyClass> or even just ILogger instead - that way, the user can wire it up with only a single registration, and without requiring open generics in their DI container. Leaning towards the non-generic ILogger, but will experiment a lot over the weekend.
  • Matthew Hostetler
    Matthew Hostetler over 5 years
    What does ILogger vs ILogger<T> have to do with DI? Either would be injected, no?
  • bokibeg
    bokibeg almost 5 years
    Your answer and Steven's article are most correct and most depressing at the same time.
  • Abhishek Avadhoot
    Abhishek Avadhoot about 4 years
    It's actually ILogger<TCategoryName> in MS Docs. It derives from ILogger and adds no new functionality except the class name to log. This also provides a unique type so the DI injects the correctly named logger. Microsoft extensions extend the non-generic this ILogger.
  • Jack Miller
    Jack Miller over 3 years
    If AddConsole is not found, make sure that NuGet "Microsoft.Extensions.Logging.Console" is installed, your source file includes using Microsoft.Extensions.Logging;. Then finally try ILoggerFactory factory = LoggerFactory.Create(builder => builder.AddConsole());
  • Jack Miller
    Jack Miller over 3 years
    If LoggerFileProvider is not found, use FileLoggerProvider from NuGet "NReco.Logging.File".
  • Alan Macdonald
    Alan Macdonald over 3 years
    Sorry which article and specifically what part of text are you claiming explains why ILogger<T> being injected is a bad thing? I agree about not injecting a factory in general but why is ILogger<T> worse than ILogger? T is typically the consuming class that is taking the dependency
  • Hooman Bahreini
    Hooman Bahreini over 3 years
    Quoting Steven's comments on this question: "Injection of a ILogger<T> is just noise to the consumer that can lead to accidental errors when a wrong T is specified and it complicates testing."... "Injecting ILoggerFactory and ILogger<T> is a terrible idea, and as I see it, the only reason Microsoft is doing this (and promoting it publicly) is because their built-in container lacks the possibility to map a non-generic interface to a generic implementation."
  • Wouter
    Wouter over 3 years
    Why giving this advice? Logging to the "default" logger just disables all configuration and filtering you can do on categories.
  • SheepReaper
    SheepReaper almost 3 years
    Yes, but where does Logger come from? You'd have to choose a logging library (even if it's MS) in order to have access to that class. If you already have an ILoggerFactory and then use some random Logger class, that logger class has to have a constructor that accepts an ILoggerFactory. This only works if you know or control which log library is in use (or are just lucky). ILoggerFactory has .CreateLogger<T>() which actually uses the ILoggerFactory's Logger implementation class without you having/caring to know what it actually is.
  • Iain Ballard
    Iain Ballard almost 3 years
    It comes from Microsoft.Extensions.Logging, as described in the original question.
  • Zoltán Tamási
    Zoltán Tamási over 2 years
    Because maybe someone doesn't care about features like that. Spent 13 years without filtering on categories. Also, more advanced DI containers have the ability to resolve ILogger in a context-aware way to get an ILogger<[Class-Where-I'm-Injected]> at runtime, which I'd prefer most of the times over this not too nice "self-generic" bolierplate. Of course this is just my opinion.
  • Admin
    Admin over 2 years
    Your answer could be improved with additional supporting information. Please edit to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers in the help center.
  • SuperPoney
    SuperPoney over 2 years
    I completly doesn't understand a word about those injections things, but with you explanation it works ! Thank you very much.
  • Wouter
    Wouter over 2 years
    Or use loggerFactory.CreateLogger<T>(); which does exactly that.
  • Wouter
    Wouter over 2 years
    @ZoltánTamási But that is exactly what you get with ILogger<Class-Where-I'm-Injected>.
  • Iain Ballard
    Iain Ballard over 2 years
    Thanks. However, ILoggerFactory.CreateLogger creates an ILogger instance, not ILogger<T>. If you're in fresh code it probably doesn't matter, but I think I was trying to shim some old code. If there's a way to get the ILogger<T> directly from the factory, that would be great.
  • Andrew Harry
    Andrew Harry about 2 years
    Because I prefer to keep it simple. Simple is ILogger and not worrying about categories etc.
  • Sam Holder
    Sam Holder about 2 years
    If you have a non generic ILogger, then you can only log in that category in your library can't you? isn't the point of taking ILogger<T> or ILoggerFactory that both allow you to log in a category specific to your library, so that collecting those logs could be excluded from collecting the logs of the application. If you just pass ILogger it'll all log in the same place won't it?
  • Hooman Bahreini
    Hooman Bahreini about 2 years
    @SamHolder: you want your non generic logger to know the category... so you want your DI container to map a non-generic interface to a generic implementation.
  • ScottyMacDev
    ScottyMacDev almost 2 years
    I liked the article but I have a key question that I cannot seem to resolve. I created my own ILoggerProvider to provide for a specific implementation of using a sproc to log to SQL Server in our environment. In what part of the DI chain can I call AddProvider on the ILoggerFactory in order to ensure that every ILogger the factory produces has the ILoggerProvider implementation added to it?