Getting Started With Middlewares in ASP.NET Core (Beginner Guide)
A friendly beginner guide to middleware in ASP.NET Core: how the request pipeline works, Use vs Run vs Map, order, and writing your own middleware.
A line of dabbawalas carrying your tiffin
Think about how a tiffin (lunch box) travels in a big city like Mumbai. Your mother packs the food at home. Then one person picks it up. He hands it to another person at the station. That person puts it on a train. Another person takes it off the train. A final person walks it to your office desk. Many hands touch the same tiffin, one after another, in a fixed order.
Each person does one small job. Each person knows exactly who to hand the tiffin to next. And at the end of the day, the empty box travels back the same way, through the same hands, in reverse.
Middleware in ASP.NET Core works in this exact way. Every web request that reaches your app is like a tiffin. It passes through a line of helpers called middleware. One middleware writes a log. One checks if you are logged in. One catches errors. Each does its small job and hands the request to the next helper in line.
And just like the empty tiffin coming back, the response travels back through the same helpers in reverse order. This in-and-out shape is the most important idea in this whole guide. Keep the tiffin picture in your head and the rest becomes easy.
What is middleware, really?
Middleware is software that is joined together into a pipeline to handle requests and responses. That is the official definition, and it is a good one. Let us break it into smaller pieces.
- A request comes in (someone opened your website or called your API).
- The request walks through a pipeline of middleware, one piece at a time.
- Each piece can do work before passing the request on.
- Each piece chooses whether to call the next piece or stop right there.
- When the deepest piece finishes, the response walks back out through the same pieces.
Because each piece can act both on the way in and on the way out, middleware is sometimes drawn like an onion or like nested boxes. Here is the simple flow.
Notice the arrows go in and then come back. That return trip is real. A middleware can change the response, add a header, or measure how long the whole request took. We will see this in code very soon.
Where middleware lives: the Program file
In a modern ASP.NET Core app, you set up your middleware in the Program.cs file. You build the app, add middleware one line at a time, and then run it. Here is a tiny but complete example.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// Each line below adds one middleware to the pipeline.
app.UseHttpsRedirection(); // sends http to https
app.UseAuthentication(); // who are you?
app.UseAuthorization(); // are you allowed?
app.MapGet("/", () => "Hello from the endpoint!");
app.Run();Read those middleware lines from top to bottom. That top-to-bottom order is the same order the request travels. This is not a small detail. It is the single most common cause of bugs for beginners, so we will give order its own section below.
The three building blocks: Use, Run, and Map
ASP.NET Core gives you three main methods to shape the pipeline. Once you understand these three, you understand most of middleware.
| Method | What it does | Calls the next middleware? |
|---|---|---|
Use | Adds a middleware that can do work, then pass control on | Yes, if you call next |
Run | Adds a final middleware that ends the line | No, it is a dead end |
Map | Creates a branch based on the request path | Branches into a new mini-pipeline |
Let us look at each one with a small example you can read out loud.
Use — do work, then keep going
Use is the everyday workhorse. It hands you the HttpContext (everything about the request and response) and a next function. You do some work, then you call next to pass the tiffin along.
app.Use(async (context, next) =>
{
// Work BEFORE the next middleware.
Console.WriteLine($"Incoming: {context.Request.Path}");
await next(context); // pass control down the line
// Work AFTER the next middleware returns.
Console.WriteLine($"Outgoing: {context.Response.StatusCode}");
});See how there is code before next and code after next? The "before" part runs on the way in. The "after" part runs on the way back out. This is the onion shape in real code.
If you forget to call await next(context), the request stops right there and the middleware below never runs. Sometimes that is what you want. Often it is a bug. Be careful.
Run — the end of the road
Run adds a terminal middleware. Terminal means it never calls anything after it. It is the last helper in the line. You use it to produce a final response.
app.Run(async context =>
{
await context.Response.WriteAsync("This is the end of the pipeline.");
});Any middleware you add after a Run will never execute, because Run does not have a next to call. Think of it as the dabbawala who finally puts the tiffin on your desk. Nobody comes after him.
Map — making a branch
Map lets you split the pipeline based on the URL path. Requests that match the path go down a separate branch with its own middleware.
app.Map("/health", branch =>
{
branch.Run(async context =>
{
await context.Response.WriteAsync("Healthy");
});
});Now only requests to /health reach that branch. Everything else carries on through the main pipeline. This is handy for health checks, admin pages, or any part of your app that needs its own special handling.
Order matters more than anything
Here is the rule, written plainly: middleware runs in the order you add it. Top to bottom on the way in. Bottom to top on the way out. If you change the order, you change the behavior.
Why does this matter so much? Think of the airport. You would not scan a bag after the person already boarded the plane. The check has to happen at the right point in the line. Middleware is the same.
Two classic mistakes show this clearly.
- If you put authorization before authentication, the app tries to check if you are allowed before it even knows who you are. That breaks.
- If you put your error handler too late, errors that happen early in the pipeline will not be caught nicely.
Microsoft suggests a recommended order for the common built-in middleware. You do not have to memorize it today, but it is good to see the shape.
| Position | Middleware | Job |
|---|---|---|
| 1 | Exception/Error Handler | Catch errors from everything below |
| 2 | HSTS | Tell browsers to use HTTPS |
| 3 | HTTPS Redirection | Move http requests to https |
| 4 | Static Files | Serve images, CSS, JS quickly |
| 5 | Routing | Decide which endpoint matches |
| 6 | CORS | Control cross-site requests |
| 7 | Authentication | Find out who you are |
| 8 | Authorization | Check what you can do |
| 9 | Your custom middleware | Your own logic |
| 10 | Endpoint | The actual handler runs |
Notice the error handler sits at the very top. Because the response travels back out in reverse, a handler at the top wraps everything below it. It is the last thing to see the response on the way out, so it can catch any error that bubbled up.
Request travelling inward
Steps
Error Handler
Wraps everything in a try
Auth
Identify the user
Routing
Pick the endpoint
Endpoint
Run the handler
Response travelling outward
Steps
Endpoint
Produced a result
Routing
Pass response up
Auth
Maybe add headers
Error Handler
Catch any error, finish
Writing your own middleware (three styles)
Once the built-in pieces are not enough, you write your own. There are three common styles. They all end up in the same pipeline, so pick the one that fits the job.
Style 1: inline with a lambda
This is what we already saw with app.Use. It is perfect for small, quick jobs. Here is a middleware that measures how long each request takes.
app.Use(async (context, next) =>
{
var start = DateTime.UtcNow;
await next(context);
var took = DateTime.UtcNow - start;
Console.WriteLine($"{context.Request.Path} took {took.TotalMilliseconds} ms");
});The timer starts before next, and the math happens after next returns. Again, the onion shape.
Style 2: a convention-based class
When the logic grows, move it into its own class. A convention-based middleware is just a class with a constructor that takes a RequestDelegate and a public InvokeAsync method.
public class RequestLoggingMiddleware
{
private readonly RequestDelegate _next;
public RequestLoggingMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
Console.WriteLine($"Handling {context.Request.Path}");
await _next(context); // pass to the next middleware
Console.WriteLine($"Finished {context.Response.StatusCode}");
}
}Then you add it in Program.cs:
app.UseMiddleware<RequestLoggingMiddleware>();One thing to know: this class is created once and shared for the whole app (it is a singleton). So you should not inject a scoped service, like a database DbContext, into its constructor. Instead, take scoped services as extra parameters of InvokeAsync, where ASP.NET Core hands them to you fresh per request.
Style 3: factory-based with IMiddleware
The third style implements the IMiddleware interface. This version is built fresh for each request from the dependency injection container, so it can safely take scoped services in its constructor.
public class AuditMiddleware : IMiddleware
{
private readonly ILogger<AuditMiddleware> _logger;
public AuditMiddleware(ILogger<AuditMiddleware> logger)
{
_logger = logger;
}
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
_logger.LogInformation("Audit: {Path}", context.Request.Path);
await next(context);
}
}Because it is resolved per request, you must register it in DI:
builder.Services.AddScoped<AuditMiddleware>();
// later
app.UseMiddleware<AuditMiddleware>();This table sums up the differences so you can choose with confidence.
| Style | Best for | Scoped services in constructor? |
|---|---|---|
Inline lambda (app.Use) | Tiny, quick logic | Not in constructor; use the lambda body |
| Convention-based class | Reusable logic, no scoped deps in ctor | No, put them in InvokeAsync |
Factory-based IMiddleware | Reusable logic that needs scoped deps | Yes, safe in the constructor |
How short-circuiting works
Sometimes a middleware decides the request should stop early. This is called short-circuiting. It simply means a middleware writes a response and does not call next. Everything below it is skipped, and the response heads straight back out.
A common example is a simple check: if a required header is missing, reject the request right away.
app.Use(async (context, next) =>
{
if (!context.Request.Headers.ContainsKey("X-Api-Key"))
{
context.Response.StatusCode = 401; // Unauthorized
await context.Response.WriteAsync("Missing API key");
return; // do NOT call next — short-circuit here
}
await next(context);
});The return after writing the response is the key. It stops the line. The endpoint below never runs. This is exactly how things like authorization and rate limiting protect your app.
A complete mental model
Let us put the whole picture together in one diagram. A request enters, flows down through your middleware, hits the endpoint, and the response flows back up. Any middleware can stop the flow early.
If you can read that diagram and explain it to a friend, you truly understand middleware. The request goes in, the response comes out, and at any point a middleware can either pass the tiffin along or stop the line.
A few friendly tips
- Add middleware in the right order. When something behaves oddly, check the order first.
- Put your error handler near the top so it wraps everything below.
- Always remember to call
await next(context)unless you mean to stop on purpose. - Keep each middleware focused on one job. Small pieces are easier to test and reuse.
- For logic that needs a database or other scoped service, prefer the
IMiddlewarestyle or pass the service intoInvokeAsync.
References and further reading
- ASP.NET Core Middleware — Microsoft Learn
- Write custom ASP.NET Core middleware — Microsoft Learn
- Factory-based middleware activation (IMiddleware) — Microsoft Learn
- Middlewares in ASP.NET Core — The Complete Guide (codewithmukesh)
- Order of ASP.NET Core Middlewares is really Important (Sriram Kumar Mannava)
Quick recap
- Middleware is a line of helpers that every request passes through, like a tiffin moving through many hands.
- Each middleware can do work before and after calling the next one, giving the pipeline its in-and-out (onion) shape.
- Use does work and passes control on, Run ends the line, and Map branches by path.
- Order matters: middleware runs top to bottom on the way in, and reverse on the way out. Put error handling near the top, and authentication before authorization.
- You can write middleware three ways: an inline lambda, a convention-based class with
InvokeAsync, or a factory-based class implementingIMiddleware. - Short-circuiting means a middleware writes a response and skips
next, stopping the request early — exactly how auth and rate limiting protect your app.
Related Posts
3 Ways To Create Middleware In ASP.NET Core (Beginner Guide)
Learn the 3 ways to create middleware in ASP.NET Core: inline request delegates, convention-based classes, and factory-based IMiddleware, with simple diagrams and code.
HTTPS Redirection and HSTS in ASP.NET Core: A Simple Guide
Learn how to configure HTTPS redirection and HSTS in ASP.NET Core with simple examples, diagrams, and clear advice for development and production.
Authentication and Authorization Best Practices in ASP.NET Core
A friendly guide to authentication and authorization in ASP.NET Core for .NET 10 — JWT, cookies, claims, roles, policies, and security best practices with diagrams.
How to Create Custom Middlewares in ASP.NET Core (Step by Step)
Learn to build custom middleware in ASP.NET Core three ways: inline Use, a convention class, and IMiddleware with DI. Beginner friendly, with clear examples.
Refit in .NET: Building Robust API Clients in C#
Learn Refit in .NET to build type-safe REST API clients in C#. Define an interface, add attributes, and Refit writes the HttpClient code for you.
Global Error Handling in ASP.NET Core 8 (Beginner Guide)
Learn global error handling in ASP.NET Core 8 with IExceptionHandler, ProblemDetails, and UseExceptionHandler, explained with simple diagrams and clear code.