• Blog
  • Understanding Middleware in ASP.NET Core: Logging, Correlation IDs, and Best Practices

Understanding Middleware in ASP.NET Core: Logging, Correlation IDs, and Best Practices

Master custom middleware in ASP.NET Core for better logging and request tracking.

Publish date:
Discover more of what matters to you

ASP.NET Core was designed with a modular structure. There is an application pipeline composed of small software components that process requests and responses like a chain. Each of these components, known as middleware, allows us to introduce additional logic before or after executing an HTTP request. 

The .NET Core framework comes with several built-in middleware commonly used in applications, such as:  

  • UseDeveloperExceptionPage
  • UseHttpsRedirection
  • UseCors

Microsoft wrote the software to cover the most common scenarios needed for an application.

But what if we want to create our custom middleware to include our logic and further enhance our apps?

In this article, we’ll explore how to create custom middlewares in an ASP.NET Core API project. By doing so, we can tackle various use cases, reduce complexity and code duplication, leading to cleaner, modular and more maintainable implementations.

Creating a Custom Middleware

First, in your ASP.NET Core API project, add a new class named RequestLoggingMiddleware:

With custom middleware, we can inject our logic into the request pipeline. It’s ideal for scenarios such as logging every request, custom exception handlers, adding custom headers, or measuring performance. 

Let’s create a request logging middleware.

1234567891011121314151617181920
public class RequestLoggingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RequestLoggingMiddleware> _logger;
public RequestLoggingMiddleware(RequestDelegate next, ILogger<RequestLoggingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
_logger.LogInformation("Incoming request: {Method} {Path}", context.Request.Method, context.Request.Path);
await _next(context);
_logger.LogInformation("Outgoing response: {StatusCode}", context.Response.StatusCode);
}
}

Understanding Middleware Execution Flow

Let’s break down each section of our middleware to understand its purpose.

In the Constructor, we inject two dependencies:

  • ILogger<RequestLoggingMiddleware>: Microsoft’s ILogger is a simple way to record events. 
  • RequestDelegate

RequestDelegate will help us pass control to the next component in the pipeline. Typically, the middleware pipeline is a chain of these RequestDelegate instances, each calling the next until the request is fully processed.

This delegate represents the next middleware component in the pipeline. When you call _next(context), you pass the current HttpContext to the next middleware. 

InvokeAsync: The Core Logic

The core functionality and the place where we will include the extra logic is the InvokeAsync method. ASP.NET Core runtime calls this method automatically for every incoming HTTP request. To understand the execution flow of the middleware, we included three steps:

  1. Log the details of the incoming request.
  2. await _next(context) to pass control to the next component in the pipeline.
  3. When control returns back, we log the response’s status code.

Imagine a ladder where the request moves down the middleware pipeline step by step, and once processed, the response travels back up the pipeline in reverse order.

Thinking about migration to the cloud?
Start smart – read our practical guide to cloud migration and modernization.
Read a Guide

Integrating a Real-World Use Case: Correlation ID

To deal with distributed systems and identify related issues with ease, it’s essential to track requests across multiple services or components. 

But how can we follow the “journey” of each request easily?

One great solution to this challenge is a unique identifier assigned to each request. To better understand its purpose, we will name it Correlation ID. This way, we can troubleshoot issues or measure performance, making end-to-end tracking straightforward. To integrate this implementation, we’ll use the middleware logic.

First, create a new class for CorrelationIdMiddleware:

123456789101112131415161718192021222324252627282930313233343536
public class CorrelationIdMiddleware
{
private const string CorrelationIdHeaderName = "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)
{
if (!context.Request.Headers.ContainsKey(CorrelationIdHeaderName))
{
context.Request.Headers[CorrelationIdHeaderName] = Guid.NewGuid().ToString();
}
var correlationId = context.Request.Headers[CorrelationIdHeaderName].ToString();
context.Items[CorrelationIdHeaderName] = correlationId;
context.Response.OnStarting(() =>
{
context.Response.Headers[CorrelationIdHeaderName] = correlationId;
return Task.CompletedTask;
});
_logger.LogInformation("Incoming request with Correlation ID: {CorrelationID}", correlationId);
await _next(context);
_logger.LogInformation("Outgoing response with Correlation ID: {CorrelationID}", correlationId);
}
}

What we do is first set a unique name to our headers to store the ID. We check if the request headers contain that key-value pair, and if not, we set a new Guid. This ensures every request has a unique identifier.

A crucial part of this process is to store the generated or existing Correlation ID in the HttpContext.Items. This way, it is available to every component or controller that needs to retrieve and use it. 

After that, we log the request/response, and in the middle, we pass the control to the next middleware. 

But what about the response and the time the control comes back?

Since response headers are not writable when the response is sent, we register a delegate to the OnStarting callback right before that. As a result, we can follow the complete flow of the request/response, correlating events using this unique identifier.

