← Back to Articles         
ASP.NET Core Middleware Pipeline

ASP.NET Core Middleware Pipeline

Published on 24 Feb 2026     6 min read Web API
C Sharp

ASP.NET Core Web API Middleware

Simple definition

Middleware is code that handles HTTP requests and responses in the Web API pipeline. It can pass a request to the next middleware or stop it and return a response.

For example, middleware can log requests, handle exceptions, check authentication, apply rate limits, or add response headers. In ASP.NET Core, middleware is usually configured in Program.cs.

Scope: The examples below use ASP.NET Core Web API. The older ASP.NET Web API on .NET Framework uses a different pipeline.

How a request moves through middleware

Suppose your API receives:

GET /api/products/10

The request passes through the configured middleware in order. After an endpoint produces a response, control returns through middleware in reverse order:

flowchart TD
    A["HTTP request"] --> B["Exception handling"]
    B --> C["Request logging"]
    C --> D["Authentication and authorization"]
    D --> E["Products endpoint"]
    E --> F["HTTP response returns through earlier middleware"]

Each middleware makes a choice:

  1. Call the next middleware using await next(context).

  2. Stop the pipeline and produce a response itself. For example, a check might return 403 Forbidden.

This is why middleware order matters. 

Your first middleware example

Create an ASP.NET Core Web API project:

dotnet new webapi -n MiddlewareDemo
cd MiddlewareDemo

Replace Program.cs with this small example:

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

app.Use(async (context, next) =>
{
    Console.WriteLine("1. Before the endpoint");

    await next(context);

    Console.WriteLine("3. After the endpoint");
});

app.MapGet("/api/products", () =>
{
    Console.WriteLine("2. Inside the endpoint");

    return Results.Ok(new[]
    {
        new { Id = 1, Name = "Laptop" },
        new { Id = 2, Name = "Mouse" }
    });
});

app.Run();

Run it:

dotnet run

Open the /api/products URL shown by dotnet run. The console displays:

1. Before the endpoint
2. Inside the endpoint
3. After the endpoint

The important line is:

await next(context);

Code above it runs as the request goes in. Code below it runs as control comes back. Microsoft calls the function that represents the remaining pipeline a RequestDelegate. 

The three methods you will commonly see

Method Meaning Typical use
app.Use(...) Run code and optionally call the next component Logging, custom checks
app.Run(...) Run terminal middleware; there is no next to call A final response
app.Map(...) Branch the pipeline for a path Special handling for a URL path

Be careful with the two forms of Run: app.Run(async context => { ... }) adds terminal middleware, while the parameterless app.Run() at the end starts the application. 

Example: stopping a request

app.Use(async (context, next) =>
{
    if (context.Request.Path == "/blocked")
    {
        context.Response.StatusCode = StatusCodes.Status403Forbidden;
        await context.Response.WriteAsync("Access denied");
        return;
    }

    await next(context);
});

For /blocked, the middleware writes a response and returns. Later middleware and endpoints do not run for that request.

A practical custom middleware: request timing

Inline middleware is useful for learning. For reusable behavior, put it in a class.

RequestTimingMiddleware.cs

using System.Diagnostics;

public sealed 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 stopwatch = Stopwatch.StartNew();

        try
        {
            await _next(context);
        }
        finally
        {
            stopwatch.Stop();

            _logger.LogInformation(
                "{Method} {Path} returned {StatusCode} in {ElapsedMs} ms",
                context.Request.Method,
                context.Request.Path,
                context.Response.StatusCode,
                stopwatch.ElapsedMilliseconds);
        }
    }
}

Program.cs

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

var app = builder.Build();

app.UseMiddleware<RequestTimingMiddleware>();

app.MapControllers();

app.Run();

A request might produce this log:

GET /api/products returned 200 in 18 ms

What each part does

  • RequestDelegate _next represents the next part of the pipeline.

  • HttpContext contains information about the current request and response.

  • InvokeAsync runs for each request that reaches this middleware.

  • await _next(context) allows the request to continue.

  • finally records the time even if code farther down throws an exception.

  • UseMiddleware<RequestTimingMiddleware>() adds the class to the pipeline.

Class based middleware uses a constructor that accepts RequestDelegate and a public Invoke or InvokeAsync method. 

A more realistic Web API pipeline

Here is a typical shape for a controller based API when those features are configured:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

// Register authentication, authorization, CORS, and rate limiting
// services here if your API uses them.

var app = builder.Build();

app.UseExceptionHandler(); // Requires appropriate exception-handler setup

app.UseHttpsRedirection();

app.UseRouting();

app.UseCors("MyPolicy");

app.UseAuthentication();
app.UseAuthorization();

app.UseRateLimiter();

app.MapControllers();

app.Run();

This is an order illustration, not a complete copy-and-run configuration: "MyPolicy", authentication, rate limiting, and exception handling each need their corresponding service setup. In particular, CORS should run before authentication and authorization, and authentication should run before authorization. Your exact pipeline depends on the features your application uses. 

Why order changes behavior

Consider:

app.UseAuthentication();
app.UseAuthorization();

Authentication determines who the caller is. Authorization then determines whether that caller is allowed to access the endpoint. Reversing their order prevents authorization from using the identity established by authentication as intended. 

Similarly, place exception handling early enough to catch exceptions from components that run after it.

Middleware versus a controller action

Suppose your API has ten controllers and you want to log the duration of every request.

Without middleware, you might repeat timing code in many actions. With middleware, you register it once:

app.UseMiddleware<RequestTimingMiddleware>();

It then applies to requests that pass through that point in the pipeline, including requests that never reach a controller. A controller action, by comparison, handles a particular endpoint.

Common built-in middleware

Middleware What it does
Exception handler Handles unhandled exceptions
HTTPS redirection Redirects HTTP requests to HTTPS
Routing Identifies the matching endpoint
CORS Applies cross-origin request rules
Authentication Establishes the caller’s identity
Authorization Enforces access requirements
Rate limiter Limits requests according to configured policies
Static files Serves files when configured

You register only the components your API needs. Some components can end a request early, while others call the next component. 

Common mistakes

  1. Forgetting await _next(context): Later middleware and the endpoint will not run. That is correct only when you intend to stop the request.

  2. Writing to the response after it has started: Headers and status codes may already have been sent.

  3. Putting middleware in the wrong order: Authentication, authorization, CORS, routing, and endpoint behavior may change.

  4. Doing expensive work on every request: Middleware may run very frequently; keep it focused.

  5. Putting request-specific data in instance fields: Conventional middleware instances can be reused across requests. Keep per-request values local to InvokeAsync; resolve scoped dependencies through that method when needed. 

Interview-ready key points

  • Middleware is a component in the ASP.NET Core HTTP request pipeline.

  • Register middleware in Program.cs; registration order defines request order.

  • HttpContext provides access to the current request and response.

  • await next(context) passes control forward; execution resumes afterward as control returns.

  • Middleware can short-circuit a request by responding without calling next.

  • Use middleware for concerns shared across requests, such as logging, exception handling, authentication, and rate limiting.

  • A reusable custom middleware class typically has a RequestDelegate constructor parameter and an InvokeAsync(HttpContext) method. 

← Back to Articles