The LoggerMessage Source Generator: High-Performance Logging
Logging is everywhere in .NET applications, but the standard ILogger extension methods have hidden costs. Every call to _logger.LogInformation("Processing order {OrderId}", orderId) allocates an object[] for the parameters, boxes any value types, and parses the message template — even if the log level is disabled. The LoggerMessage source generator eliminates all of these costs.
The Problem
Consider this common logging pattern:
public class OrderService
{
private readonly ILogger<OrderService> _logger;
public void ProcessOrder(int orderId, decimal amount)
{
_logger.LogInformation(
"Processing order {OrderId} for {Amount:C}",
orderId, amount);
}
}
Every call to LogInformation:
- Allocates an
object[]to hold the parameters. - Boxes
orderId(int) andamount(decimal) intoobject. - Parses the message template string to find
{OrderId}and{Amount:C}. - Only then checks whether
Informationlevel is enabled.
If the log level is set to Warning, all that work is wasted. In a hot path handling thousands of requests per second, these allocations add up.
The Source Generator Solution
The [LoggerMessage] attribute tells the source generator to produce an optimised logging method:
using Microsoft.Extensions.Logging;
public partial class OrderService
{
private readonly ILogger<OrderService> _logger;
[LoggerMessage(
EventId = 1001,
Level = LogLevel.Information,
Message = "Processing order {OrderId} for {Amount:C}")]
partial void LogOrderProcessing(int orderId, decimal amount);
public void ProcessOrder(int orderId, decimal amount)
{
LogOrderProcessing(orderId, amount);
}
}
The method must be partial and the class must be partial. The generator fills in the implementation.
What Gets Generated
The generated code looks approximately like this:
partial class OrderService
{
private static readonly Action<ILogger, int, decimal, Exception?>
__LogOrderProcessingCallback =
LoggerMessage.Define<int, decimal>(
LogLevel.Information,
new EventId(1001, nameof(LogOrderProcessing)),
"Processing order {OrderId} for {Amount:C}");
partial void LogOrderProcessing(int orderId, decimal amount)
{
if (_logger.IsEnabled(LogLevel.Information))
{
__LogOrderProcessingCallback(
_logger, orderId, amount, null);
}
}
}
Key differences from the manual approach:
- Level check first:
IsEnabledis called before any work is done. - No boxing: The
Define<int, decimal>method creates a strongly-typed delegate. Noobject[]allocation. - Template pre-parsed:
LoggerMessage.Defineparses the template once and caches the result in a static field. - No allocation when disabled: If the level is not enabled, the method returns immediately.
Usage Patterns
Instance Logger
If the class has an ILogger field, the generator finds it automatically:
public partial class PaymentService
{
private readonly ILogger _logger;
public PaymentService(ILogger<PaymentService> logger)
{
_logger = logger;
}
[LoggerMessage(
EventId = 2001,
Level = LogLevel.Warning,
Message = "Payment {PaymentId} declined: {Reason}")]
partial void LogPaymentDeclined(string paymentId, string reason);
}
Static Methods
You can also use static methods by passing the logger as a parameter:
public static partial class LogMessages
{
[LoggerMessage(
EventId = 3001,
Level = LogLevel.Error,
Message = "Unhandled error in {Operation}")]
public static partial void LogUnhandledError(
this ILogger logger,
string operation,
Exception ex);
}
Note the this ILogger logger parameter — this creates an extension method. The last parameter of type Exception is automatically used as the exception parameter for the log entry.
Dynamic Log Level
If you omit the Level property, the log level becomes a parameter:
[LoggerMessage(
EventId = 4001,
Message = "Cache {Operation} for key {Key}")]
public static partial void LogCacheOperation(
this ILogger logger,
LogLevel level,
string operation,
string key);
// Usage:
logger.LogCacheOperation(LogLevel.Debug, "hit", cacheKey);
logger.LogCacheOperation(LogLevel.Warning, "miss", cacheKey);
Structured Logging
The generated code preserves structured logging semantics. The parameter names in the message template ({OrderId}, {Amount}) become property names in the structured log output. This means log aggregation tools like Seq, Elasticsearch, or Application Insights can filter and search by these properties — just as they would with the manual approach.
// Both produce identical structured output:
_logger.LogInformation("Order {OrderId} shipped", orderId); // manual
LogOrderShipped(orderId); // source-generated
Compile-Time Validation
The source generator validates your log methods at compile time:
// Error: Template has {OrderId} but parameter is named 'id'
[LoggerMessage(Level = LogLevel.Information,
Message = "Processing {OrderId}")]
partial void LogOrder(int id);
// Error: No ILogger field found and no logger parameter
[LoggerMessage(Level = LogLevel.Information,
Message = "Hello")]
partial void LogHello();
These errors appear immediately in the IDE, not at runtime.
Performance Impact
Microsoft's benchmarks show the source-generated approach is roughly six times faster than the standard extension methods when the log level is enabled, and effectively zero cost when it is disabled. Memory allocation drops to zero in both cases.
For applications that log heavily — particularly in request pipelines, middleware, or background services — switching to the source generator is one of the easiest performance wins available.
Migration Strategy
You do not need to migrate all logging at once. The source-generated methods and the standard extension methods use the same ILogger infrastructure and can coexist in the same class. Start with your hottest paths and work outward.
- Identify high-throughput code paths.
- Add
partialto the class. - Create
[LoggerMessage]methods for each log call. - Replace the manual calls with the generated methods.
The structured output is identical, so log aggregation and alerting rules do not need to change.