这可能与Pass ILogger or ILoggerFactory to constructors in AspNet Core?有些相关,但这特别是关于库设计,而不是关于使用这些库的实际应用程序如何实现其日志记录。
我正在编写一个将通过Nuget安装的.net标准2.0库,并允许使用该库的人获得一些调试信息,我依靠Microsoft.Extensions.Logging.Abstractions来允许注入标准化的Logger。
但是,我看到了多个接口,网上的示例代码有时会使用ILoggerFactory
并在类的ctor中创建一个记录器。还有ILoggerProvider
看起来像Factory的只读版本,但实现可能会也可能不会实现两个接口,所以我必须选择。 (工厂似乎比提供商更常见)。
我见过的一些代码使用了非通用的ILogger
接口,甚至可能共享同一个记录器的一个实例,有些代码在他们的ctor中使用ILogger<T>
并期望DI容器支持开放泛型类型或每个ILogger<T>
的显式注册我的图书馆用的变种。
现在,我确实认为ILogger<T>
是正确的方法,也许是一个不采用该参数而只是通过Null Logger的ctor。这样,如果不需要记录,则不使用任何记录。然而,一些DI容器选择了最大的ctor,因此无论如何都会失败。
我很好奇我应该在这里做什么来为用户创造最少的头痛,同时如果需要仍然允许适当的日志记录支持。
我们有3个接口:ILogger
,ILoggerProvider
和ILoggerFactory
。让我们看看source code,了解他们的责任:
ILogger
:主要职责是编写给定日志级别的日志消息。
ILoggerProvider
:只有一个责任,那就是创造一个ILogger
。
ILoggerFactory
:有2个职责,创建一个ILogger
并添加一个ILoggerProvider
。
请注意,我们可以向工厂注册一个或多个提供程序(控制台,文件等)。当我们使用工厂创建记录器时,它使用所有已注册的提供程序来创建相应的记录器。
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
因此Logger维护一个记录器列表,并将日志消息写入所有记录器。 Looking at Logger source code我们可以确认Logger
有一个ILoggers
数组(即LoggerInformation []),同时它正在实现ILogger
接口。
MS documentation提供了两种注入记录器的方法:
1.注射工厂:
public TodoController(ITodoRepository todoRepository, ILoggerFactory logger) { _todoRepository = todoRepository; _logger = logger.CreateLogger("TodoApi.Controllers.TodoController"); }
使用Category = TodoApi.Controllers.TodoController创建一个Logger。
2.注入通用的
ILogger<T>
:public TodoController(ITodoRepository todoRepository, ILogger<TodoController> logger) { _todoRepository = todoRepository; _logger = logger; }
创建一个具有Category =完全限定类型名称T的记录器
在我看来,令文档混淆的是它没有提到注入非泛型的ILogger
。在上面的相同例子中,我们注入了一个非泛型的ITodoRepository
,但它并没有解释为什么我们不对ILogger
做同样的事情。
据Mark Seemann说:
注入构造函数应该只接收依赖项。
将工厂注入Controller不是一个好方法,因为Controller不负责初始化Logger(违反SRP)。同时注入一个通用的ILogger<T>
会增加不必要的噪音。请参阅史蒂文在Simple Injector blog上的帖子
什么应该注入(至少根据上面的文章)是一个非通用的ILogger
,但是,这不是微软的内置DI容器可以做的事情,你需要使用第三方DI库。
这是Nikola Malovic的another article,他解释了他的IoC法则。
尼古拉的IoC第四定律
除了接受一组自己的依赖项之外,正在解析的类的每个构造函数都不应该具有任何实现。
除了ILoggerProvider之外,这些都是有效的。 ILogger和ILogger <T>是你应该用于记录的东西。要获得ILogger,请使用ILoggerFactory。 ILogger <T>是获取特定类别的记录器的快捷方式(类型的快捷方式作为类别)。
当您使用ILogger执行日志记录时,每个注册的ILoggerProviders都有机会处理该日志消息。使用代码直接调用ILoggerProvider并不是真的有效。
ILogger<T>
是为DI制作的实际的。 ILogger的出现是为了帮助您更轻松地实现工厂模式,而不是您自己编写所有DI和Factory逻辑,这是asp.net核心中最明智的决策之一。
你可以选择:
ILogger<T>
如果您需要在代码中使用工厂和DI模式,或者您可以使用ILogger
,实现简单的日志记录,无需DI。
鉴于此,ILoggerProvider
只是处理每个注册日志消息的桥梁。没有必要使用它,因为它不会影响您应该干预代码的任何内容。它会监听已注册的ILoggerProvider并处理消息。就是这样。
对于图书馆设计,好的方法是:
1.不要强迫消费者向你的课程注入记录器。只需创建另一个传递NullLoggerFactory的ctor。
class MyClass
{
private readonly ILoggerFactory _loggerFactory;
public MyClass():this(NullLoggerFactory.Instance)
{
}
public MyClass(ILoggerFactory loggerFactory)
{
this._loggerFactory = loggerFactory ?? NullLoggerFactory.Instance;
}
}
2.限制在创建记录器时使用的类别数,以允许使用者轻松配置日志过滤。 this._loggerFactory.CreateLogger(Consts.CategoryName)
坚持这个问题,我认为ILogger<T>
是正确的选择,考虑到其他选择的缺点:
ILoggerFactory
会强制您的用户将可变全局记录器工厂的控制权交给您的类库。此外,通过接受ILoggerFactory
,您的类现在可以使用CreateLogger
方法使用任意类别名称写入日志。虽然ILoggerFactory
通常在DI容器中作为单例提供,但我作为用户会怀疑为什么任何库都需要使用它。ILoggerProvider.CreateLogger
看起来像它,它不打算注射。它与ILoggerFactory.AddProvider
一起使用,因此工厂可以创建聚合的ILogger
,写入从每个注册的提供商创建的多个ILogger
。当你检查the implementation of LoggerFactory.CreateLogger
时,这一点很清楚ILogger
也看起来像是要走的路,但是使用.NET Core DI是不可能的。这实际上听起来就像他们需要首先提供ILogger<T>
的原因。毕竟,如果我们要从这些课程中进行选择,我们没有比ILogger<T>
更好的选择。
另一种方法是注入包装非泛型ILogger
的其他东西,在这种情况下应该是非泛型的。我们的想法是,通过使用您自己的类包装它,您可以完全控制用户如何配置它。
默认方法是ILogger<T>
。这意味着在日志中,特定类的日志将清晰可见,因为它们将包含完整的类名作为上下文。例如,如果您的类的全名是MyLibrary.MyClass
,您将在此类创建的日志条目中获得此名称。例如:
MyLibrary.MyClass:信息:我的信息日志
如果要指定自己的上下文,则应使用ILoggerFactory
。例如,来自库的所有日志都具有相同的日志上下文而不是每个类。例如:
loggerFactory.CreateLogger("MyLibrary");
然后日志将如下所示:
MyLibrary:信息:我的信息日志
如果你在所有类中都这样做,那么上下文将只是所有类的MyLibrary。我想如果你不想在日志中公开内部类结构,你会想要为库做这件事。
关于可选的日志记录。我认为你应该始终在构造函数中需要ILogger或ILoggerFactory,并将其保留给库的使用者以将其关闭,或者如果他们不想要记录,则提供在依赖注入中不执行任何操作的Logger。在配置中转换特定上下文的日志记录非常容易。例如:
{
"Logging": {
"LogLevel": {
"Default": "Warning",
"MyLibrary": "None"
}
}
}