The Ultimate ASP.NET Core Middleware Cheat Sheet

Introduction: Understanding Middleware in ASP.NET Core

Middleware in ASP.NET Core refers to software components that form a pipeline to handle HTTP requests and responses. Each middleware component in the request pipeline is responsible for invoking the next component in the pipeline or short-circuiting the pipeline. When a middleware short-circuits, it’s called a terminal middleware because it prevents further middleware from processing the request. This architecture allows developers to build flexible, modular web applications by composing different middleware components that each perform specific tasks in processing HTTP requests.

Core Middleware Concepts

Middleware Fundamentals

  • Definition: Software components that handle HTTP requests and responses
  • Pipeline Structure: Ordered sequence of middleware components
  • Execution Flow: Bidirectional – processes requests on the way in and responses on the way out
  • Capabilities: Can inspect, modify, or terminate request processing
  • Component Types: Built-in middleware, custom middleware, and inline middleware

Key Characteristics

  • Executes in the sequence it’s added to the pipeline
  • Can perform operations before and after the next component
  • Has access to both the incoming request and outgoing response
  • Can short-circuit the pipeline by not calling the next middleware
  • Can be registered with different extension methods (Use, Run, Map)

Middleware Pipeline Execution Flow

                REQUEST                            RESPONSE
                ↓                                      ↑
Client → [Middleware 1] → [Middleware 2] → ... → [Middleware n] → Application Logic
                ↓ ↑           ↓ ↑                     ↓ ↑

Request Processing Stages

  1. Incoming Request: Middleware executes in registration order (1 → 2 → … → n)
  2. Response Generation: After the endpoint/logic generates a response
  3. Outgoing Response: Middleware executes in reverse order (n → … → 2 → 1)

Built-in ASP.NET Core Middleware Components

Essential Middleware (in Recommended Order)

MiddlewareRegistration MethodPurposeTypical Position
Exception HandlerUseExceptionHandler()Catches exceptions, logs them, and serves custom error pagesFirst
HTTPS RedirectionUseHttpsRedirection()Redirects HTTP requests to HTTPSEarly
Static FilesUseStaticFiles()Serves static files from wwwrootEarly
RoutingUseRouting()Matches request to an endpointBefore endpoint-specific middleware
CORSUseCors()Enables Cross-Origin Resource SharingBefore authentication
AuthenticationUseAuthentication()Identifies the userBefore authorization
AuthorizationUseAuthorization()Determines if user has accessAfter authentication
EndpointUseEndpoints()Executes the matched endpointLast

Additional Middleware Components

MiddlewareRegistration MethodPurpose
Response CachingUseResponseCaching()Caches responses to improve performance
Response CompressionUseResponseCompression()Compresses response content
SessionUseSession()Enables session state
Health ChecksUseHealthChecks()Provides health monitoring endpoints
Request LocalizationUseRequestLocalization()Sets culture information for requests
RewriteUseRewriter()Modifies request URLs based on rules
WebSocketsUseWebSockets()Enables WebSocket support
Host FilteringUseHostFiltering()Restricts hosts that can access the app
Rate LimitingUseRateLimiter()Controls the rate at which clients can access resources

Registering Middleware

Basic Registration Methods

// Program.cs in ASP.NET Core 6+
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// 1. Using built-in middleware
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();

// 2. Using inline middleware with Use (non-terminal)
app.Use(async (context, next) => {
    // Do something before the next middleware
    Console.WriteLine("Before next middleware");
    
    await next();
    
    // Do something after the next middleware
    Console.WriteLine("After next middleware");
});

// 3. Using inline middleware with Run (terminal)
app.Run(async context => {
    await context.Response.WriteAsync("Hello World!");
});

app.Run();

Map and MapWhen For Branching

// Branch based on path
app.Map("/admin", adminApp => {
    adminApp.Use(async (context, next) => {
        // Admin-specific middleware
        await next();
    });
});

// Branch based on condition
app.MapWhen(
    context => context.Request.Query.ContainsKey("api"),
    apiApp => {
        apiApp.Run(async context => {
            await context.Response.WriteAsync("API Response");
        });
    }
);

UseWhen For Conditional Middleware

// Conditionally execute middleware but stay in same pipeline
app.UseWhen(
    context => context.Request.Path.StartsWithSegments("/api"),
    appBuilder => {
        appBuilder.UseMiddleware<ApiLoggingMiddleware>();
    }
);

Creating Custom Middleware

Convention-Based Middleware

public class RequestLoggingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<RequestLoggingMiddleware> _logger;
    
    // Constructor
    public RequestLoggingMiddleware(RequestDelegate next, ILogger<RequestLoggingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }
    
    // Method called by runtime
    public async Task InvokeAsync(HttpContext context)
    {
        // Pre-processing logic
        _logger.LogInformation($"Request: {context.Request.Method} {context.Request.Path}");
        
        // Call the next middleware in the pipeline
        await _next(context);
        
        // Post-processing logic
        _logger.LogInformation($"Response: {context.Response.StatusCode}");
    }
}

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

// Usage in Program.cs
app.UseRequestLogging();

Factory-Based Middleware

