Getting Started with ASP.NET Core Middleware for Better Logging and Correlation IDs
Introduction
When you build a web application in ASP.NET Core, every HTTP request and response flows through a pipeline. This pipeline is made of components called middleware.
A middleware is essentially a piece of code that can:
- inspect the incoming request,
- perform an action such as logging, authentication, or error handling,
- pass the request to the next middleware,
- or short-circuit the pipeline by returning a response directly.
ASP.NET Core itself relies heavily on middleware. For example:
- UseRouting() adds routing to the pipeline,
- UseAuthentication() checks the user identity,
- UseStaticFiles() serves CSS, JavaScript, and images.
The great advantage is that you are not limited to these built-in components. You can easily write your own middleware to introduce cross-cutting concerns tailored to your application, such as:
- logging the duration of requests,
- attaching a correlation ID,
- handling exceptions in a consistent way.
Mastering middleware is key to understanding how ASP.NET Core works under the hood. It gives you fine-grained control over the request pipeline and allows you to improve both observability and resilience in your applications.
In this article, we will build two practical middleware examples to improve logging and add correlation IDs in ASP.NET Core applications.

The Minimal Middleware Structure
A custom middleware in ASP.NET Core is just a simple class with two key parts:
- A constructor that accepts a
RequestDelegate. - An Invoke or InvokeAsync method that processes the request.
Here’s the minimal structure:
public class MyCustomMiddleware
{
private readonly RequestDelegate _next;
public MyCustomMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
// Do something before
await _next(context);
// Do something after
}
}
Once the middleware is created, it must be added to the request pipeline in Program.cs:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.UseMiddleware<MyCustomMiddleware>();
app.Run();
This is all you need to get started. With this minimal structure in mind, we can now move on to some real-world examples that demonstrate how middleware can improve logging and error handling in your ASP.NET Core applications.
What is a Correlation ID and Why is it Useful?
When working with distributed applications or microservices, a single user request can travel through multiple services:
- API Gateway → Backend Service → Database → Message Queue → Another Service.
Without a way to tie all these pieces together, it’s very difficult to trace what happened when something goes wrong.
A Correlation ID is a unique identifier attached to each request. It helps you:
- Trace a request end-to-end: You can follow the same request across logs from different services.
- Simplify debugging: When a customer reports an error, asking them to provide the
X-Correlation-IDfrom the response makes it easy to find the exact log entries. - Improve observability: Monitoring tools and log aggregators (like Serilog, ELK, or Application Insights) can use the correlation ID as a common key.
In practice:
- The client can send its own correlation ID in the
X-Correlation-IDheader. - If none is provided, the server generates a new one.
- The ID is then included in logs and sent back in the response, so the client can see it.
This small addition makes a huge difference in troubleshooting complex systems.
Example: Custom Middleware in ASP.NET Core Minimal API
Disclaimer: This example is purely for educational purposes. There are better ways to write code and applications that can optimize this example. Use this as a starting point for learning, but always strive to follow best practices and improve your implementation.
Before we start with the code, it’s important to mention a design principle that also applies to middleware: the Single Responsibility Principle (SRP).
Each middleware should ideally take care of one responsibility only.
- If you try to combine multiple concerns (for example logging, correlation IDs, and exception handling) into a single middleware, the code becomes harder to test and maintain.
- By keeping them separate, each middleware is focused and reusable in different projects.
This is also the reason why the ASP.NET Core pipeline is so flexible: you can compose as many middleware as you want, each doing a specific job.
In the following example we’ll create two distinct middleware:
- One for logging slow requests.
- Another for adding correlation IDs.
Both are small, independent, and easy to understand and when combined in the pipeline, they provide powerful observability features.
Prerequisites
Before starting, make sure you have the following installed:
- .NET SDK: Download and install the .NET SDK if you haven’t already.
- Visual Studio Code (VSCode): Install Visual Studio Code for a lightweight code editor.
- C# Extension for VSCode: Install the C# extension for VSCode to enable C# support.
Step 1 – Create a Minimal API Project
Open your terminal and run:
dotnet new web -n MiddlewareDemo
cd MiddlewareDemo
This will generate a minimal ASP.NET Core project with a basic Program.cs file.

Step 2 – Add the Request Timing Middleware
Create a new file called RequestTimingMiddleware.cs and add the following code:
using System.Diagnostics;
public class RequestTimingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RequestTimingMiddleware> _logger;
public RequestTimingMiddleware(RequestDelegate next, ILogger<RequestTimingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
var sw = Stopwatch.StartNew();
try
{
await _next(context);
}
finally
{
sw.Stop();
if (sw.ElapsedMilliseconds > 5000)
{
_logger.LogWarning("Slow request detected: {Path} took {Elapsed} ms",
context.Request.Path,
sw.ElapsedMilliseconds);
}
else
{
_logger.LogInformation("Request {Path} completed in {Elapsed} ms",
context.Request.Path,
sw.ElapsedMilliseconds);
}
}
}
}
This middleware logs the duration of each request. If a request takes longer than 5 seconds, it logs a Warning instead of an Information message.

