Skip to main content
SEMastery
ASP.NETbeginner

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.

13 min readUpdated December 8, 2025

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.

A request flows through three middleware, reaches the endpoint, then the response flows back out through the same middleware in reverse.

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.

MethodWhat it doesCalls the next middleware?
UseAdds a middleware that can do work, then pass control onYes, if you call next
RunAdds a final middleware that ends the lineNo, it is a dead end
MapCreates a branch based on the request pathBranches 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.

Map splits the pipeline. Requests to /health take a separate branch, while all other requests continue down the main line.

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.

PositionMiddlewareJob
1Exception/Error HandlerCatch errors from everything below
2HSTSTell browsers to use HTTPS
3HTTPS RedirectionMove http requests to https
4Static FilesServe images, CSS, JS quickly
5RoutingDecide which endpoint matches
6CORSControl cross-site requests
7AuthenticationFind out who you are
8AuthorizationCheck what you can do
9Your custom middlewareYour own logic
10EndpointThe 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

Error Handler
Auth
Routing
Endpoint

Steps

1

Error Handler

Wraps everything in a try

2

Auth

Identify the user

3

Routing

Pick the endpoint

4

Endpoint

Run the handler

On the way in, middleware runs top to bottom until it reaches the endpoint.

Response travelling outward

Endpoint
Routing
Auth
Error Handler

Steps

1

Endpoint

Produced a result

2

Routing

Pass response up

3

Auth

Maybe add headers

4

Error Handler

Catch any error, finish

On the way back, the same middleware runs in reverse order with the response.

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.

StyleBest forScoped services in constructor?
Inline lambda (app.Use)Tiny, quick logicNot in constructor; use the lambda body
Convention-based classReusable logic, no scoped deps in ctorNo, put them in InvokeAsync
Factory-based IMiddlewareReusable logic that needs scoped depsYes, 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 guard middleware short-circuits when the API key is missing, so the request never reaches the endpoint.

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.

The full lifecycle: request travels down, endpoint runs, response travels back up, and any layer may short-circuit.

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 IMiddleware style or pass the service into InvokeAsync.

References and further reading

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 implementing IMiddleware.
  • 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