Skip to main content

Middleware Pipeline

Overview

Middleware is software assembled into an application pipeline to handle requests and responses. Each component:

  • Chooses whether to pass the request to the next component
  • Can perform work before and after the next component

How Middleware Works

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

// Middleware pipeline
app.UseHttpsRedirection(); // 1. Redirect HTTP → HTTPS
app.UseRouting(); // 2. Match endpoint
app.UseCors(); // 3. CORS policy
app.UseAuthentication(); // 4. Authenticate user
app.UseAuthorization(); // 5. Authorize user
app.UseRateLimiter(); // 6. Rate limiting
app.MapControllers(); // 7. Execute endpoint

app.Run();

Order Matters!

Correct Order:

Request → HTTPS → Routing → CORS → Auth(N) → Auth(Z) → Rate Limit → Endpoint

⚠️ Wrong Order = Security Issues:

// WRONG! Authorization before Authentication
app.UseAuthorization(); // ❌ Can't authorize without knowing who the user is
app.UseAuthentication(); // ❌ Too late!

Built-in Middleware

MiddlewarePurposePosition
UseHttpsRedirectionRedirect HTTP to HTTPSEarly
UseStaticFilesServe static filesEarly
UseRoutingMatch endpointsBefore auth
UseCorsCORS policyAfter routing
UseAuthenticationIdentify userBefore authorization
UseAuthorizationCheck permissionsBefore endpoint
UseSessionSession stateAfter routing
UseResponseCachingCache responsesEarly
UseResponseCompressionCompress responsesEarly

Creating Custom Middleware

Method 1: Inline Lambda

app.Use(async (context, next) =>
{
// Before next middleware
var startTime = DateTime.UtcNow;

await next(context);

// After next middleware
var elapsed = DateTime.UtcNow - startTime;
context.Response.Headers.Add("X-Response-Time", elapsed.TotalMilliseconds.ToString());
});

Method 2: Custom Class

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)
{
_logger.LogInformation("Request: {Method} {Path}",
context.Request.Method,
context.Request.Path);

await _next(context);

_logger.LogInformation("Response: {StatusCode}",
context.Response.StatusCode);
}
}

// Extension method for cleaner registration
public static class MiddlewareExtensions
{
public static IApplicationBuilder UseRequestLogging(
this IApplicationBuilder builder)
{
return builder.UseMiddleware<RequestLoggingMiddleware>();
}
}

// Usage
app.UseRequestLogging();

Method 3: Factory-based

public class RequestLoggingMiddleware : IMiddleware
{
private readonly ILogger<RequestLoggingMiddleware> _logger;

public RequestLoggingMiddleware(ILogger<RequestLoggingMiddleware> logger)
{
_logger = logger;
}

public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
_logger.LogInformation("Handling request: {Path}", context.Request.Path);
await next(context);
_logger.LogInformation("Finished handling request.");
}
}

// Register as service
builder.Services.AddTransient<RequestLoggingMiddleware>();

// Use in pipeline
app.UseMiddleware<RequestLoggingMiddleware>();

Short-Circuiting

Middleware can short-circuit (stop) the pipeline:

app.Use(async (context, next) =>
{
if (!context.Request.Headers.ContainsKey("X-API-Key"))
{
context.Response.StatusCode = 401;
await context.Response.WriteAsync("API Key required");
return; // Short-circuit! Don't call next()
}

await next(context);
});

Terminal Middleware

Terminal middleware doesn't call next():

app.Run(async context =>
{
await context.Response.WriteAsync("Final handler");
// No next() call - this ends the pipeline
});

Conditional Middleware

Map (Branch based on path)

app.Map("/admin", adminApp =>
{
adminApp.UseAuthentication();
adminApp.UseAuthorization();
adminApp.Run(async context =>
{
await context.Response.WriteAsync("Admin area");
});
});

MapWhen (Branch based on predicate)

app.MapWhen(
context => context.Request.Query.ContainsKey("debug"),
app => app.Run(async context =>
{
await context.Response.WriteAsync("Debug mode");
})
);

UseWhen (Rejoin main pipeline)

app.UseWhen(
context => context.Request.Path.StartsWithSegments("/api"),
app => app.UseRateLimiter()
);

Real-World Example: API Key Middleware

public class ApiKeyMiddleware
{
private readonly RequestDelegate _next;
private readonly IConfiguration _configuration;
private const string API_KEY_HEADER = "X-API-Key";

public ApiKeyMiddleware(RequestDelegate next, IConfiguration configuration)
{
_next = next;
_configuration = configuration;
}

public async Task InvokeAsync(HttpContext context)
{
// Skip auth check for health endpoint
if (context.Request.Path.StartsWithSegments("/health"))
{
await _next(context);
return;
}

if (!context.Request.Headers.TryGetValue(API_KEY_HEADER, out var providedKey))
{
context.Response.StatusCode = 401;
await context.Response.WriteAsJsonAsync(new { error = "API Key missing" });
return;
}

var validKey = _configuration["ApiKey"];
if (providedKey != validKey)
{
context.Response.StatusCode = 401;
await context.Response.WriteAsJsonAsync(new { error = "Invalid API Key" });
return;
}

await _next(context);
}
}

Exception Handling Middleware

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

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

public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception ex)
{
_logger.LogError(ex, "Unhandled exception occurred");
await HandleExceptionAsync(context, ex);
}
}

private static Task HandleExceptionAsync(HttpContext context, Exception exception)
{
context.Response.ContentType = "application/json";
context.Response.StatusCode = exception switch
{
ArgumentException => 400,
UnauthorizedAccessException => 401,
KeyNotFoundException => 404,
_ => 500
};

var response = new
{
error = exception.Message,
statusCode = context.Response.StatusCode
};

return context.Response.WriteAsJsonAsync(response);
}
}

Standard Pipeline Order

var app = builder.Build();

// 1. Exception handling (must be first!)
app.UseExceptionHandler("/error");

// 2. HTTPS redirection
app.UseHttpsRedirection();

// 3. Static files (short-circuits for static content)
app.UseStaticFiles();

// 4. Routing (match endpoint)
app.UseRouting();

// 5. CORS (after routing)
app.UseCors();

// 6. Authentication (who are you?)
app.UseAuthentication();

// 7. Authorization (what can you do?)
app.UseAuthorization();

// 8. Rate limiting
app.UseRateLimiter();

// 9. Response caching
app.UseResponseCaching();

// 10. Session (if needed)
app.UseSession();

// 11. Endpoints (execute matched endpoint)
app.MapControllers();

app.Run();

Interview Questions

Q: Why does middleware order matter? A: Each middleware can process the request before passing to the next. If authentication runs after authorization, you're authorizing an unauthenticated user. If exception handling is last, unhandled exceptions won't be caught.

Q: What's the difference between Use, Run, and Map? A:

  • Use: Middleware that can call next()
  • Run: Terminal middleware (doesn't call next)
  • Map: Branches pipeline based on path

Q: How do you short-circuit the pipeline? A: Don't call await next(context). Instead, write directly to the response and return.

Resources