ASP.NET Core CSharp DotNET Middleware

ASP.NET Core Middleware Explained: Pipeline, Order and C# Examples

ASP.NET Core Middleware Explained: Pipeline, Order and C# Examples

When an HTTP request reaches an ASP.NET Core application, more happens than calling a controller. The application may need to handle errors, identify the user, check permissions and record useful diagnostics.

Middleware is how ASP.NET Core organizes much of that work. This guide explains the pipeline and shows two small C# examples you can try in a new project.

What is middleware in ASP.NET Core?

Middleware is a component in the HTTP request pipeline. It can perform work before calling the next component and after that component returns. It can also finish handling the request without continuing.

Registration order determines request execution order. Code after an awaited next call normally runs in reverse order as the calls return. An exception can skip ordinary after-code; a finally block is useful when cleanup must run.

The destination is an endpoint, which might be a controller action, a Minimal API handler or another supported endpoint. A controller is not required for every application.

Microsoft explains the underlying behavior in its middleware documentation.

Try the pipeline yourself

With a supported .NET SDK installed, create an empty ASP.NET Core project:

dotnet new web -n MiddlewareDemo
cd MiddlewareDemo

Replace Program.cs with the following. These examples assume the template's default implicit usings are enabled.

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

app.Use(async (context, next) =>
{
    app.Logger.LogInformation("A: before");
    await next(context);
    app.Logger.LogInformation("A: after");
});

app.Use(async (context, next) =>
{
    app.Logger.LogInformation("B: before");
    await next(context);
    app.Logger.LogInformation("B: after");
});

app.MapGet("/", () =>
{
    app.Logger.LogInformation("Endpoint");
    return "Hello from the endpoint!";
});

app.Run();

Run dotnet run, open the listening URL printed in the terminal, and request the root path. Among the framework logs, you should see this order:

A: before
B: before
Endpoint
B: after
A: after

Try swapping the two app.Use blocks. The before/after pairs move together, making registration order easy to observe. Requesting another path can produce a 404; endpoint selection is separate from the fact that middleware ran.

What does short-circuiting mean?

A middleware short-circuits when it handles a request without calling next. For example, add this block before the mapped endpoint in the demonstration:

app.Use(async (context, next) =>
{
    if (context.Request.Path == "/maintenance")
    {
        context.Response.StatusCode =
            StatusCodes.Status503ServiceUnavailable;
        await context.Response.WriteAsync("Maintenance in progress.");
        return;
    }

    await next(context);
});

Request /maintenance. The response should contain the maintenance message and HTTP status 503. Requests to other paths continue. Upstream middleware that already called next can still resume; downstream components that were never invoked do not run.

Build reusable request-timing middleware

For a more practical exercise, add a file called RequestTimingMiddleware.cs:

using System.Diagnostics;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;

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 timer = Stopwatch.StartNew();
        bool completed = false;

        try
        {
            await _next(context);
            completed = true;
        }
        finally
        {
            timer.Stop();
            _logger.LogInformation(
                "{Method} {Path} took {ElapsedMs} ms. " +
                "Downstream completed: {Completed}",
                context.Request.Method,
                context.Request.Path,
                timer.ElapsedMilliseconds,
                completed);
        }
    }
}

Then replace Program.cs with this separate example:

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

app.UseMiddleware<RequestTimingMiddleware>();
app.MapGet("/", () => "Timing middleware is active.");

app.Run();

The logger records the method, path and elapsed time. The finally block runs even if downstream processing throws. The Completed field means the awaited downstream call returned normally; it does not mean that the HTTP status was successful.

This measures the middleware's downstream execution, not the client's full download time. Avoid interpreting it as a complete network-latency measurement.

The example follows the conventional constructor and InvokeAsync shape documented in Microsoft's custom middleware guide. Register it with UseMiddleware rather than manually constructing it.

Where should middleware go?

Exception handling generally belongs early so it can catch failures from components after it. For an application using endpoint routing and configured authentication, the usual relative order is routing, authentication, authorization, then endpoint execution. CORS and other features have their own placement requirements.

This is not a complete security setup:

app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();

Those lines require corresponding service registration, controller services and an authentication scheme. Do not paste them into the empty demo and assume authentication is configured. Check your application's actual features and hosting requirements.

Why middleware helps in real projects

Consider a hypothetical API with customer lookup, report generation and file import endpoints. Request-level timing can be shared across all three instead of copied into every handler. Each endpoint keeps its own validation and business behavior.

Centralization makes some concerns easier to maintain, but middleware is not an automatic scalability improvement. A component that performs expensive work on every request can slow the whole application.

Common mistakes to avoid

  • Forgetting next: Later components are skipped unless the middleware intentionally ends the request.
  • Not awaiting next: Response work and diagnostics can run before downstream execution finishes.
  • Changing headers too late: Once the response has started, some changes are no longer possible. Set required headers before writing or use an appropriate response callback.
  • Logging everything: Avoid indiscriminately recording authorization tokens, query strings, request bodies or personal data.
  • Storing request state in instance fields: Keep per-request values inside InvokeAsync. Conventional middleware is constructed at startup; scoped dependencies should be supplied through InvokeAsync when needed.
  • Adding database rules to global middleware: A shared request concern is different from a transaction-specific business operation.

Frequently asked questions

Is middleware the same as a controller?

No. Middleware participates in request processing; a controller action is an endpoint that handles a selected operation.

What is the difference between app.Use and app.Run?

Use adds a component that can call next. Run with a request delegate adds terminal middleware. The parameterless app.Run() in these examples starts the application; it is a different overload.

Does the response always pass through every middleware?

No. Only components invoked on that request participate. Branches, short-circuits and exceptions can change the path.

Final takeaway

Start by observing before/after execution, then add one reusable component with a clear job. Understanding what calls next, what finishes a request and where each component belongs is more useful than memorizing a long pipeline configuration.

Written by Vasim

Thanks for reading. Explore more practical ideas, tutorials and technology stories on this blog.

0 Comments :

Post a Comment

Vasim Tamboli

Technology | Android | Blogging