public class FactoryMiddleware : IMiddleware
{
    private readonly IServiceProvider _serviceProvider;
    
    public FactoryMiddleware(IServiceProvider serviceProvider) 
    {
        _serviceProvider = serviceProvider;
    }
    
    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        // Access scoped services from the container
        var scopedService = context.RequestServices.GetRequiredService<IScopedService>();
        
        // Process the request
        await next(context);
    }
}

// Registration in Program.cs
builder.Services.AddTransient<FactoryMiddleware>();
app.UseMiddleware<FactoryMiddleware>();

Inline Middleware

app.Use(async (context, next) => {
    var timer = Stopwatch.StartNew();
    
    // Call next component
    await next();
    
    timer.Stop();
    Console.WriteLine($"Request took {timer.ElapsedMilliseconds}ms");
});

Common Middleware Patterns and Examples

Authentication and Authorization

// Add services
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options => {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = builder.Configuration["JWT:Issuer"],
            ValidAudience = builder.Configuration["JWT:Audience"],
            IssuerSigningKey = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes(builder.Configuration["JWT:Secret"]))
        };
    });

// Register middleware
app.UseAuthentication();
app.UseAuthorization();

Error Handling

if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

Custom Error Handler Middleware

public class ErrorHandlerMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<ErrorHandlerMiddleware> _logger;
    
    public ErrorHandlerMiddleware(RequestDelegate next, ILogger<ErrorHandlerMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }
    
    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "An unhandled exception occurred");
            
            context.Response.StatusCode = StatusCodes.Status500InternalServerError;
            context.Response.ContentType = "application/json";
            
            var response = new { 
                error = app.Environment.IsDevelopment() ? ex.Message : "An error occurred" 
            };
            
            await context.Response.WriteAsJsonAsync(response);
        }
    }
}

// Extension method
public static class ErrorHandlerMiddlewareExtensions
{
    public static IApplicationBuilder UseErrorHandler(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<ErrorHandlerMiddleware>();
    }
}

// Register in Program.cs
app.UseErrorHandler();

Request/Response Logging

app.Use(async (context, next) =>
{
    // Log the request
    var requestBodyStream = new MemoryStream();
    var originalRequestBody = context.Request.Body;
    
    try
    {
        await context.Request.Body.CopyToAsync(requestBodyStream);
        requestBodyStream.Seek(0, SeekOrigin.Begin);
        
        var requestBodyText = await new StreamReader(requestBodyStream).ReadToEndAsync();
        Console.WriteLine($"Request: {requestBodyText}");
        
        requestBodyStream.Seek(0, SeekOrigin.Begin);
        context.Request.Body = requestBodyStream;
        
        // Capture the response
        var originalResponseBody = context.Response.Body;
        using var responseBodyStream = new MemoryStream();
        context.Response.Body = responseBodyStream;
        
        await next();
        
        responseBodyStream.Seek(0, SeekOrigin.Begin);
        var responseBodyText = await new StreamReader(responseBodyStream).ReadToEndAsync();
        Console.WriteLine($"Response: {responseBodyText}");
        
        responseBodyStream.Seek(0, SeekOrigin.Begin);
        await responseBodyStream.CopyToAsync(originalResponseBody);
    }
    finally
    {
        context.Request.Body = originalRequestBody;
    }
});

API Key Validation

public class ApiKeyMiddleware
{
    private readonly RequestDelegate _next;
    private readonly IConfiguration _configuration;
    
    public ApiKeyMiddleware(RequestDelegate next, IConfiguration configuration)
    {
        _next = next;
        _configuration = configuration;
    }
    
    public async Task InvokeAsync(HttpContext context)
    {
        if (!context.Request.Headers.TryGetValue("X-API-Key", out var extractedApiKey))
        {
            context.Response.StatusCode = 401;
            await context.Response.WriteAsync("API Key was not provided");
            return;
        }
        
        var apiKey = _configuration["ApiKey"];
        
        if (!apiKey.Equals(extractedApiKey))
        {
            context.Response.StatusCode = 401;
            await context.Response.WriteAsync("Invalid API Key");
            return;
        }
        
        await _next(context);
    }
}

// Extension method
public static class ApiKeyMiddlewareExtensions
{
    public static IApplicationBuilder UseApiKey(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<ApiKeyMiddleware>();
    }
}

// Register in Program.cs
app.UseApiKey();

Response Caching

// Add services
builder.Services.AddResponseCaching();

// Middleware registration
app.UseResponseCaching();

// In controller
[HttpGet]
[ResponseCache(Duration = 60)]
public IActionResult Get()
{
    return Ok(DateTime.Now);
}

Culture Middleware

app.Use(async (context, next) =>
{
    var cultureQuery = context.Request.Query["culture"];
    if (!string.IsNullOrWhiteSpace(cultureQuery))
    {
        var culture = new CultureInfo(cultureQuery);
        CultureInfo.CurrentCulture = culture;
        CultureInfo.CurrentUICulture = culture;
    }
    
    await next();
});

Performance Considerations

