Structured Logging with ILogger in ASP.NET Core

Logging is one of those things every application needs but few get right from the start. ASP.NET Core ships with a mature logging abstraction built around ILogger<T> that supports structured logging out of the box.

Getting Started

ILogger<T> is registered automatically by WebApplication.CreateBuilder(). Inject it into any class:

OrderService.cs
public class OrderService
{
    private readonly ILogger<OrderService> _logger;

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

    public void PlaceOrder(int orderId, decimal total)
    {
        _logger.LogInformation("Order {OrderId} placed with total {Total:C}",
            orderId, total);
    }
}

The category name is derived from T — in this case, OrderService — making it easy to filter logs by source.

Structured Logging vs String Interpolation

This is a critical distinction. Never use string interpolation for log messages:

Example.cs
// WRONG — loses structure, allocates even if the log level is disabled
_logger.LogInformation($"Order {orderId} placed for {total:C}");

// RIGHT — parameters are captured as structured data
_logger.LogInformation("Order {OrderId} placed for {Total:C}", orderId, total);

With the structured approach, OrderId and Total become named properties that logging providers (like Serilog, Seq, or Application Insights) can index and query. The message template is only formatted if the log level is enabled, avoiding unnecessary allocations.

Log Levels

ASP.NET Core defines six log levels, from most to least verbose:

Level Method Use for
Trace LogTrace Detailed diagnostic information
Debug LogDebug Development-time debugging
Information LogInformation General application flow
Warning LogWarning Unexpected events that are not errors
Error LogError Errors that affect a specific operation
Critical LogCritical Application-wide failures

Filtering with Configuration

Control which log levels are emitted per category in appsettings.json:

appsettings.json
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning",
      "MyApp.Services.OrderService": "Debug"
    }
  }
}

This configuration suppresses the noisy ASP.NET Core framework logs below Warning, whilst showing Debug-level logs for OrderService. The most specific category match wins.

High-Performance Logging with LoggerMessage

For hot paths, the LoggerMessage.Define pattern avoids boxing and parsing the message template on every call:

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

// Usage
LogMessages.OrderPlaced(_logger, orderId, total);

The source generator produces an optimised method that checks the log level before doing any work. This is the recommended approach for any logging in performance-sensitive code.

Scopes for Contextual Information

Log scopes attach additional properties to every log entry within a block:

Example.cs
public async Task ProcessOrder(int orderId, string userId)
{
    using (_logger.BeginScope(new Dictionary<string, object>
    {
        ["OrderId"] = orderId,
        ["UserId"] = userId
    }))
    {
        _logger.LogInformation("Processing started");
        await ValidateInventory();
        await ChargePayment();
        _logger.LogInformation("Processing completed");
    }
}

Every log entry inside the using block will include OrderId and UserId, making it trivial to correlate related log entries when diagnosing issues.

Adding a Third-Party Provider

The built-in console and debug providers are fine for development, but production applications typically use a structured logging provider like Serilog:

Program.cs
builder.Host.UseSerilog((context, configuration) =>
{
    configuration
        .ReadFrom.Configuration(context.Configuration)
        .WriteTo.Console()
        .WriteTo.Seq("http://localhost:5341");
});

Because ILogger<T> is an abstraction, your application code does not change — you only swap the provider configuration.

Key Takeaways

Use ILogger<T> with message templates (not string interpolation) to preserve structured data. Use LoggerMessage source generators for hot paths. Configure log levels per category to control verbosity, and use scopes to attach contextual information that flows through your entire operation.