ASP.NET Core Web API · Cross-Cutting

Logging

Logs from an API are read by machines first: Application Insights, Seq, Loki or Elasticsearch index them so you can ask “show me every failed payment for order 1042”. That only works if logs are structured. This lesson makes the ShopSmart API write useful, structured logs.

Intermediate 6 min Lesson 21 of 37Examples target .NET 9.
Before you start:ASP.NET Core MVC: Logging
0/37 in ASP.NET Core Web API
On this page · 6 min · Intermediate

In simple words#

API logs are the CCTV recordings of your service: who called what, how long it took, and what failed.

ILogger<T>
Write logs from any class.
Correlation ID
Follow one request across services.
Levels
Information for normal, Error for failures.

Learning objectives#

  • Write structured logs with message templates
  • Output JSON logs that log platforms can index
  • Add context to many log lines with scopes
  • Choose what to log (and what never to log) in an API

Explanation#

Use message templates with named holes: logger.LogInformation("Order {OrderId} placed with {ItemCount} items", id, count). The values are stored as separate fields, not just baked into text.

For production, write JSON: builder.Logging.AddJsonConsole() (or Serilog / OpenTelemetry exporters). Each log line becomes an object with Category, LogLevel, Message and State fields.

Scopes attach context to every log inside them: using (logger.BeginScope(new Dictionary<string, object> { ["OrderId"] = id })) { … }. Enable IncludeScopes in the formatter to write them.

Log at the right level: Information for business events (order placed), Warning for unusual but handled cases (payment retried), Error for failures. Keep framework noise down with filters (Microsoft.AspNetCore at Warning).

Never log passwords, tokens, card numbers or full request bodies with personal data. For request/response logging use app.UseHttpLogging() with only the fields you need.

Syntax#

C# Illustrative
builder.Logging.ClearProviders().AddJsonConsole();
logger.LogInformation("Order {OrderId} placed with {ItemCount} items", order.Id, order.Items.Count);
using (logger.BeginScope(new Dictionary<string, object> { ["OrderId"] = order.Id })) { … }

Code example: JSON logs with fields and filtering#

C#Controllers/OrdersController.cs Illustrative
// Program.cs
using System.Net.Http.Json;
using System.Text.Json;


var builder = WebApplication.CreateBuilder(args);
builder.Logging.ClearProviders();
builder.Logging.AddJsonConsole(options => options.JsonWriterOptions = new JsonWriterOptions { Indented = false });
builder.Logging.AddFilter("Microsoft", LogLevel.Warning);           // hide framework noise
builder.Services.AddControllers();
var app = builder.Build();
app.MapControllers();
app.Urls.Add("http://127.0.0.1:5221");
await app.StartAsync();


using var http = new HttpClient { BaseAddress = new Uri("http://127.0.0.1:5221") };
await http.PostAsJsonAsync("/api/orders", new { customerId = 7, items = new[] { "MOUSE-1", "HUB-2" }, cardNumber = "4111111111111111" });
await http.PostAsJsonAsync("/api/orders", new { customerId = 8, items = Array.Empty<string>(), cardNumber = "5500000000000004" });
await app.StopAsync();
await app.DisposeAsync();                                            // flush the console logger


// Controllers/OrdersController.cs
using Microsoft.AspNetCore.Mvc;


public record PlaceOrderRequest(int CustomerId, string[] Items, string CardNumber);


[ApiController]
[Route("api/orders")]
public class OrdersController(ILogger<OrdersController> logger) : ControllerBase
{
    private static int _nextId = 1041;


    [HttpPost]
    public IActionResult Place(PlaceOrderRequest request)
    {
        if (request.Items.Length == 0)
        {
            logger.LogWarning("Customer {CustomerId} tried to order an empty cart", request.CustomerId);
            return Problem(statusCode: 422, title: "The cart is empty");
        }
        var orderId = Interlocked.Increment(ref _nextId);
        // Log ids and counts — never the card number
        logger.LogInformation("Order {OrderId} placed by customer {CustomerId} with {ItemCount} items",
            orderId, request.CustomerId, request.Items.Length);
        return Created(
quot;/api/orders/
{orderId}", new { orderId });
} }
Expected output
{"EventId":0,"LogLevel":"Information","Category":"OrdersController","Message":"Order 1042 placed by customer 7 with 2 items","State":{"Message":"Order 1042 placed by customer 7 with 2 items","OrderId":1042,"CustomerId":7,"ItemCount":2,"{OriginalFormat}":"Order {OrderId} placed by customer {CustomerId} with {ItemCount} items"}}
{"EventId":0,"LogLevel":"Warning","Category":"OrdersController","Message":"Customer 8 tried to order an empty cart","State":{"Message":"Customer 8 tried to order an empty cart","CustomerId":8,"{OriginalFormat}":"Customer {CustomerId} tried to order an empty cart"}}

This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.

Step-by-step explanation#

  1. AddJsonConsole writes each log entry as one JSON object; the Microsoft filter removes framework chatter.
  2. The message template keeps OrderId, CustomerId and ItemCount as separate fields under State — searchable in any log platform.
  3. The empty-cart request logs a Warning with the customer id.
  4. The card number from the request body is never written to the logs.
  5. Disposing the app flushes the logger before the process ends.

Real-world use case#

ShopSmart ships these JSON logs to Azure Application Insights. A dashboard counts “Order {OrderId} placed” events per minute, and an alert fires when Warnings for empty carts spike after a front-end release.

Common mistakes#

String interpolation in log calls

quot;Order {id} placed" loses the OrderId field. Use templates.

Logging request bodies wholesale

Bodies contain passwords, card numbers and personal data. Log selected fields only.

Information-level framework logs in production

Every request producing ten framework lines drowns the useful ones and costs money in log storage.

Best practices#

  • Templates with named fields
  • JSON (or OpenTelemetry) output in production
  • Scopes for request or order context
  • Strict rules about sensitive data

Remember this#

"Order {OrderId}" — template, not interpolation.

AddJsonConsole / Serilog / OpenTelemetry for machine-readable logs.

Never log secrets or card numbers.

Practice exercise#

Your task: Wrap the order-placing code in a log scope with OrderId so every log line inside it carries that field, and enable IncludeScopes on the JSON console formatter.

Need a hint?
using (logger.BeginScope(new Dictionary<string, object> { ["OrderId"] = orderId })) { … } and options.IncludeScopes = true.
Show solution
C# Illustrative
// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Logging.ClearProviders();
builder.Logging.AddJsonConsole(options => options.IncludeScopes = true);
builder.Services.AddControllers();
var app = builder.Build();
app.MapControllers();
app.Run();


// Controllers/ScopedOrdersController.cs
using Microsoft.AspNetCore.Mvc;


[ApiController]
[Route("api/scoped-orders")]
public class ScopedOrdersController(ILogger<ScopedOrdersController> logger) : ControllerBase
{
    [HttpPost("{orderId:int}/ship")]
    public IActionResult Ship(int orderId)
    {
        using (logger.BeginScope(new Dictionary<string, object> { ["OrderId"] = orderId }))
        {
            logger.LogInformation("Packing started");
            logger.LogInformation("Label printed");          // both lines carry OrderId
        }
        return Accepted();
    }
}

Mini quiz#

Logging quizMultiple choice Question 1 of 3

Why use logger.LogInformation("Order {OrderId}", id) instead of

quot;Order {id}"?

Score so far: 0/0

Want more? Practise all ASP.NET Core Web API MCQs · ASP.NET Core Web API interview questions

My notes#

Saved in this browser.

Progress is saved in this browser. Sign in to sync.