Lessons in this course (8)

3. Scopes, correlation ids and high-performance logging

Intermediate★ Must knowLesson 3 of 8

Scopes attach properties to every log written inside a block:

csharp
using (logger.BeginScope(new Dictionary<string, object> { ["OrderId"] = order.Id, ["CustomerId"] = order.CustomerId }))
{
    logger.LogInformation("Validating");     // includes OrderId and CustomerId
    logger.LogInformation("Charging");       // same
}

ASP.NET Core automatically adds RequestId, RequestPath, TraceId, SpanId scopes (when the provider includes scopes, e.g. "IncludeScopes": true for console).

Correlation id — one id that follows a request through UI → API → other services → logs:

csharp
app.Use(async (ctx, next) =>
{
    var id = ctx.Request.Headers["X-Correlation-ID"].FirstOrDefault() ?? Activity.Current?.TraceId.ToString() ?? Guid.NewGuid().ToString();
    ctx.Response.Headers["X-Correlation-ID"] = id;
    using (ctx.RequestServices.GetRequiredService<ILogger<Program>>().BeginScope(new Dictionary<string, object> { ["CorrelationId"] = id }))
        await next();
});

With OpenTelemetry/W3C trace context, the TraceId already plays this role across services automatically.

High-performance logging — the source generator (.NET 6+):

csharp
public static partial class Log
{
    [LoggerMessage(EventId = 1001, Level = LogLevel.Information, Message = "Order {OrderId} placed for {Total}")]
    public static partial void OrderPlaced(ILogger logger, int orderId, decimal total);

    [LoggerMessage(EventId = 5001, Level = LogLevel.Error, Message = "Payment failed for order {OrderId}")]
    public static partial void PaymentFailed(ILogger logger, Exception ex, int orderId);
}

Log.OrderPlaced(logger, order.Id, order.Total);   // no boxing, no template parsing at runtime, level checked first

Also: if (logger.IsEnabled(LogLevel.Debug)) before expensive message construction; avoid logging in tight loops; let sinks batch asynchronously.