为什么需要 LoggingModule
ASP.NET Core 自带的 ILogger<T> 在控制器里是自动注入的。但项目里往往有非控制器的服务类(领域服务、Repository、事件处理器等),如果也用 Autofac 自己解析,ILogger<T> 默认拿不到——因为 Autofac 不知道 ILogger<T> 怎么构造。
LoggingModule 解决两件事:
- 从 Autofac 解析
ILogger<T>时,按T的实际类型提供 logger。 - 对属性类型为
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。
使用准则
- 永远优先构造函数注入,属性注入只用于无法控制的第三方类。
- 在构造函数里加
ILogger<TMyClass>,比ILogger非泛型更友好——日志自动带上类名。 - 日志格式用占位符(
{OrderId})而不是字符串拼接——结构化日志能用。 - 关键链路用
BeginScope带上下文,CI 出问题才查得动。