Should I take ILogger, ILoggerT, ILoggerFactory or ILoggerProvider for a library?
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.
Should private class member use its own ILogger instance?
You have a few ways of doing this.
- If you'd like
Qux
to be an implementation detail ofFoo
as it is now, then:
public Foo(IBar bar, ILoggerFactory loggerFactory)
{
_logger = loggerFactory.CreateLogger<Foo>();
_qux = new Qux(loggerFactory.CreateLogger<Qux>());
}
- If you want to use DI more correctly, and can depend on users correctly using your library (as suggested by @Llama):
public Foo(IBar bar, Qux qux, ILogger<Foo> logger)
{
_logger = logger;
_qux = qux;
}
// inside your library where you can see the internal Qux
public static IServiceCollection InjectMyLibraryServices(this IServiceCollection services)
{
// ...
services.AddScoped<IQux, Qux>();
services.AddScoped<IFoo, Foo>();
}
- You can get rid of DI for
Qux
and just get the logger (as mentioned by @canton7):
public Foo(IBar bar, ILogger<Foo> logger)
{
_logger = logger;
_qux = new Qux();
}
internal class Qux
{
private readonly ILogger _logger = LogManager.GetLogger(typeof(Qux));
}
Side note: this last approach represents the Service Locator anti-pattern and hides the logging dependency. Use only if you understand the pros and cons of doing things this way. I personally wouldn't recommend this approach.
Related Topics
Compile-Time and Runtime Casting C#
C# Regular Expressions, String Between Single Quotes
Hook on Default "Paste" Event of Winforms Textbox Control
Pass a Value from One Form to Another
.Net:How to Get the Type of a Null Object
Is This Thread.Abort() Normal and Safe
How to Quickly Up-Cast Object[,] into Double[,]
C# Arrow Key Input for a Console App
Strange Behaviour of Console.Readkey() with Multithreading
Converting ASP.NET MVC Razor @Helper Function into a Method of a Helper Class
Trying to Debug Windows Store App from Dump Files
String Interpolation with Format Variable
Incorrectly Aligned or Overlapped by a Non-Object Field Error
Groupby on Complex Object (E.G. List<T>)