30% OFF - Use code HANDSON30 for 30% off any course or Dometrain Pro! Browse courses →
  • Courses
  • Learning Paths
  • Blogs
  • Authors
  • Leaderboard
  • Dometrain Pro
  • Shopping Basket

    Your basket is empty

  • Business Portal
  • Logging in .NET - Best Practices

    August 13, 2025
    Listen to this blog 4m 3s
    Sign in to unlock the audio version of this article
    Sign in to listen

    Logs should help you fix bugs and explain what the app is doing right now. They shouldn’t drown you in text or hide the one line you need. This guide shows how to wire logging in modern .NET, write structured messages, pick the right level, and add request correlation. Also, don’t confuse logs with metrics, it is wrong to add logs to check for API calls per X amount of time.


    Quick start (Minimal API)

    Program.cs

    var builder = WebApplication.CreateBuilder(args);
    
    // Console logging is on by default; you can tweak it via appsettings.json
    var app = builder.Build();
    
    app.MapGet("/", (ILogger<Program> log) =>
    {
        log.LogInformation("Health probe hit at {UtcNow}", DateTime.UtcNow);
        return "OK";
    });
    
    app.Run();
    

    This already writes to the console with a timestamp, log level, category, and message. Configuration lives in appsettings.json.


    Log levels that make sense

    • Trace — very chatty, step‑by‑step flow. Off in production.
    • Debug — useful during development; safe to disable later.
    • Information — high‑level events: app start, request completed, business events.
    • Warning — unusual conditions: retries, timeouts that recover.
    • Error — failures you catch and handle (include the exception).
    • Critical — the app is not healthy.

    Pick the lowest level that still conveys the right urgency. If everything is Information, nothing is.


    Structured logging beats string concatenation

    Message templates keep data in fields for querying later. Avoid string interpolation here.

    // ✅ good
    log.LogInformation("User {UserId} logged in from {Ip}", userId, ip);
    
    // ❌ bad (hard to query)
    log.LogInformation($"User {userId} logged in from {ip}");
    

    With templates, tools can filter on UserId or Ip without parsing text.

    Include exceptions as the first argument:

    try
    {
        await service.ProcessAsync(orderId);
    }
    catch (Exception ex)
    {
        log.LogError(ex, "Failed to process order {OrderId}", orderId);
    }
    

    Configure logging via appsettings.json

    {
      "Logging": {
        "LogLevel": {
          "Default": "Information",
          "Microsoft.AspNetCore": "Warning",
          "Microsoft.EntityFrameworkCore.Database.Command": "Warning"
        },
        "Console": {
          "FormatterName": "json",
          "FormatterOptions": {
            "IncludeScopes": true,
            "UseUtcTimestamp": true
          }
        }
      }
    }
    
    • Lower noisy categories (e.g., framework internals) to Warning.
    • JSON formatter plays well with log aggregators and keeps fields structured.

    Categories and DI

    The logger category is the type you ask for. This helps filter and group logs.

    public sealed class BillingService(ILogger<BillingService> log)
    {
        public void Charge(Guid orderId, decimal amount)
            => log.LogInformation("Charging {OrderId} {Amount}", orderId, amount);
    }
    

    In Minimal APIs, inject ILogger<Program> (or another type) into the handler.


    Scopes and correlation IDs

    Scopes attach extra properties to every log line inside a block. Add a correlation ID per request so you can trace a path across modules.

    app.Use(async (ctx, next) =>
    {
        var cid = ctx.Request.Headers.TryGetValue("X-Correlation-ID", out var h) && !string.IsNullOrWhiteSpace(h)
            ? h.ToString()
            : Guid.NewGuid().ToString();
    
        using (loggerFactory.CreateLogger("Correlation").BeginScope(new Dictionary<string, object?>
        {
            ["CorrelationId"] = cid
        }))
        {
            ctx.Response.Headers.Append("X-Correlation-ID", cid);
            await next();
        }
    });
    

    Now every log line in that request includes CorrelationId (and will show up in JSON output).


    Request logging middleware (built‑in)

    For basic HTTP logs without extra packages:

    using Microsoft.AspNetCore.HttpLogging;
    
    builder.Services.AddHttpLogging(o =>
    {
        o.LoggingFields = HttpLoggingFields.RequestMethod
                        | HttpLoggingFields.RequestPath
                        | HttpLoggingFields.ResponseStatusCode
                        | HttpLoggingFields.Duration;
    });
    
    var app = builder.Build();
    app.UseHttpLogging();
    

    This records method, path, status, and duration. Avoid logging bodies unless you have a strong reason.


    High‑throughput hot paths: LoggerMessage.Define

    Avoid string formatting allocations by pre‑compiling message templates. The source generator creates efficient logging code (no boxing/allocations on the hot path) and validates your templates at build time.

    static class Logs
    {
        private static readonly Action<ILogger, string, Exception?> _cacheMiss =
            LoggerMessage.Define<string>(LogLevel.Debug, new EventId(1001, "CacheMiss"),
                "Cache miss for key {Key}");
    
        public static void CacheMiss(this ILogger log, string key) => _cacheMiss(log, key, null);
    }
    
    // usage
    log.CacheMiss(key);
    

    You still get structured fields, but with fewer allocations.


    Filtering per endpoint or area

    Sometimes a feature needs extra detail without turning the whole app to Debug.

    builder.Logging.AddFilter("Payments", LogLevel.Debug); // category prefix
    
    app.MapGroup("/payments")
       .WithGroupName("Payments")
       .MapPost("/", (ILoggerFactory factory) =>
       {
           var log = factory.CreateLogger("Payments.Checkout");
           log.LogDebug("Entering checkout");
           return Results.Ok();
       });
    

    You can also tune filters in appsettings.json by category name.


    File logs (optional, with Serilog)

    The built‑in providers don’t write to files. If you want rolling files locally, add Serilog:

    dotnet add package Serilog.AspNetCore
    dotnet add package Serilog.Sinks.File
    
    using Serilog;
    
    var logger = new LoggerConfiguration()
        .WriteTo.Console()
        .WriteTo.File("logs/app-.log", rollingInterval: RollingInterval.Day, retainedFileCountLimit: 7)
        .CreateLogger();
    
    builder.Host.UseSerilog(logger);
    

    Keep this optional. For many apps, console + an aggregator is enough.


    What to log (and what to skip)

    • Do log: start/stop events, external calls (target + duration), business events, warnings with context, handled errors with stack traces.
    • Skip: secrets, full request/response bodies with PII, chatty loops, heartbeat spam.
    • Protect data: hash or mask identifiers when in doubt.

    Small sample tying it together

    var builder = WebApplication.CreateBuilder(args);
    builder.Services.AddHttpLogging(o =>
    {
        o.LoggingFields = HttpLoggingFields.RequestMethod
                        | HttpLoggingFields.RequestPath
                        | HttpLoggingFields.ResponseStatusCode
                        | HttpLoggingFields.Duration;
    });
    
    var app = builder.Build();
    
    var log = app.Services.GetRequiredService<ILoggerFactory>().CreateLogger("Startup");
    app.UseHttpLogging();
    
    app.Use(async (ctx, next) =>
    {
        var cid = ctx.Request.Headers["X-Correlation-ID"].FirstOrDefault() ?? Guid.NewGuid().ToString();
        using (app.Logger.BeginScope(new { CorrelationId = cid }))
        {
            ctx.Response.Headers.Append("X-Correlation-ID", cid);
            await next();
        }
    });
    
    app.MapGet("/weather", (ILogger<Program> logger) =>
    {
        using var _ = logger.BeginScope(new { Feature = "Weather" });
        logger.LogInformation("Fetching weather");
        return new[] { "Rain", "Sun" };
    });
    
    app.Run();
    

    Try hitting /weather and check the console output. You’ll see JSON lines that include CorrelationId and Feature.


    Common mistakes

    • Using string interpolation in log messages. Use templates with named placeholders.
    • Logging exceptions without passing the exception object.
    • No correlation between logs from the same request.
    • Turning everything to Information or Debug and never trimming it.
    • Writing secrets to logs by accident (tokens, passwords).

    A short checklist

    • [ ] Message templates with named fields.
    • [ ] Log levels picked with intent.
    • [ ] Per‑category filters to cut noise.
    • [ ] BeginScope for correlation IDs.
    • [ ] UseHttpLogging for basic request logs.
    • [ ] LoggerMessage.Define for hot paths.
    • [ ] No secrets or PII.

    About the Author

    Nick Chapsas

    Nick Chapsas

    Nick Chapsas is a .NET & C# content creator, educator and a Microsoft MVP for Developer Technologies with years of experience in Software Engineering and Engineering Management.

    He has worked for some of the biggest companies in the world, building systems that served millions of users and tens of thousands of requests per second.

    Nick creates free content on YouTube and is the host of the Keep Coding Podcast.

    View all courses by Nick Chapsas

    What's New

    Getting Started: Model Context Protocol (MCP)
    course

    Getting Started: Model Context Protocol (MCP)

    Learn how to get started with the Model Context Protocol (MCP) and integrate it into your applications.

    Learn more about Getting Started: Model Context Protocol (MCP)
    Hands-On: Learn TypeScript
    course

    Hands-On: Learn TypeScript

    Learn TypeScript through hands-on coding exercises. Practice what you learn with interactive challenges designed for every level.

    Learn more about Hands-On: Learn TypeScript
    Hands-On: Learn JavaScript
    course

    Hands-On: Learn JavaScript

    Learn JavaScript through hands-on coding exercises. Practice what you learn with interactive challenges designed for every level.

    Learn more about Hands-On: Learn JavaScript
    Hands-On: Data Structures & Algorithms in C#
    course

    Hands-On: Data Structures & Algorithms in C#

    Master data structures and algorithms through hands-on coding exercises in C#. Free to enroll for 7 days!

    Learn more about Hands-On: Data Structures & Algorithms in C#
    Blogsmith.ai
    feature

    Blogsmith.ai

    Turn your videos into blogs and newsletters with AI. Check out our new product at blogsmith.ai.

    Learn more about Blogsmith.ai
    Leaderboard
    feature

    Leaderboard

    See how you stack up against other learners. Track your progress, climb the ranks, and compete with the Dometrain community.

    Learn more about Leaderboard
    Hands-On: Learn PostgreSQL
    course

    Hands-On: Learn PostgreSQL

    Learn PostgreSQL through hands-on coding exercises. Practice what you learn with interactive challenges designed for every level.

    Learn more about Hands-On: Learn PostgreSQL
    Free Hands-On: C# for Beginners
    course

    Free Hands-On: C# for Beginners

    Learn C# through hands-on coding exercises. Practice what you learn with interactive challenges designed for everyone, from beginners to experts.

    Learn more about Free Hands-On: C# for Beginners
    Getting Started: AI for .NET Developers
    course

    Getting Started: AI for .NET Developers

    Get started with integrating AI into your .NET applications effectively using the latest LLM best practices.

    Learn more about Getting Started: AI for .NET Developers
    Getting Started: Building .NET Applications on AWS
    course

    Getting Started: Building .NET Applications on AWS

    Learn how to build and deploy .NET applications on AWS using CDK, Lambda, DynamoDB, S3, and more.

    Learn more about Getting Started: Building .NET Applications on AWS
    What's new in C# 14
    blog

    What's new in C# 14

    This guide covers every new C# 14 feature, explains its benefits, and provides practical code examples to help you navigate how you can use them.

    Learn more about What's new in C# 14
    Let's Build It: AI Chatbot with RAG in .NET Using Your Data
    course

    Let's Build It: AI Chatbot with RAG in .NET Using Your Data

    Build a Retrieval-Augmented Generation (RAG) chatbot that can answer questions using your data.

    Learn more about Let's Build It: AI Chatbot with RAG in .NET Using Your Data
    From Zero to Hero: SignalR in .NET
    course

    From Zero to Hero: SignalR in .NET

    Enable enterprise-grade real-time communication for your web apps with SignalR.

    Learn more about From Zero to Hero: SignalR in .NET
    Deep Dive: Solution Architecture
    course

    Deep Dive: Solution Architecture

    Master solution architecture and turn business needs into scalable, maintainable systems.

    Learn more about Deep Dive: Solution Architecture
    Migrating: ASP.NET Web APIs to ASP.NET Core
    course

    Migrating: ASP.NET Web APIs to ASP.NET Core

    A step-by-step process to migrate ASP.NET Web APIs from .NET Framework to ASP.NET Core.

    Learn more about Migrating: ASP.NET Web APIs to ASP.NET Core
    Getting Started: Caching in .NET
    course

    Getting Started: Caching in .NET

    Let's make the hardest thing in programming easy for .NET software engineers.

    Learn more about Getting Started: Caching in .NET
    From Zero to Hero: Testing with xUnit in C#
    course

    From Zero to Hero: Testing with xUnit in C#

    Learn how to test any codebase in .NET with the latest version of xUnit, the industry-standard testing library.

    Learn more about From Zero to Hero: Testing with xUnit in C#
    Create a ChatGPT Console AI Chatbot in C#
    blog

    Create a ChatGPT Console AI Chatbot in C#

    This walkthrough is your hands-on entry point to create a basic C# console application that talks to ChatGPT using the OpenAI API.

    Learn more about Create a ChatGPT Console AI Chatbot in C#