LoggerMessage:编写高性能的 .NET 日志

LoggerMessage 的四大性能法宝

LoggerMessage(尤其是结合 .NET 6+ 的源代码生成器)专门为了解决上述痛点而生。它通过以下四个核心机制,实现了极高性能和零内存分配(Zero Allocation)

1. 预解析并缓存模板

LoggerMessage 会在底层将模板的解析工作提前。字符串模板只会在类加载时被解析一次,并将结果缓存在静态字段中。后续每次记录该日志时,直接复用缓存,彻底干掉重复解析的 CPU 开销。

2. 强类型泛型委托(拒绝装箱)

LoggerMessage 使用预生成的泛型委托(例如 Action<ILogger, int, string, Exception?>)来处理参数。因为明确指出了 orderIdint 类型,参数传递时不会发生任何装箱操作。

3. 消灭 params object[] 数组

由于直接调用的是强类型委托,参数被直接塞进方法调用中,底层的 object[] 数组分配也被完美避开。

4. 前置的日志级别检查

生成的委托会在执行任何逻辑之前,第一时间检查 _logger.IsEnabled(LogLevel)。如果级别未启用,直接 return,实现零浪费。


🛠️ 现代玩法:使用源代码生成器 (.NET 6+)

在以前,手动编写 LoggerMessage.Define 需要写一堆繁琐的样板代码。从 .NET 6 开始,微软引入了 [LoggerMessage] 特性,配合编译器 Source Generators(源代码生成器),让你只需声明方法签名,剩下的全由编译器代劳。

你的代码(极简且优雅):

public partialclassOrderProcessor
{
    privatereadonly ILogger _logger;

    public OrderProcessor(ILogger<OrderProcessor> logger) => _logger = logger;

    // 1. 加上 [LoggerMessage] 特性
    // 2. 声明为 partial 方法
    [LoggerMessage(
        EventId = 1, 
        Level = LogLevel.Information, 
        Message = "Processing order {OrderId} for user {UserId}")]
    public partial void LogOrderProcessing(int orderId, string userId);

    public void Process(int orderId, string userId)
    {
        // 像普通方法一样调用,享受极高的性能
        LogOrderProcessing(orderId, userId); 
    }
}

 ********************

.NET 9 中的 LoggerMessage 源码生成器

LoggerMessage 是 .NET 6 引入的编译时日志源码生成特性,在 .NET 9 中持续演进,是目前 .NET 中高性能日志记录的推荐方案。

核心概念

[LoggerMessage] 特性通过 Source Generator 在编译时自动生成高效的日志方法,底层调用 LoggerMessage.Define() 缓存委托,避免运行时字符串拼接和装箱开销。

基本用法

1. 扩展方法方式(推荐)

public static partial class MyServiceLogging
{
    [LoggerMessage(
        EventId = 1000,
        Level = LogLevel.Information,
        Message = "用户 {UserId} 在 {LoginTime:yyyy-MM-dd HH:mm:ss} 登录")]
    public static partial void UserLoggedIn(this ILogger logger, int userId, DateTime loginTime);

    [LoggerMessage(
        EventId = 1001,
        Level = LogLevel.Error,
        Message = "处理订单 {OrderId} 失败")]
    public static partial void OrderProcessingFailed(this ILogger logger, Exception ex, Guid orderId);
}

 使用:

public class MyService(ILogger<MyService> logger)
{
    public void DoWork(int userId)
    {
        logger.UserLoggedIn(userId, DateTime.Now);
    }
}

2.  实例方法方式(依赖注入)

public partial class MyService
{
    private readonly ILogger<MyService> _logger;

    public MyService(ILogger<MyService> logger)
    {
        _logger = logger;
    }

    [LoggerMessage(
        EventId = 0,
        Level = LogLevel.Information,
        Message = "PrintMessage: {message}")]
    public partial void LogPrintMessage(string message);
}

3.  动态日志级别

[LoggerMessage(
    EventId = 0,
    Message = "PrintMessage: {message}")]
public static partial void LogPrintMessage(this ILogger logger, LogLevel logLevel, string message);

 


.NET 9 的重要改进

改进项说明
主构造函数支持 .NET 9 修复了 LoggerMessage 与 C# 12 主构造函数不兼容的问题
 
更简洁的 API EventId 不再是必需参数,可直接写 [LoggerMessage(LogLevel.Warning, "message")]
 
属性支持 修复了使用 ILogger 属性(而非字段)时源码生成器崩溃的问题
 

约束条件

使用 LoggerMessage 时必须遵守以下规则:
  • 日志方法必须是 partial 且返回 void
  • 方法名不能以 _ 开头
  • 参数名不能以 _ 开头
  • 日志方法不能是泛型方法
  • 如果是 static 方法,ILogger 实例必须作为参数传入
  • 类必须是 partial
     

生成的代码示例

编译器会自动生成类似下面的代码:
[GeneratedCode("Microsoft.Extensions.Logging.Generators", "9.0.12.6610")]
private static readonly Action<ILogger, int, DateTime, Exception?> __UserLoggedInCallback =
    LoggerMessage.Define<int, DateTime>(
        LogLevel.Information,
        new EventId(1000, "UserLoggedIn"),
        "Goat number {Id} logged in at {LoginTime}",
        new LogDefineOptions() { SkipEnabledCheck = true }
    );

[GeneratedCode("Microsoft.Extensions.Logging.Generators", "9.0.12.6610")]
public static void UserLoggedIn(this ILogger logger, int id, DateTime loginTime)
{
    if (!logger.IsEnabled(LogLevel.Information))
        return;
    __UserLoggedInCallback(logger, id, loginTime, null);
}

 

揭秘魔法:编译器为你生成的另一半代码

为什么上面那个没有方法体的 partial 方法能工作?如果你扒开编译器的幕后,你会发现它为你生成了这样一段高度优化的代码:

// <auto-generated/>
// 编译器自动生成的另一半 OrderProcessor 类

#nullable enable
publicpartialclassOrderProcessor
{
    // 静态缓存强类型委托,模板只解析一次!
    privatestaticreadonly Action<ILogger, int, string, Exception?> __LogOrderProcessingCallback =
        LoggerMessage.Define<int, string>(
            LogLevel.Information,
            new EventId(1, nameof(LogOrderProcessing)),
            "Processing order {OrderId} for user {UserId}",
            new LogDefineOptions() { SkipEnabledCheck = true }); 

    // 补全你的 partial 方法
    public partial void LogOrderProcessing(int orderId, string userId)
    {
        // 极致的前置性能检查
        if (_logger.IsEnabled(LogLevel.Information))
        {
            // 直接调用委托,传值类型 int,零装箱,零数组分配!
            __LogOrderProcessingCallback(_logger, orderId, userId, null);
        }
    }
}

 

看看这段生成的代码,我们前面提到的四大性能法宝(静态缓存、前置检查、强类型、无数组分配)在这里展现得淋漓尽致。


💡 总结

如果你的应用对性能要求极高,或者某个模块(如中间件、后台循环任务、高频接口)会产生海量的日志,请务必抛弃标准的扩展方法,拥抱 [LoggerMessage] 源代码生成器

posted @ 2026-06-17 15:21  Charltsing  阅读(6)  评论(0)    收藏  举报