Step 3 – Add the Correlation ID Middleware
public class CorrelationIdMiddleware
{
private const string HeaderName = "X-Correlation-ID";
private readonly RequestDelegate _next;
private readonly ILogger<CorrelationIdMiddleware> _logger;
public CorrelationIdMiddleware(RequestDelegate next, ILogger<CorrelationIdMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
// Check if client provided a correlation ID
var correlationId = context.Request.Headers[HeaderName].FirstOrDefault();
// If not provided, generate a new one
if (string.IsNullOrWhiteSpace(correlationId))
{
correlationId = Guid.NewGuid().ToString();
}
// Add it to the response so clients can see it
context.Response.Headers[HeaderName] = correlationId;
// Log it for tracing
using (_logger.BeginScope(new Dictionary<string, object>
{
["CorrelationId"] = correlationId
}))
{
await _next(context);
}
}
}
This middleware ensures that every request has a Correlation ID. If the client doesn’t send one, a new ID is generated and added to both the response and the logs.

Step 4 – Register the Middleware in Program.cs
Open Program.cs and update it like this:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// Order matters: Correlation ID first, then Request Timing
app.UseMiddleware<CorrelationIdMiddleware>();
app.UseMiddleware<RequestTimingMiddleware>();
app.MapGet("/", async () =>
{
// Simulate some work
await Task.Delay(2000);
return "Hello World with Middleware!";
});
app.Run();

Step 5 – Run and Test
Run the application:
dotnet run
In the project folder you’ll find a file named MiddlewareDemo.http (created automatically when you generate a Minimal API project in VS Code). You can use this file to test your API directly inside the editor.
Add the following requests to the file:
### Test request without Correlation ID
GET https://localhost:7254/
### Test request with Correlation ID
GET https://localhost:7254/
X-Correlation-ID: test-123
Then click “Send Request” above each call (available when you have the REST Client extension installed).
- In the first request, the server generates a new
X-Correlation-IDand returns it in the response headers. - In the second request, the custom ID
test-123is echoed back in the response and also appears in the logs.

Here is the complete Program.cs
using System.Diagnostics;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// Order matters: Correlation ID first, then Request Timing
app.UseMiddleware<CorrelationIdMiddleware>();
app.UseMiddleware<RequestTimingMiddleware>();
app.MapGet("/", async () =>
{
await Task.Delay(6000);
return "Hello World Middleware!";
});
app.Run();
public class RequestTimingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RequestTimingMiddleware> _logger;
public RequestTimingMiddleware(RequestDelegate next, ILogger<RequestTimingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
var sw = Stopwatch.StartNew();
try
{
await _next(context);
}
finally
{
sw.Stop();
if (sw.ElapsedMilliseconds > 5000)
{
_logger.LogWarning("Slow request detected: {Path} took {Elapsed} ms",
context.Request.Path,
sw.ElapsedMilliseconds);
}
else
{
_logger.LogInformation("Request {Path} completed in {Elapsed} ms",
context.Request.Path,
sw.ElapsedMilliseconds);
}
}
}
}
public class CorrelationIdMiddleware
{
private const string HeaderName = "X-Correlation-ID";
private readonly RequestDelegate _next;
private readonly ILogger<RequestTimingMiddleware> _logger;
public CorrelationIdMiddleware(RequestDelegate next, ILogger<RequestTimingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
// Check if client provided a correlation ID
var correlationId = context.Request.Headers[HeaderName].FirstOrDefault();
// If not provided, generate a new one
if (string.IsNullOrWhiteSpace(correlationId))
{
correlationId = Guid.NewGuid().ToString();
}
// Add it to the response so clients can see it
context.Response.Headers[HeaderName] = correlationId;
// Log it for tracing
using (_logger.BeginScope(new Dictionary<string, object>
{
["CorrelationId"] = correlationId
}))
{
await _next(context);
}
}
}
Conclusion
Middleware is one of the core building blocks of ASP.NET Core applications. Every request flows through the middleware pipeline, and each component has the power to inspect, modify, or short-circuit the request.
In this article we created two practical examples:
- a Request Timing Middleware to log slow requests and highlight performance issues,
- a Correlation ID Middleware to trace requests end-to-end and simplify debugging across distributed systems.
These examples show how even small middleware components can make a big difference in observability and maintainability. The strength of ASP.NET Core lies in this flexible pipeline: you can compose multiple middleware, each with a single responsibility, to build applications that are reliable, extensible, and easy to operate in production.lications easier to monitor and support in production. And the best part is that each of them takes only a few lines of code.
If you think your friends or network would find this article useful, please consider sharing it with them. Your support is greatly appreciated.
Thanks for reading!
Discover CodeSwissKnife Bar, your all-in-one, offline Developer Tools from Your Menu Bar

Piotr
You can use:
long startTime = Stopwatch.GetTimestamp();
// Your code: Doing something e.g Thread.Sleep(3000);
TimeSpan elapsedTime = Stopwatch.GetElapsedTime(startTime);
to measure time. It is more performant.
Ottorino Bruni
Thanks for your comment, as I always write in the Disclaimer my code is not optimized so all junior developers can understand it better.