What Is ASP.NET Core Middleware and How to Write It

This article explains the concept of middleware in ASP.NET Core, illustrates how the HTTP request pipeline operates, and provides practical C# code examples for creating and registering custom middleware to process requests and responses.


What Is Middleware in ASP.NET Core?

Middleware is software assembled into an application pipeline to handle incoming HTTP requests and outgoing responses. Each middleware component in ASP.NET Core:

  • Decides whether to pass the request to the next component in the pipeline.
  • Performs logic both before and after the next component is invoked.

The sequence of middleware components executes in the order they are defined in Program.cs. When a request enters the application, it travels through the middleware chain sequentially. Once a terminating middleware handles the request (or execution finishes), the response travels back out through the same components in reverse order.

Common built-in middleware includes authentication, authorization, static file serving, routing, and exception handling.


The Middleware Execution Model

The pipeline relies on a RequestDelegate—a function that processes an HttpContext. Three primary extension methods configure middleware in ASP.NET Core:

  • app.Use(...): Chained middleware that executes code and calls the next delegate to continue the pipeline.
  • app.Run(...): Terminal middleware that handles the request and ends the pipeline without calling subsequent components.
  • app.Map(...): Branches the request pipeline based on matching request paths.

Writing Inline Middleware

For simple operations, you can register inline middleware directly in Program.cs using app.Use:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.Use(async (context, next) =>
{
    // Code executed before the next middleware
    Console.WriteLine($"Request: {context.Request.Method} {context.Request.Path}");

    await next(context);

    // Code executed after the next middleware
    Console.WriteLine($"Response: {context.Response.StatusCode}");
});

app.MapGet("/", () => "Hello World");

app.Run();

Writing Class-Based Custom Middleware

For maintainability, complex logic should reside in dedicated middleware classes. Standard middleware adheres to the convention-based pattern:

  1. A public constructor accepting a RequestDelegate.
  2. A public method named InvokeAsync or Invoke taking an HttpContext and returning a Task.

Step 1: Create the Middleware Class

The following example defines a custom middleware class that calculates and logs the execution duration of an HTTP request:

using System.Diagnostics;

public class PerformanceTrackingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<PerformanceTrackingMiddleware> _logger;

    public PerformanceTrackingMiddleware(RequestDelegate next, ILogger<PerformanceTrackingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

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

        try
        {
            // Call the next middleware in the pipeline
            await _next(context);
        }
        finally
        {
            stopwatch.Stop();
            var elapsedMs = stopwatch.ElapsedMilliseconds;

            _logger.LogInformation(
                "HTTP {Method} {Path} completed in {Elapsed} ms with status code {StatusCode}",
                context.Request.Method,
                context.Request.Path,
                elapsedMs,
                context.Response.StatusCode);
        }
    }
}

Step 2: Create an Extension Method

Expose an extension method on IApplicationBuilder to cleanly integrate the middleware into the pipeline:

public static class PerformanceTrackingMiddlewareExtensions
{
    public static IApplicationBuilder UsePerformanceTracking(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<PerformanceTrackingMiddleware>();
    }
}

Step 3: Register the Middleware in the Pipeline

Add the middleware in Program.cs. Ensure placement reflects the intended execution order relative to routing, authentication, and endpoints:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// Register custom middleware early in the pipeline
app.UsePerformanceTracking();

app.UseRouting();
app.MapGet("/", () => "Pipeline processed successfully.");

app.Run();

Key Considerations for Custom Middleware

  • Pipeline Order: Position matters. For instance, security middleware should precede endpoint execution, while error-handling middleware should run near the start of the pipeline to catch unhandled exceptions from downstream components.
  • Scoped Services: Middleware constructors only inject Singleton services because middleware instances are instantiated once at application startup. To use Scoped services (such as an Entity Framework DbContext), inject them directly into the InvokeAsync method signature.
  • Short-Circuiting: Omit calling await _next(context) to short-circuit the pipeline if validation fails, request limits are exceeded, or unauthorized access is detected.