Home / .NET / ASP.NET Core Middleware Pipeline: Order, Custom Middleware, and Best Practices
.NET

ASP.NET Core Middleware Pipeline: Order, Custom Middleware, and Best Practices

The middleware pipeline is the heart of ASP.NET Core request processing.

What you will learn

Practical execution with concise explanations, real implementation patterns, and production-ready recommendations.

ASP.NET Core Middleware Pipeline: Order, Custom Middleware, and Best Practices

.FirstOrDefault()?.Split(" ").Last();

if (token != null) { await AttachUserToContext(context, userService, token); }

await _next(context); }

private async Task AttachUserToContext( HttpContext context, IUserService userService, string token) { try { var tokenHandler = new JwtSecurityTokenHandler(); var key = Encoding.ASCII.GetBytes(_configuration["Jwt:Secret"]!);

tokenHandler.ValidateToken(token, new TokenValidationParameters { ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey(key), ValidateIssuer = true, ValidIssuer = _configuration["Jwt:Issuer"], ValidateAudience = true, ValidAudience = _configuration["Jwt:Audience"], ClockSkew = TimeSpan.Zero }, out SecurityToken validatedToken);

var jwtToken = (JwtSecurityToken)validatedToken; var userId = int.Parse(jwtToken.Claims.First(x => x.Type == "id").Value);

// Attach user to context context.Items["User"] = await userService.GetByIdAsync(userId); } catch { // Token validation failed - user not attached to context } }``` }

// Attribute for protected endpoints [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] public class AuthorizeAttribute : Attribute, IAuthorizationFilter {

public void OnAuthorization(AuthorizationFilterContext context)
{
    var user = context.HttpContext.Items["User"];
    
    if (user == null)
    {
        context.Result = new JsonResult(new { message = "Unauthorized" })
        {
            StatusCode = StatusCodes.Status401Unauthorized
        };
    }
}```
}

Error Handling

Error Handling

Global Exception Handler

Centralized error handling:

public class GlobalExceptionMiddleware
{
```csharp
private readonly RequestDelegate _next;
private readonly ILogger<GlobalExceptionMiddleware> _logger;
private readonly IHostEnvironment _env;

public GlobalExceptionMiddleware(
    RequestDelegate next,
    ILogger<GlobalExceptionMiddleware> logger,
    IHostEnvironment env)
{
    _next = next;
    _logger = logger;
    _env = env;
}

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

private async Task HandleExceptionAsync(HttpContext context, Exception exception)
{
    context.Response.ContentType = "application/json";
    
    var response = exception switch
    {
        ValidationException => (
            StatusCodes.Status400BadRequest,
            new { error = exception.Message, errors = ((ValidationException)exception).Errors }
        ),
        NotFoundException => (
            StatusCodes.Status404NotFound,
            new { error = exception.Message }
        ),
        UnauthorizedException => (
            StatusCodes.Status401Unauthorized,
            new { error = "Unauthorized" }
        ),


        _ => (
            StatusCodes.Status500InternalServerError,
            new { error = _env.IsDevelopment() ? exception.ToString() : "Internal server error" }
        )
    };
    
    context.Response.StatusCode = response.Item1;
    await context.Response.WriteAsJsonAsync(response.Item2);
}```
}

Status Code Pages

Custom error responses:

app.UseStatusCodePages(async context =>
{
```text
var response = context.HttpContext.Response;

if (response.StatusCode == 404)
{
    response.ContentType = "application/json";
    await response.WriteAsJsonAsync(new
    {
        statusCode = 404,
        message = "Resource not found"
    });
}
else if (response.StatusCode >= 400 && response.StatusCode < 500)
{
    response.ContentType = "application/json";
    await response.WriteAsJsonAsync(new
    {
        statusCode = response.StatusCode,
        message = "Client error"
    });
}```
});

Short-Circuiting

Terminal Middleware

Stopping pipeline execution:

// Maintenance mode check
app.Use(async (context, next) =>
{
```text
if (IsMaintenanceMode())
{
    context.Response.StatusCode = 503;
    await context.Response.WriteAsync("Service under maintenance");
    return; // Don't call next
}

await next(context);```
});

// Health check endpoint
app.Map("/health", appBuilder =>
{
```javascript
appBuilder.Run(async context =>
{
    await context.Response.WriteAsync("Healthy");
    // Pipeline ends here for /health requests
});```
});

Performance Considerations

Performance Considerations

Response Caching

Caching middleware:

builder.Services.AddResponseCaching();

var app = builder.Build();

app.UseResponseCaching();

// Use with attributes
[ResponseCache(Duration = 60, Location = ResponseCacheLocation.Any)]
public IActionResult GetProducts()
{
```text
return Ok(_products);```
}

Response Compression

Enable compression:

builder.Services.AddResponseCompression(options =>
{
```text
options.EnableForHttps = true;
options.Providers.Add<BrotliCompressionProvider>();
options.Providers.Add<GzipCompressionProvider>();```
});

builder.Services.Configure<BrotliCompressionProviderOptions>(options =>
{
```text
options.Level = CompressionLevel.Fastest;```
});

var app = builder.Build();

// Near end of pipeline
app.UseResponseCompression();

Best Practices

  1. Order Matters: Exception handling first, endpoints last
  2. Short-Circuit Early: Static files and health checks before expensive middleware
  3. Use Extension Methods: Encapsulate middleware registration
  4. Minimize Buffering: Read request/response bodies only when necessary
  5. Leverage Scoped Services: Use IMiddleware for dependency injection
  6. Test Middleware: Unit test with DefaultHttpContext
  7. Monitor Performance: Track middleware execution time

Troubleshooting

Troubleshooting

Middleware Not Executing:

// ❌ Order matters - endpoints terminate pipeline
app.MapControllers();
app.UseMiddleware<MyMiddleware>(); // Won't execute!

// ✅ Correct order
app.UseMiddleware<MyMiddleware>();
app.MapControllers();

Request Body Already Read:

// ✅ Enable buffering for multiple reads
context.Request.EnableBuffering();

Architecture Decision and Tradeoffs

When designing application development solutions with .NET, consider these key architectural trade-offs:

Approach Best For Tradeoff
Managed / platform service Rapid delivery, reduced ops burden Less customisation, potential vendor lock-in
Custom / self-hosted Full control, advanced tuning Higher operational overhead and cost

Recommendation: Start with the managed approach for most workloads and move to custom only when specific requirements demand it.

Validation and Versioning

  • Last validated: April 2026
  • Validate examples against your tenant, region, and SKU constraints before production rollout.
  • Keep module, CLI, and SDK versions pinned in automation pipelines and review quarterly.

Security and Governance Considerations

  • Apply least-privilege access using RBAC roles and just-in-time elevation for admin tasks.
  • Store secrets in managed secret stores and avoid embedding credentials in scripts or source files.
  • Enable audit logging, data protection policies, and periodic access reviews for regulated workloads.

Cost and Performance Notes

  • Define budgets and alerts, then monitor usage and cost trends continuously after go-live.
  • Baseline performance with synthetic and real-user checks before and after major changes.
  • Scale resources with measured thresholds and revisit sizing after usage pattern changes.

Official Microsoft References

  • https://learn.microsoft.com/dotnet/
  • https://learn.microsoft.com/aspnet/core/
  • https://learn.microsoft.com/azure/developer/dotnet/

Public Examples from Official Sources

  • These examples are sourced from official public Microsoft documentation and sample repositories.
  • Documentation examples: https://learn.microsoft.com/dotnet/
  • Sample repositories: https://github.com/dotnet/samples
  • Prefer adapting these examples to your tenant, subscriptions, and governance requirements before production use.

Key Takeaways

  • Middleware order is critical for correct request processing
  • Custom middleware enables cross-cutting concerns like logging and authentication
  • Request/response modification requires careful stream handling
  • Conditional middleware reduces overhead for specific request paths
  • Short-circuiting improves performance for simple endpoints

Next Steps

  • Implement health checks with custom middleware
  • Add correlation IDs for distributed tracing
  • Build rate limiting middleware with Redis
  • Create API versioning middleware

Additional Resources


The pipeline is the application.

Discussion