put, directly impacting cloud compute costs and scalability limits.
Core Solution
Implementing a robust request pipeline requires understanding the delegate chain, mastering middleware creation patterns, and enforcing strict ordering discipline.
1. Pipeline Architecture Fundamentals
The pipeline is a chain of RequestDelegate instances. Each middleware receives the next delegate and decides whether to invoke it.
public delegate Task RequestDelegate(HttpContext context);
Key control methods:
Use: Adds middleware that can run logic before and after the next delegate.
Run: Adds terminal middleware; execution stops here.
Map: Branches the pipeline based on a path match.
MapWhen: Branches the pipeline based on a predicate.
2. Middleware Implementation Patterns
Convention-Based Middleware (Recommended for Performance)
The framework instantiates middleware classes using a factory pattern. This allows constructor injection for singleton services and method injection for scoped services.
public class PerformanceMetricsMiddleware
{
private readonly RequestDelegate _next;
private readonly IMetricsCollector _metrics; // Singleton via constructor
public PerformanceMetricsMiddleware(RequestDelegate next, IMetricsCollector metrics)
{
_next = next;
_metrics = metrics;
}
// Scoped service injected via Invoke/InvokeAsync method
public async Task InvokeAsync(HttpContext context, IScopedService scopedService)
{
var sw = Stopwatch.StartNew();
try
{
await _next(context);
}
finally
{
sw.Stop();
_metrics.RecordDuration(context.Request.Path, sw.Elapsed);
}
}
}
Factory-Based Middleware (IMiddleware)
Use IMiddleware when middleware requires scoped dependencies in the constructor. The framework resolves the middleware from DI per request.
public class AuditMiddleware : IMiddleware
{
private readonly IAuditRepository _auditRepo; // Scoped resolved per request
public AuditMiddleware(IAuditRepository auditRepo)
{
_auditRepo = auditRepo;
}
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
// Logic using scoped _auditRepo
await next(context);
}
}
3. Strategic Ordering and Short-Circuiting
The optimal pipeline order minimizes work for high-volume, low-complexity requests.
var builder = WebApplication.CreateBuilder(args);
// 1. Exception Handling (Must be outermost)
// 2. Security Headers / HSTS
// 3. Routing (Endpoint Routing)
// 4. Static Files (Short-circuit via UseStaticFiles)
// 5. Authentication / Authorization (Only runs on matched endpoints)
// 6. Custom Middleware (e.g., Rate Limiting, Logging)
// 7. Endpoint Execution (MapControllers, MapRazorPages)
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles(); // Short-circuits static content automatically
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
// Branching for high-throughput health checks
app.MapWhen(ctx => ctx.Request.Path.StartsWithSegments("/health"), healthApp =>
{
healthApp.Run(async context =>
{
context.Response.StatusCode = 200;
await context.Response.WriteAsync("OK");
});
});
app.MapControllers();
app.Run();
4. Branching with Map and MapWhen
Branching creates isolated pipeline segments. Middleware added to a branch does not affect the main pipeline.
// Isolate API versioning pipeline
app.Map("/api/v1", v1App =>
{
v1App.UseMiddleware<V1SpecificMiddleware>();
v1App.MapControllers();
});
app.Map("/api/v2", v2App =>
{
v2App.UseMiddleware<V2SpecificMiddleware>();
v2App.MapControllers();
});
Pitfall Guide
1. Scoped Services in Middleware Constructors
Mistake: Injecting a scoped service into the middleware constructor.
Impact: The middleware instance is cached and reused. The scoped service is resolved once at startup and effectively becomes a singleton. This causes data leakage between requests and thread-safety violations.
Fix: Inject scoped services in the Invoke or InvokeAsync method parameters.
2. Forgetting await next(context)
Mistake: Omitting the call to the next delegate or failing to await it.
Impact: The pipeline breaks. The request may hang, return an empty response, or execute subsequent middleware on the wrong thread context.
Fix: Always await _next(context); unless intentionally short-circuiting.
Mistake: Attempting to set headers or status code after the response body has started flushing.
Impact: System.InvalidOperationException: Headers are already sent.
Fix: Set headers/status codes before invoking _next or before writing to the body. Use context.Response.HasStarted checks if necessary.
4. Synchronous Blocking in Async Pipeline
Mistake: Using .Result or .Wait() on async operations within middleware.
Impact: Thread pool starvation. Under load, threads block waiting for I/O, causing throughput to collapse.
Fix: Use async/await throughout the pipeline. Ensure all downstream calls are truly asynchronous.
5. Misplaced Exception Handling
Mistake: Placing UseExceptionHandler after routing or authentication.
Impact: Exceptions thrown in earlier middleware may not be caught, or the exception handler itself may trigger auth failures.
Fix: Place exception handling middleware as early as possible in the pipeline, typically immediately after environment checks.
6. Over-Reliance on UseWhen
Mistake: Using UseWhen for complex routing logic.
Impact: Predicate evaluation adds overhead. Deeply nested conditions make the pipeline graph difficult to debug and maintain.
Fix: Prefer Map for path-based branching and Endpoint Routing for complex matching. Reserve UseWhen for simple header or query-based splits.
7. Ignoring Endpoint Routing Implications
Mistake: Using legacy UseMvc patterns or ignoring UseRouting.
Impact: Loss of endpoint metadata, inability to use [Authorize] on controllers, and inefficient request matching.
Fix: Always use UseRouting followed by UseAuthorization and UseEndpoints (or MapControllers). This enables the modern endpoint-based middleware model.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| High-Throughput API | Endpoint Routing + Minimal API | Lowest overhead, direct delegate execution, reduced allocations. | Reduces compute cost by ~30% vs MVC. |
| Complex Business Logic | MVC with Convention-Based Middleware | Separation of concerns, robust model binding, standard DI support. | Moderate compute cost; higher dev velocity. |
| Static Content Serving | UseStaticFiles + CDN Offload | Short-circuits pipeline; file system caching; avoids app server load. | Near-zero app server cost for static assets. |
| Multi-Tenant Routing | MapWhen based on Host/Path | Isolates tenant pipelines; prevents cross-tenant middleware leakage. | Slight CPU overhead for predicate; improves security. |
| Legacy Migration | Hybrid Routing (MapWhen to legacy) | Gradual migration path; isolates legacy pipeline behavior. | Temporary overhead; reduces migration risk. |
Configuration Template
Copy this template for a production-ready pipeline setup with best practices.
var builder = WebApplication.CreateBuilder(args);
// Services registration
builder.Services.AddRouting();
builder.Services.AddControllers();
builder.Services.AddAuthentication();
builder.Services.AddAuthorization();
builder.Services.AddHealthChecks();
// Register middleware services
var app = builder.Build();
// 1. Diagnostics & Error Handling
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/error");
app.UseHsts();
}
// 2. Security & Protocol
app.UseHttpsRedirection();
app.UseHsts();
// 3. Static & Health (Short-Circuit Layer)
app.UseStaticFiles();
app.MapHealthChecks("/health");
app.MapHealthChecks("/ready");
// 4. Routing
app.UseRouting();
// 5. Security Middleware
app.UseAuthentication();
app.UseAuthorization();
// 6. Custom Middleware (Ordered by cost/dependency)
app.UseMiddleware<RateLimitingMiddleware>();
app.UseMiddleware<RequestCorrelationMiddleware>();
// 7. Endpoint Execution
app.MapControllers();
app.MapRazorPages();
// 8. Fallback
app.Run(async context =>
{
context.Response.StatusCode = 404;
await context.Response.WriteAsync("Not Found");
});
app.Run();
Quick Start Guide
-
Create Middleware Class:
Define a class with a constructor accepting RequestDelegate and an InvokeAsync method accepting HttpContext.
public class MyMiddleware
{
private readonly RequestDelegate _next;
public MyMiddleware(RequestDelegate next) => _next = next;
public async Task InvokeAsync(HttpContext context)
{
// Pre-logic
await _next(context);
// Post-logic
}
}
-
Register in DI:
Add the middleware to the service collection in Program.cs.
builder.Services.AddTransient<MyMiddleware>();
-
Add to Pipeline:
Insert the middleware in the correct order within Program.cs.
app.UseMiddleware<MyMiddleware>();
-
Validate Execution:
Run the application and use a tool like curl or Postman to trigger requests. Verify middleware execution via logging or breakpoints. Check that scoped services resolve correctly per request.
-
Benchmark:
Use bombardier or k6 to test throughput. Compare metrics before and after adding middleware to ensure performance targets are met. Adjust ordering if latency increases disproportionately.