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:
-
Call the next middleware using
await next(context). -
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 _nextrepresents the next part of the pipeline. -
HttpContextcontains information about the current request and response. -
InvokeAsyncruns for each request that reaches this middleware. -
await _next(context)allows the request to continue. -
finallyrecords 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
-
Forgetting
await _next(context): Later middleware and the endpoint will not run. That is correct only when you intend to stop the request. -
Writing to the response after it has started: Headers and status codes may already have been sent.
-
Putting middleware in the wrong order: Authentication, authorization, CORS, routing, and endpoint behavior may change.
-
Doing expensive work on every request: Middleware may run very frequently; keep it focused.
-
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. -
HttpContextprovides 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
RequestDelegateconstructor parameter and anInvokeAsync(HttpContext)method.