.NET Core  

๐Ÿงพ Creating Custom Middleware in ASP.NET Core โ€“ The Complete Guide with Real Examples

๐Ÿง  Introduction

ASP.NET Core is known for its modular pipeline, where middleware components play a critical role in handling HTTP requests and responses. Middleware can perform a variety of tasks such as request logging, authentication, routing, response modification, error handling, and more.

While ASP.NET Core offers built-in middleware like UseRouting, UseAuthorization, etc., there are many scenarios where you’ll want to create your own.

In this article, we’ll explore:

  • What middleware is

  • Why and when to use custom middleware

  • How to create and register custom middleware

  • Real-world examples

  • Best practices

๐Ÿ”ง What is Middleware?

Middleware in ASP.NET Core is software that's assembled into the HTTP request pipeline. Each piece of middleware:

  • Processes an incoming request

  • Can optionally pass the request to the next component

  • Optionally processes the response on the way back

Conceptually, it looks like a chain:

Request --> Middleware 1 --> Middleware 2 --> Middleware 3 --> ... --> Response

๐Ÿ“ Why Create Custom Middleware?

You should consider writing custom middleware when:

  • You want reusable, centralized behavior (like logging or metrics)

  • You need to intercept or transform requests or responses

  • Built-in middleware doesn’t meet your specific need

๐Ÿ“ฆ Project Setup

Let’s build a simple ASP.NET Core Web API project to demonstrate.

dotnet new webapi -n CustomMiddlewareDemo
cd CustomMiddlewareDemo

๐Ÿ”จ Example 1: Logging Request Duration

โœจ Purpose:

Log how long each HTTP request takes.

๐Ÿ“ Step 1. Create the Middleware

File: Middleware/RequestTimingMiddleware.cs

using Microsoft.AspNetCore.Http;
using System.Diagnostics;
using System.Threading.Tasks;
using System;

namespace CustomMiddlewareDemo.Middleware
{
    public class RequestTimingMiddleware
    {
        private readonly RequestDelegate _next;

        public RequestTimingMiddleware(RequestDelegate next)
        {
            _next = next;
        }

        public async Task InvokeAsync(HttpContext context)
        {
            var stopwatch = Stopwatch.StartNew();

            await _next(context); // Call next middleware

            stopwatch.Stop();

            var elapsedMs = stopwatch.ElapsedMilliseconds;
            Console.WriteLine($"โฑ๏ธ [{DateTime.Now}] Request {context.Request.Method} {context.Request.Path} took {elapsedMs}ms");
        }
    }
}

๐Ÿ“ Step 2. Create an Extension Method

File: Middleware/RequestTimingMiddlewareExtensions.cs

using Microsoft.AspNetCore.Builder;

namespace CustomMiddlewareDemo.Middleware
{
    public static class RequestTimingMiddlewareExtensions
    {
        public static IApplicationBuilder UseRequestTiming(this IApplicationBuilder builder)
        {
            return builder.UseMiddleware<RequestTimingMiddleware>();
        }
    }
}

๐Ÿ“„ Step 3. Register Middleware in Program.cs

using CustomMiddlewareDemo.Middleware;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();

var app = builder.Build();

app.UseRequestTiming(); // Our custom middleware

app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();

๐Ÿงช Step 4. Add a Test Endpoint

File: Controllers/TestController.cs

using Microsoft.AspNetCore.Mvc;
using System.Threading;

namespace CustomMiddlewareDemo.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class TestController : ControllerBase
    {
        [HttpGet]
        public IActionResult Get()
        {
            Thread.Sleep(200); // Simulate processing delay
            return Ok("Middleware test successful!");
        }
    }
}

๐Ÿ” Output Example (Console)

When you visit /api/test, the console logs:

โฑ๏ธ [7/28/2025 12:34:56 PM] Request GET /api/test took 201ms

๐Ÿ”จ Example 2: Inject a Custom Header into Every Response

โœจ Purpose:

Add a custom header to every outgoing HTTP response.

Middleware/CustomHeaderMiddleware.cs

using Microsoft.AspNetCore.Http;
using System.Threading.Tasks;

namespace CustomMiddlewareDemo.Middleware
{
    public class CustomHeaderMiddleware
    {
        private readonly RequestDelegate _next;

        public CustomHeaderMiddleware(RequestDelegate next)
        {
            _next = next;
        }

        public async Task InvokeAsync(HttpContext context)
        {
            context.Response.OnStarting(() =>
            {
                context.Response.Headers.Add("X-Powered-By", "ASP.NET Core Custom Middleware");
                return Task.CompletedTask;
            });

            await _next(context);
        }
    }
}

Register it in Program.cs:

app.UseMiddleware<CustomHeaderMiddleware>();

Now every HTTP response will include:

X-Powered-By: ASP.NET Core Custom Middleware

โœ… Best Practices

Practice Why it Matters
Keep middleware small and focused Easier to test, reuse, and maintain
Avoid blocking code (Thread.Sleep) Use await Task.Delay() instead for async code
Handle exceptions gracefully Prevent application crashes and improve logging
Use dependency injection Middleware can use services like logging, DB, etc.

๐Ÿ“š Summary

Task Middleware
Measure request time RequestTimingMiddleware
Add custom headers CustomHeaderMiddleware
Log request/response Custom logging middleware
Handle errors globally Exception-handling middleware

๐Ÿงช Want to Go Further?

  • Try building middleware for JWT validation

  • Create a centralized exception handling middleware

  • Write middleware that reads/writes request bodies