为什么需要 LoggingModule

ASP.NET Core 自带的 ILogger<T> 在控制器里是自动注入的。但项目里往往有非控制器的服务类(领域服务、Repository、事件处理器等),如果也用 Autofac 自己解析,ILogger<T> 默认拿不到——因为 Autofac 不知道 ILogger<T> 怎么构造。

LoggingModule 解决两件事:

  1. 从 Autofac 解析 ILogger<T> 时,按 T 的实际类型提供 logger。
  2. 对属性类型为 ILogger 的可写属性,激活后自动赋值。

实现

using Autofac;
using Autofac.Core;
using Autofac.Core.Registration;
using Microsoft.Extensions.Logging;

public class LoggingModule : Module
{
    private readonly ILoggerFactory _factory;

    public LoggingModule(ILoggerFactory factory) => _factory = factory;

    protected override void AttachToComponentRegistration(
        IComponentRegistryBuilder registry,
        IComponentRegistration registration)
    {
        // 构造函数参数:把 ILogger<T> 重定向到 factory
        registration.Preparing += OnComponentPreparing;

        // 属性注入:把可写的 ILogger 属性激活后赋值
        registration.Activated += InjectLoggerProperties;
    }

    private void OnComponentPreparing(object sender, PreparingEventArgs e)
    {
        var t = e.Component.Activator.LimitType;
        e.Parameters = e.Parameters.Union(new[]
        {
            new ResolvedParameter(
                (p, i) => p.ParameterType.IsGenericType
                           && p.ParameterType.GetGenericTypeDefinition() == typeof(ILogger<>),
                (p, i) =>
                {
                    var category = p.ParameterType.GetGenericArguments()[0];
                    return _factory.CreateLogger(category);
                }),
        });
    }

    private void InjectLoggerProperties(object instance)
    {
        var props = instance.GetType()
            .GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance)
            .Where(p => p.PropertyType == typeof(ILogger) && p.CanWrite && p.GetIndexParameters().Length == 0);

        foreach (var prop in props)
            prop.SetValue(instance, _factory.CreateLogger(instance.GetType()));
    }
}
原 Memo 用的 log4net ILog 仍是可行的写法(实现思路一致);这里换成 ILogger<T>,与现代 ASP.NET Core 日志管线直接打通(Serilog / NLog / Application Insights 都是 ILoggerFactory 的实现)。

注册

var builder = WebApplication.CreateBuilder(args);

// 日志工厂由 ASP.NET Core 默认提供,可加任何 ILoggerProvider
builder.Logging.AddSimpleConsole(o =>
{
    o.IncludeScopes = true;
    o.SingleLine = true;
});

var factory = LoggerFactory.Create(b =>
    b.AddSimpleConsole().AddDebug());   // 或者从 builder.Services 拿

builder.Host.ConfigureContainer<ContainerBuilder>(b =>
{
    b.RegisterModule(new LoggingModule(factory));
});

使用

构造函数注入(推荐):

public class OrderService(ILogger<OrderService> logger)
{
    public async Task PlaceAsync(Order order)
    {
        logger.LogInformation("Placing order {OrderId}", order.Id);
        // ...
    }
}

属性注入(用于不能改构造函数的第三方类型):

public class SomeLegacyService
{
    public ILogger Logger { get; set; }   // 由 LoggingModule 自动赋值
}

也支持任何非泛型 ILogger:

public class DispatchJob(ILogger logger) { ... }

验证

启用 scope 让日志带上上下文:

using (logger.BeginScope(new Dictionary<string, object> { ["OrderId"] = order.Id }))
{
    // 日志会自动带上 OrderId
}

接 Serilog 时也直接生效——在 Program.cs 用 builder.Host.UseSerilog() 替换默认工厂即可。

常见问题

  • 属性名 Logger 冲突:类里已有其他 logger 字段,会被覆盖。改名为 Logger { get; set; } 即可。
  • Property 是 ILogger<T>:本模块不处理泛型 logger 属性(仅 ILogger 非泛型)。建议直接构造函数注入。
  • Logger 在子容器里创建:作用域里的 ILogger 不会自动带上 ILoggerScope。需要 scope 显式 begin。

使用准则

  1. 永远优先构造函数注入,属性注入只用于无法控制的第三方类。
  2. 在构造函数里加 ILogger<TMyClass>,比 ILogger 非泛型更友好——日志自动带上类名。
  3. 日志格式用占位符({OrderId})而不是字符串拼接——结构化日志能用。
  4. 关键链路用 BeginScope 带上下文,CI 出问题才查得动。
参考资料