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
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
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
- Order Matters: Exception handling first, endpoints last
- Short-Circuit Early: Static files and health checks before expensive middleware
- Use Extension Methods: Encapsulate middleware registration
- Minimize Buffering: Read request/response bodies only when necessary
- Leverage Scoped Services: Use
IMiddlewarefor dependency injection - Test Middleware: Unit test with
DefaultHttpContext - Monitor Performance: Track middleware execution time
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