Middleware Performance Best Practices

  1. Order Matters: Place frequently used middleware earlier in the pipeline
  2. Short-Circuit When Possible: Don’t process the entire pipeline if unnecessary
  3. Avoid Blocking Operations: Use async/await for I/O-bound operations
  4. Minimize Middleware: Only include middleware you need
  5. Cache Where Appropriate: Use response caching for static or rarely changing content
  6. Avoid Capturing HttpContext: Don’t store or capture HttpContext for later use
  7. Use Memory Efficiently: Be careful with large request/response buffering
  8. Profile Your Middleware: Test performance in realistic scenarios

Common Performance Issues and Solutions

IssueSolution
Middleware executes for all requestsUse Map/MapWhen to branch for specific paths
Too many middleware componentsConsolidate related functionality
Heavy processing middlewareMove to background processing
Memory leaksAvoid static references to HttpContext
Unnecessary middlewareRemove middleware not needed for your scenario

Debugging Middleware

Logging Pipeline Execution

app.Use(async (context, next) =>
{
    var logger = context.RequestServices.GetRequiredService<ILogger<Program>>();
    logger.LogInformation("Middleware pipeline started");
    
    await next();
    
    logger.LogInformation("Middleware pipeline completed");
});

Middleware Diagnostic Tools

  1. Application Insights: Monitor request processing times
  2. Logging: Add detailed logs in each middleware
  3. Metrics: Track performance counters for requests
  4. Visual Studio Diagnostics: Use profiling tools
  5. Correlation IDs: Add request correlation for tracing

Advanced Middleware Scenarios

Middleware Scopes and DI

public class ScopedMiddleware
{
    private readonly RequestDelegate _next;
    
    public ScopedMiddleware(RequestDelegate next)
    {
        _next = next;
    }
    
    public async Task InvokeAsync(HttpContext context, IScopedService scopedService)
    {
        // Use scoped service with proper lifetime
        await _next(context);
    }
}

Conditional Middleware Execution

public class ConditionalMiddleware
{
    private readonly RequestDelegate _next;
    private readonly Func<HttpContext, bool> _predicate;
    
    public ConditionalMiddleware(RequestDelegate next, Func<HttpContext, bool> predicate)
    {
        _next = next;
        _predicate = predicate;
    }
    
    public async Task InvokeAsync(HttpContext context)
    {
        if (_predicate(context))
        {
            // Execute middleware logic
        }
        
        await _next(context);
    }
}

// Registration helper
public static class ConditionalMiddlewareExtensions
{
    public static IApplicationBuilder UseWhen(
        this IApplicationBuilder app,
        Func<HttpContext, bool> predicate,
        Action<IApplicationBuilder> configureMiddleware)
    {
        ArgumentNullException.ThrowIfNull(app);
        ArgumentNullException.ThrowIfNull(predicate);
        ArgumentNullException.ThrowIfNull(configureMiddleware);
        
        var branchBuilder = app.New();
        configureMiddleware(branchBuilder);
        
        var branch = branchBuilder.Build();
        
        app.Use(async (context, next) =>
        {
            if (predicate(context))
            {
                await branch(context);
            }
            
            await next();
        });
        
        return app;
    }
}

Middleware vs. Filters

AspectMiddlewareFilters
ScopeEntire applicationSpecific controllers/actions
PipelineRequest processing pipelineMVC/API action pipeline
ExecutionFor all requestsFor specific routing endpoints
ContextHttpContextActionContext
Typical UseCross-cutting concernsModel validation, action results

Common Middleware Extensions

Health Checks

// Add services
builder.Services.AddHealthChecks()
    .AddCheck("Database", () => {
        // Check database connection
        return HealthCheckResult.Healthy();
    })
    .AddCheck<ExternalApiHealthCheck>("ExternalApi");

// Register middleware
app.MapHealthChecks("/health");

Rate Limiting

// Add services
builder.Services.AddRateLimiter(options =>
{
    options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
        RateLimitPartition.GetFixedWindowLimiter(
            partitionKey: httpContext.Connection.RemoteIpAddress?.ToString() ?? "localhost",
            factory: partition => new FixedWindowRateLimiterOptions
            {
                AutoReplenishment = true,
                PermitLimit = 10,
                QueueLimit = 0,
                Window = TimeSpan.FromMinutes(1)
            }));
});

// Register middleware
app.UseRateLimiter();

CORS Configuration

// Add services
builder.Services.AddCors(options =>
{
    options.AddPolicy("AllowSpecificOrigin", policy =>
    {
        policy.WithOrigins("https://example.com")
              .AllowAnyMethod()
              .AllowAnyHeader();
    });
});

// Register middleware
app.UseCors("AllowSpecificOrigin");

Resources for Further Learning

Official Documentation

Community Resources

Books and Tutorials

  • “ASP.NET Core in Action” by Andrew Lock
  • “Pro ASP.NET Core” by Adam Freeman
  • “Building Modern Web Applications with ASP.NET Core” – Microsoft Learn

This cheat sheet provides a comprehensive overview of middleware in ASP.NET Core. Remember that middleware order is critical for proper functioning of your application, as it determines how requests flow through your application pipeline.

Scroll to Top