We already know how to use the middleware that Microsoft provides (UseCors, UseStaticFiles…). But the real power of ASP.NET Core lies in the fact that we can create our own middleware.
For example, do you need to log every request that takes more than 1 second? Do you want to block requests coming from a specific country? Do you need to inject a custom HTTP header into all responses?
For all of that, you need to create a Custom Middleware. Let’s see how to create our own middleware.
The Anatomy of Interception
Remember that the pipeline is a series of components connected in a chain that process HTTP requests. Our middleware will be “in the middle”.
The fundamental pattern for writing a middleware is this:
- Do something before (Manipulate the Request).
- Call the next one (await next()).
- Do something after (Manipulate the Response).
Method 1: “Inline” Middleware
If the logic is very simple, we can define the middleware directly in Program.cs using a lambda function.
var app = builder.Build();
app.Use(async (context, next) =>
{
// 1. ENTRY logic
Console.WriteLine($"--> Received request to: {context.Request.Path}");
// 2. Pass the baton to the next middleware
await next();
// 3. EXIT logic
Console.WriteLine($"<-- Response sent with status code: {context.Response.StatusCode}");
});
app.MapGet("/", () => "Hello Middleware");
app.Run();
Notice the await next(). It’s the most important line.
- Everything you write before it happens when the request is going down.
- Everything you write after it happens when the response is coming back up.
Don’t forget the await next()! If you forget to call next(), the pipeline is cut off (short-circuit). The request will never reach your controller, and the user will receive a blank page or a 404 error.
Method 2: Middleware Class
Filling Program.cs with lambdas is messy. The correct approach is to encapsulate this logic in a dedicated class.
Let’s create our ResponseTimeMiddleware. Its mission will be to time how long our API takes to respond.
Step 1: Create the Class
A middleware in .NET must follow two rules by convention (not by a required interface, interestingly):
- Have a constructor that receives a
RequestDelegate(the pointer to the “next” middleware). - Have an
InvokeAsyncmethod that receives theHttpContext.
using System.Diagnostics;
public class ResponseTimeMiddleware
{
private readonly RequestDelegate _next;
// The constructor receives the delegate to call the next link in the chain
public ResponseTimeMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
// 1. BEFORE logic: Start the stopwatch
var watch = Stopwatch.StartNew();
// 2. Call the next middleware
await _next(context);
// 3. AFTER logic: Stop the stopwatch and log
watch.Stop();
var time = watch.ElapsedMilliseconds;
// Write to console (or we could save to a database)
Console.WriteLine($"The request took: {time} ms");
}
}
Step 2: Create the Extension Method
So that it looks elegant in Program.cs (like app.UseResponseTime()), it’s good practice to create an extension method.
public static class ResponseTimeMiddlewareExtensions
{
public static IApplicationBuilder UseResponseTime(this IApplicationBuilder builder)
{
return builder.UseMiddleware<ResponseTimeMiddleware>();
}
}
Step 3: Register it in the Pipeline
Now we go back to Program.cs and use it as just another member of the family.
var app = builder.Build();
// Our custom middleware
app.UseResponseTime();
app.MapGet("/", async () =>
{
await Task.Delay(500); // Simulate slow work
return "Hello World";
});
app.Run();
If you run the API and call the endpoint, you will see in the console: The request took: 508 ms
