一个“简单”的解决方案是注入
ILoggerFactory
并调用
CreateLogger
。类别名称是一个参数,因此可以直接提供。
然而,注入
ILogger<T>
通常是首选模式,并且切换到注入
ILoggerFactory
可能是一个不轻松的任务。这个解决方案有助于避免这种情况。
这个解决方案有3个部分:
1. 可以用于标记类或结构体的类别名称的自定义属性。
2. 自定义
ILoggerFactory
实现,如果存在,则使用属性值替换模糊的类型名称。
3. 用于注册自定义
ILoggerFactory
实现的扩展方法。
感谢@Taco タコス的测试,我们知道所涉及的混淆器并没有尝试在字符串文字中查找类型名称并在那里混淆它。这意味着
nameof
可以为属性中的类别名称提供有意义的值。
第一步,我们需要一个属性。它没什么特别之处,但是这就是它。
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
public class LoggerCategoryAttribute : Attribute
{
public LoggerCategoryAttribute(string name)
{
Name = name;
}
public string Name { get; }
}
这个属性可以应用于作为ILogger<T>
中的T
的每个类或结构体。它甚至不必被注入到那个T
的构造函数中;它可以在任何地方使用。
自定义日志记录器工厂
接下来,我们将创建一个自定义的ILoggerFactory
实现,它包装了一个真正的实现。真正的日志记录器工厂可以是默认的LoggerFactory
,也可以是其他日志记录框架提供的实现。
CreateLogger
方法接收一个categoryName
参数,该参数可能是完全限定类型名称,但它也可以是其他任何内容。如果categoryName
是类型名称,并且Type.GetType
能够找到它的Type
实例,则CreateLogger
查找LoggerCategory
属性以获取名称,并将其分配给categoryName
(如果它不为null/empty)。否则,它会保留categoryName
不变。
其他方法只是简单地转发到包装的ILoggerFactory
实现。
public sealed class RecategorizingLoggerFactory : ILoggerFactory
{
private readonly ILoggerFactory _originalLoggerFactory;
public RecategorizingLoggerFactory(ILoggerFactory originalLoggerFactory)
{
_originalLoggerFactory = originalLoggerFactory;
}
public void AddProvider(ILoggerProvider provider) => _originalLoggerFactory.AddProvider(provider);
public ILogger CreateLogger(string categoryName)
{
Type? type = Type.GetType(categoryName);
if (type is not null)
{
var attribute = type.GetCustomAttribute<LoggerCategoryAttribute>();
if (!string.IsNullOrEmpty(attribute?.Name))
categoryName = attribute.Name;
}
return _originalLoggerFactory.CreateLogger(categoryName);
}
public void Dispose() => _originalLoggerFactory.Dispose();
}
注意:此类是sealed
的,以避免关于在Dispose
中调用GC.SuppressFinalize
的分析消息。
DI注册
最后,我们需要为依赖注入注册自定义日志记录器工厂。
如果已经有一个ILoggerFactory
注册,我们不能使用Add
或TryAdd
,因为前者会抛出异常,后者将不执行任何操作。相反,我们需要找到现有的注册并直接替换它。由于IServiceCollection
继承了IList<ServiceDescriptor>
,因此可以直接替换服务描述符。
如果没有ILoggerFactory
注册,我们就添加一个新的注册,并创建一个新的LoggerFactory
来包装,因为我们的自定义日志记录器工厂仍然需要包装实际创建日志记录器的某些内容。
public static IServiceCollection InjectRecategorizingLoggerFactory(this IServiceCollection services)
{
if (services is null)
throw new ArgumentNullException(nameof(services));
static int FindIndex(IList<ServiceDescriptor> list, Func<ServiceDescriptor, bool> predicate)
{
for (int i = 0; i < list.Count; ++i)
{
if (predicate(list[i]))
return i;
}
return -1;
}
int loggerFactoryIndex = FindIndex(services, sd => sd.ServiceType == typeof(ILoggerFactory));
Func<IServiceProvider, object> serviceFactory;
ServiceLifetime lifetime;
if (loggerFactoryIndex >= 0)
{
var oldServiceDescriptor = services[loggerFactoryIndex];
lifetime = oldServiceDescriptor.Lifetime;
if (oldServiceDescriptor.ImplementationFactory is not null)
{
serviceFactory = oldServiceDescriptor.ImplementationFactory;
}
else if (oldServiceDescriptor.ImplementationInstance is ILoggerFactory oldLoggerFactory)
{
serviceFactory = sp => oldLoggerFactory;
}
else if (oldServiceDescriptor.ImplementationType is Type implementationType)
{
serviceFactory = sp =>
{
var loggerProviders = sp.GetServices<ILoggerProvider>();
var filterOption = sp.GetRequiredService<IOptionsMonitor<LoggerFilterOptions>>();
var options = sp.GetService<IOptions<LoggerFactoryOptions>>();
try
{
return ActivatorUtilities.CreateInstance(
sp,
implementationType,
new object[] { loggerProviders, filterOption, options! });
}
catch
{
return new LoggerFactory(loggerProviders, filterOption, options);
}
};
}
else
{
throw new InvalidOperationException("Invalid service descriptor encountered for ILoggerFactory.");
}
}
else
{
lifetime = ServiceLifetime.Singleton;
serviceFactory = sp => new LoggerFactory(
sp.GetServices<ILoggerProvider>(),
sp.GetRequiredService<IOptionsMonitor<LoggerFilterOptions>>(),
sp.GetService<IOptions<LoggerFactoryOptions>>());
}
var newServiceDescriptor = new ServiceDescriptor(
typeof(ILoggerFactory),
sp => new RecategorizingLoggerFactory((ILoggerFactory)serviceFactory(sp)),
lifetime);
if (loggerFactoryIndex >= 0)
{
services[loggerFactoryIndex] = newServiceDescriptor;
}
else
{
services.Add(newServiceDescriptor);
}
return services;
}
关于上述代码的一些要点:
虽然可以通过重新排列来进行一次loggerFactoryIndex >= 0
测试,但我进行了两次测试,因为我只想实例化newServiceDescriptor
一次,因为它非常重要,而且所有东西都旨在导致该点。
如果已经注册了除默认LoggerFactory
之外的其他ILoggerFactory
实现,并且仅当使用implementationType
而不是implementationInstance
或implementationFactory
(这些是AddSingleton
参数名称)进行注册时,ActivatorUtilities.CreateInstance
可能会失败。此代码包含了一种适度的尝试,以避免硬编码LoggerFactory
。可以做更多工作来提供更好的支持,以包装其他记录器工厂。我将其留给读者作为练习。
我使用“BANG:”注释来证明使用null-forgiving运算符的合理性。我发现,在代码审查中,一旦其他人的眼睛被吸引,这通常会以各种方式带来改进,甚至完全不需要使用该运算符。当它无法重写时,它可以帮助指出当合理性已经不存在时,NullReferenceException
的潜在原因。
除非使用反射将所有私有字段置空,否则不应该为“无效的服务描述符”而得到异常,并且ServiceDescriptor
确保正确构造。它主要用于明确分配分析,如其中一条注释中所述。
为了完整起见,也许还有一些幽默,这里是一些将该属性应用于HomeController
类的示例。
[LoggerCategory(nameof(HomeController))]
[LoggerCategory("HomeController")]
[LoggerCategory("Home")]
[LoggerCategory($"MyApplication.Controllers.{nameof(HomeController)}")]
[LoggerCategory($"{nameof(MyApplication)}.{nameof(MyApplication.Controllers)}.{nameof(HomeController)}")]
nameof
,它也会被混淆吗?我特别想知道[SomeAttribute(nameof(ThisClass))] class ThisClass
。 - madreflection