Registering Middleware in the Pipeline

We created two middlewares and we want to register them. Middlewares should be registered in the correct order to ensure they capture all relevant requests. Since the Correlation ID is essential for logging and tracing, we want it to be registered after authentication and authorization, but before other middlewares that may need access to it.

Let’s see how it looks:

12345678910111213141516171819202122232425262728
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.UseMiddleware<CorrelationIdMiddleware>();
app.UseMiddleware<RequestLoggingMiddleware>();
app.MapControllers();
app.MapGet("/", () => "Hello World!");
app.Run();

With this order, Correlation ID Middleware runs first, ensuring that all requests receive a unique identifier before other middleware needs it. Then, logging middleware runs after, so every log entry includes the Correlation ID. This way, we avoid the conflicts with the other middlewares and allow easy tracking across distributed services using logs. 

Now, update RequestLoggingMiddleware to retrieve and log the Correlation ID:

12345678910111213141516171819202122
private readonly RequestDelegate _next;
private readonly ILogger<RequestLoggingMiddleware> _logger;
private const string CorrelationIdHeaderName = "X-Correlation-ID";
public RequestLoggingMiddleware(RequestDelegate next, ILogger<RequestLoggingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
var correlationId = context.Items[CorrelationIdHeaderName]?.ToString();
_logger.LogInformation("Incoming request: {Method} {Path}, Correlation ID: {CorrelationID}",
context.Request.Method, context.Request.Path, correlationId);
await _next(context);
_logger.LogInformation("Outgoing response: {StatusCode}, Correlation ID: {CorrelationID}",
context.Response.StatusCode, correlationId);
}
Curious about what .NET can do for your business?
Explore our services!
Explore

Building an Endpoint

To illustrate how our middleware setup handles request tracking in a realistic scenario, let’s assume we have an e-commerce site selling t-shirts. Each order is tagged to easily track the request/response flow. We’ll create a minimal API endpoint that returns order details and see how all these work together.

In the Program.cs file, right after the MapControllers, define a minimal API endpoint:

1234567891011121314
app.MapGet("/orders/{orderId}", (HttpContext context, int orderId) =>
{
var correlationId = context.Items["X-Correlation-ID"]?.ToString();
var order = new
{
OrderId = orderId,
ProductName = "T-Shirt",
Quantity = 3,
CorrelationId = correlationId
};
return Results.Ok(order);
});

We return a mock order to demonstrate how the correlation ID flows through the pipeline.

Start the application and call the endpoint:

GET /orders/1 

You’ll receive a JSON response:

123456
{
"orderId": 1,
"productName": "T-Shirt",
"quantity": 3,
"correlationId": "3200d8db-5962-4728-afcf-0a927d4410e4"
}

Meanwhile, your application logs will reflect the end-to-end request journey to both the Correlation ID Middleware and Request Logging Middleware:

12345678
info: CorrelationIdMiddleware[0]
Incoming request with Correlation ID: 3200d8db-5962-4728-afcf-0a927d4410e4
info: RequestLoggingMiddleware[0]
Incoming request: GET /orders/1, Correlation ID: 3200d8db-5962-4728-afcf-0a927d4410e4
info: RequestLoggingMiddleware[0]
Outgoing response: 200, Correlation ID: 3200d8db-5962-4728-afcf-0a927d4410e4
info: CorrelationIdMiddleware[0]
Outgoing response with Correlation ID: 3200d8db-5962-4728-afcf-0a927d4410e4

Mapping the Steps:

  1. Correlation ID Middleware logs the incoming request and assigns the ID to the headers.
  2. Request Logging Middleware captures method, path, and response status.
  3. The correlation ID appears consistently throughout the entire lifecycle.

Conclusion

In this article, we explored the modular pipeline structure of ASP.NET Core and demonstrated how to create custom middleware to extend our application’s functionality. We focused on two middleware components to understand how requests and responses are processed in them and how they integrate with the overall application flow. We also emphasized the importance of middleware order, as the placement of middleware in the pipeline directly impacts its behavior and implementation. With this knowledge, you now have a powerful tool to introduce additional logic into your application, reduce code duplication, and improve maintainability while keeping your codebase clean and modular.

Ready to build or modernize with .NET?
Let’s talk! Book your consultation today.
Book a Call

Subscribe to our newsletter and get amazing content right in your inbox.

This field is required
This field is required Invalid email address
By submitting data, I agree to the Privacy Policy

Thank you for subscribing!
See you soon... in your inbox!

confirm your subscription, make sure to check your promotions/spam folder

Subscribe to our newsletter and get amazing content right in your inbox.

You can unsubscribe from the newsletter at any time

This field is required
This field is required Invalid email address

You're almost there...

A confirmation was sent to your email

confirm your subscription, make sure to check
your promotions/spam folder