Skip to content

Middleware in ASP.NET Core — Complete Guide

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you will learn about Middleware in ASP.NET Core. We cover key concepts, practical examples, and best practices to help you master this topic.

Hook

Middleware is the heart of the ASP.NET Core request pipeline. Every request flows through a series of middleware components that can inspect, modify, or short-circuit the request and response. Understanding middleware enables you to handle cross-cutting concerns like logging, authentication, error handling, and caching in a modular, reusable way using C#.

Learning Path

graph LR
  A[Middleware] --> B[Pipeline Concept]
  B --> C[Custom Middleware]
  B --> D[Built-in Middleware]
  C --> E[Factory-based Middleware]
  C --> F[Convention-based Middleware]
  style A fill:#4a90d9,color:#fff
  style B fill:#4a90d9,color:#fff
  style C fill:#4a90d9,color:#fff
  style D fill:#4a90d9,color:#fff
  style E fill:#4a90d9,color:#fff
  style F fill:#4a90d9,color:#fff

The Pipeline Concept

ASP.NET Core middleware is assembled as a pipeline. Each component can process the request, pass it to the next component, and process the response on the way back.

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

app.Use(async (context, next) =>
{
    Console.WriteLine("1. Before");
    await next();
    Console.WriteLine("1. After");
});

app.Use(async (context, next) =>
{
    Console.WriteLine("2. Before");
    await next();
    Console.WriteLine("2. After");
});

app.Run(async context =>
{
    Console.WriteLine("3. Terminal");
    await context.Response.WriteAsync("Hello from pipeline");
});

app.Run();

Output (request to /):

1. Before
2. Before
3. Terminal
2. After
1. After

Built-in Middleware

ASP.NET Core includes many built-in middleware components.

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

// Exception handling (should be early in pipeline)
app.UseExceptionHandler("/error");

// HTTPS redirection
app.UseHttpsRedirection();

// Static files
app.UseStaticFiles();

// Routing
app.UseRouting();

// Authentication and Authorization
app.UseAuthentication();
app.UseAuthorization();

// CORS
app.UseCors("AllowSpecificOrigin");

// Response caching
app.UseResponseCaching();

// Rate limiting (.NET 7+)
app.UseRateLimiter();

app.MapControllers();
app.Run();

Writing Custom Middleware

There are two approaches: convention-based and Factory-based.

Convention-based Middleware

public class RequestLoggingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<RequestLoggingMiddleware> _logger;

    public RequestLoggingMiddleware(RequestDelegate next, ILogger<RequestLoggingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        var start = DateTime.UtcNow;

        _logger.LogInformation("Request: {Method} {Path}",
            context.Request.Method, context.Request.Path);

        await _next(context);

        var duration = (DateTime.UtcNow - start).TotalMilliseconds;
        _logger.LogInformation("Response: {StatusCode} in {Duration}ms",
            context.Response.StatusCode, duration);
    }
}

// Register in Program.cs
app.UseMiddleware<RequestLoggingMiddleware>();

Factory-based Middleware

Use IMiddleware for middleware with DI-scoped dependencies.

public class TenantResolutionMiddleware : IMiddleware
{
    private readonly ITenantService _tenantService;

    public TenantResolutionMiddleware(ITenantService tenantService)
    {
        _tenantService = tenantService;
    }

    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        var tenantId = context.Request.Headers["X-Tenant-ID"].FirstOrDefault();
        if (!string.IsNullOrEmpty(tenantId))
        {
            _tenantService.SetCurrentTenant(tenantId);
        }

        await next(context);
    }
}

// Register
builder.Services.AddScoped<TenantResolutionMiddleware>();
app.UseMiddleware<TenantResolutionMiddleware>();

Short-Circuiting the Pipeline

A middleware can short-circuit the pipeline by not calling next.

public class MaintenanceModeMiddleware
{
    private readonly RequestDelegate _next;
    private readonly bool _isUnderMaintenance;

    public MaintenanceModeMiddleware(RequestDelegate next, IConfiguration config)
    {
        _next = next;
        _isUnderMaintenance = config.GetValue<bool>("MaintenanceMode");
    }

    public async Task InvokeAsync(HttpContext context)
    {
        if (_isUnderMaintenance && !context.Request.Path.StartsWithSegments("/health"))
        {
            context.Response.StatusCode = 503;
            context.Response.ContentType = "application/json";

            var response = JsonSerializer.Serialize(new
            {
                error = "Service temporarily unavailable",
                retryAfter = 60
            });

            await context.Response.WriteAsync(response);
            return; // Short-circuit
        }

        await _next(context);
    }
}

Branching the Pipeline

Use Map and MapWhen to create pipeline branches.

app.Map("/api", apiApp =>
{
    // Middleware only applies to /api paths
    apiApp.UseAuthentication();
    apiApp.UseAuthorization();
    apiApp.Run(async context =>
    {
        await context.Response.WriteAsync("API endpoint");
    });
});

