我可以更改给ILogger<T>实例的类别名称吗?

3

我有一个自定义的ILogger实现和一个ILoggerProvider,我注意到ILoggerProvider.CreateLogger中的categoryName参数似乎是Type.FullName:

获取类型的完全限定名称,包括它的命名空间但不包括其程序集。

然而,我的生产代码被混淆了,虽然代码库中不是所有的消费者都被混淆,但大多数都被混淆了,它们的名称变得无关紧要(例如),但这就是混淆的本质。


回答 @madreflection 的评论

我花了一些时间创建了一个设置来测试 @madreflection 对 nameof 表达式的好奇心:

public class SampleAttribute : Attribute {
    public string Name { get; set; }
    public SampleAttribute(string name) =>
        Name = name;
}
...
[Sample(nameof(InvoiceService))]
[Obfuscation(Exclude = false)]
public class TestClass {
    public TestClass(ILogger<TestClass> logger) {
        var attribute = GetType().GetCustomAttribute<SampleAttribute>();
        logger.LogInformation($"inline: {nameof(TestClass)}");
        logger.LogInformation($"attribute: {attribute.Name}");
    }
}

输出结果显示 nameof 表达式未被混淆:

inline: TestClass
attribute: TestClass

现在,我很好奇他们的意图是什么!


考虑到这一点,是否有文档记录的方法可以将类别名称更改为常量值?


1
如果你使用nameof,它也会被混淆吗?我特别想知道[SomeAttribute(nameof(ThisClass))] class ThisClass - madreflection
如果一种未记录的技术仅依赖于已记录的细节,您会感兴趣吗? - madreflection
@madreflection,没错 :) 只需要一种方法仍然能够在模糊状态下知道它是什么。 - Hazel へいぜる
1
今天下班后我会写出来。 - madreflection
@madreflection 暂时还不行,哈哈,不过我的脑袋正在转动。我很可能现在缺少一个关键点。我期待着看到你的解决方案! - Hazel へいぜる
显示剩余4条评论
1个回答

3
一个“简单”的解决方案是注入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注册,我们不能使用AddTryAdd,因为前者会抛出异常,后者将不执行任何操作。相反,我们需要找到现有的注册并直接替换它。由于IServiceCollection继承了IList<ServiceDescriptor>,因此可以直接替换服务描述符。

如果没有ILoggerFactory注册,我们就添加一个新的注册,并创建一个新的LoggerFactory来包装,因为我们的自定义日志记录器工厂仍然需要包装实际创建日志记录器的某些内容

public static IServiceCollection InjectRecategorizingLoggerFactory(this IServiceCollection services)
{
    if (services is null)
        throw new ArgumentNullException(nameof(services));

    // List<T> has FindIndex(), but IList<T> does not, so it's inline as a local function here.
    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));

    // These two variables will be assigned eventually.  Initializing them here prevents definite
    // assignment analysis from detecting that all branches of execution below have assigned
    // them, and with such large blocks, that can be a real concern, so they are intentionally
    // not initialized.
    Func<IServiceProvider, object> serviceFactory;
    ServiceLifetime lifetime;

    if (loggerFactoryIndex >= 0)
    {
        var oldServiceDescriptor = services[loggerFactoryIndex];
        lifetime = oldServiceDescriptor.Lifetime;

        // The service could be registered in one of three ways.  The first two are easy to support,
        // but the third can be fragile.  See the notes below the code for details.
        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
                {
                    // BANG: ActivatorUtilities.CreateInstance is not annotated to accept null
                    //       elements in the 'arguments' parameter, but the constructor we want
                    //       to use takes a nullable IOptions<LoggerFactoryOptions>.  The method
                    //       signature is too restrictive, and passing null works where null is
                    //       allowed by the constructor.
                    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;

        // No ILoggerFactory was registered.  Default to wrapping LoggerFactory.
        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而不是implementationInstanceimplementationFactory(这些是AddSingleton参数名称)进行注册时,ActivatorUtilities.CreateInstance可能会失败。此代码包含了一种适度的尝试,以避免硬编码LoggerFactory。可以做更多工作来提供更好的支持,以包装其他记录器工厂。我将其留给读者作为练习。

  • 我使用“BANG:”注释来证明使用null-forgiving运算符的合理性。我发现,在代码审查中,一旦其他人的眼睛被吸引,这通常会以各种方式带来改进,甚至完全不需要使用该运算符。当它无法重写时,它可以帮助指出当合理性已经不存在时,NullReferenceException的潜在原因。

  • 除非使用反射将所有私有字段置空,否则不应该为“无效的服务描述符”而得到异常,并且ServiceDescriptor确保正确构造。它主要用于明确分配分析,如其中一条注释中所述。


为了完整起见,也许还有一些幽默,这里是一些将该属性应用于HomeController类的示例。

// Confirmed, nameof works.
[LoggerCategory(nameof(HomeController))]

// But you don't have to use nameof if you don't wnat to.
[LoggerCategory("HomeController")]

// Who needs "Controller" anyway?  The *Name* of the controller is "Home", after all!
[LoggerCategory("Home")]

// You can include a hard-coded namespace so it looks like the original,
// unobfuscated type name.
[LoggerCategory($"MyApplication.Controllers.{nameof(HomeController)}")]

// A more robust version of the former.  As long as the depth of namespace
// doesn't change, this will capture renaming of *any* of the parts.
[LoggerCategory($"{nameof(MyApplication)}.{nameof(MyApplication.Controllers)}.{nameof(HomeController)}")]

抱歉我还没接受答案,因为我还没有实践运行所有的东西。但是,我想花点时间感谢你写了这么详细的实现和答案! - Hazel へいぜる
@Tacoタコス:有什么问题需要我回答吗? - madreflection
不,我完全理解了,只是由于当前的工作量,忘记回来接受答案了。我为延迟感到抱歉,非常感谢您给出详尽有用的答案,并提醒我需要接受它! - Hazel へいぜる

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接