app.MapWhen(context => context.Request.Query.ContainsKey("admin"), adminApp =>
{
    adminApp.Use(async (context, next) =>
    {
        // Custom admin authentication
        await next();
    });
    adminApp.Run(async context =>
    {
        await context.Response.WriteAsync("Admin area");
    });
});

Custom Middleware with Options

Use the options pattern for configurable middleware.

public class RateLimitingOptions
{
    public int MaxRequestsPerMinute { get; set; } = 100;
    public string ClientIdHeader { get; set; } = "X-Client-ID";
}

public static class RateLimitingMiddlewareExtensions
{
    public static IApplicationBuilder UseRateLimiting(
        this IApplicationBuilder builder,
        Action<RateLimitingOptions> configureOptions)
    {
        var options = new RateLimitingOptions();
        configureOptions(options);
        return builder.UseMiddleware<RateLimitingMiddleware>(Options.Create(options));
    }
}

// Usage
app.UseRateLimiting(options =>
{
    options.MaxRequestsPerMinute = 50;
    options.ClientIdHeader = "X-API-Key";
});

Common Mistakes

  1. Wrong middleware order: Exception Handling must be first. Authentication before Authorization. CORS before routing.

  2. Not calling next: Forgetting await next() causes the pipeline to terminate silently. Only skip next intentionally for short-circuiting.

  3. Modifying request/response after next: The response body cannot be modified after the next middleware has written to it. Buffer or intercept before next.

  4. Disposing scoped services in Singleton middleware: Convention-based middleware is singleton. Use factory-based middleware (IMiddleware) for scoped dependencies.

  5. Blocking async code: Always use async/await in middleware. Calling .Result or .Wait() on tasks causes deadlocks.

Practice Questions

  1. Write middleware that blocks requests from specific IP addresses defined in configuration.

  2. Create middleware that adds security headers (X-Content-Type-Options, X-Frame-Options) to every response.

  3. Implement request validation middleware that ensures every POST request has a valid JSON body.

  4. Challenge: Build a correlation ID middleware that generates or forwards a correlation ID through HTTP headers and makes it available via IHttpContextAccessor.

FAQ

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

Use can call the next middleware via the next delegate. Run is a terminal middleware that does not call next.

Can middleware be added conditionally?

Yes, use the environment check (app.Environment.IsDevelopment()) or feature flags to conditionally add middleware.

How do I pass data between middleware components?

Use HttpContext.Items, which is a dictionary scoped to the current request. It is reset on each request.

What is the performance impact of middleware?

Minimal. Each middleware adds a small overhead. Optimize by short-circuiting early when possible and avoiding synchronous blocking.

Can I write middleware in a separate library?

Yes, middleware can be packaged as NuGet packages. Create extension methods for IApplicationBuilder to register them cleanly.

Mini Project: Request Timer Middleware

Build middleware that measures and logs request duration with configurable thresholds.

using System.Diagnostics;

public class RequestTimerMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<RequestTimerMiddleware> _logger;
    private readonly long _slowThresholdMs;

    public RequestTimerMiddleware(RequestDelegate next,
        ILogger<RequestTimerMiddleware> logger,
        long slowThresholdMs = 1000)
    {
        _next = next;
        _logger = logger;
        _slowThresholdMs = slowThresholdMs;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        var sw = Stopwatch.StartNew();

        await _next(context);

        sw.Stop();
        var elapsed = sw.ElapsedMilliseconds;

        if (elapsed > _slowThresholdMs)
        {
            _logger.LogWarning("SLOW REQUEST: {Method} {Path} took {Duration}ms (threshold: {Threshold}ms)",
                context.Request.Method, context.Request.Path,
                elapsed, _slowThresholdMs);
        }
        else
        {
            _logger.LogInformation("Request: {Method} {Path} completed in {Duration}ms",
                context.Request.Method, context.Request.Path, elapsed);
        }

        // Add timing header
        context.Response.Headers["X-Request-Duration-Ms"] = elapsed.ToString();
    }
}

// Extension method
public static class RequestTimerExtensions
{
    public static IApplicationBuilder UseRequestTimer(
        this IApplicationBuilder builder, long slowThresholdMs = 1000)
    {
        return builder.UseMiddleware<RequestTimerMiddleware>(slowThresholdMs);
    }
}

// Program.cs
var app = WebApplication.Create(args);
app.UseRequestTimer(slowThresholdMs: 500);
app.MapGet("/", () => {
    Thread.Sleep(200); // Simulate work
    return "Hello!";
});
app.MapGet("/slow", async () => {
    await Task.Delay(2000); // Simulate slow operation
    return "Slow response";
});
app.Run();

Middleware is one of the most powerful concepts in ASP.NET Core. By mastering custom middleware, you can build clean, modular .NET web applications that handle cross-cutting concerns elegantly without cluttering your business logic.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro