();
```
### When to Use Which?
| Approach | Lifetime | DI Support | Best for |
| ---------------- | ----------- | ------------------------------------------------------- | ------------------------------------- |
| Inline `Use()` | N/A | Captured closures | Prototyping, one-liners |
| Convention class | Singleton | Constructor (singleton) + `InvokeAsync` params (scoped) | Most production middleware |
| `IMiddleware` | Per-request | Full constructor injection | Middleware that needs scoped services |
---
## 6. Middleware Ordering – Why It Matters
The **order you add middleware is the order they execute**. Getting it wrong causes subtle (and
not-so-subtle) bugs.
### Recommended Order
flowchart TD
A["1. UseExceptionHandler"] --> B["2. UseHsts"]
B --> C["3. UseHttpsRedirection"]
C --> D["4. UseStaticFiles"]
D --> E["5. UseRouting"]
E --> F["6. UseCors"]
F --> G["7. UseAuthentication"]
G --> H["8. UseAuthorization"]
H --> I["9. UseRateLimiter"]
I --> J["10. UseResponseCaching"]
J --> K["11. Custom middlewares"]
K --> L["12. MapControllers / MapEndpoints"]
---
### Why This Order?
- **Exception handler first** → catches errors from ALL downstream middleware
- **Static files before routing** → avoids unnecessary auth checks for CSS/JS/images
- **Authentication before Authorization** → you must know _who_ the user is before checking _what_
they can do
- **CORS between Routing and Auth** → preflight requests need CORS headers but shouldn't require
auth
### ⚠️ Common Mistakes
```csharp
// ❌ WRONG: Authorization before Authentication
app.UseAuthorization(); // User not identified yet!
app.UseAuthentication();
// ❌ WRONG: Exception handler too late
app.UseAuthentication();
app.UseExceptionHandler(); // Won't catch auth errors!
// ❌ WRONG: Static files after auth
app.UseAuthentication();
app.UseAuthorization();
app.UseStaticFiles(); // Now CSS requires login!
```
---
## 7. Short-Circuiting the Pipeline
A middleware **short-circuits** when it doesn't call `next()`. The request never reaches downstream
middleware or endpoints.
### When to Short-Circuit
- ❌ Request fails validation (bad API key, rate limit exceeded)
- ❌ Cached response available (no need to execute the endpoint)
- ❌ Health check endpoint (simple response, skip everything)
### Example: API Key Validation
```csharp
public async Task InvokeAsync(HttpContext context)
{
if (!context.Request.Headers.TryGetValue("X-Api-Key", out var apiKey)
|| apiKey != _expectedKey)
{
context.Response.StatusCode = 401;
await context.Response.WriteAsJsonAsync(new { error = "Invalid API key" });
return; // ⛔ Pipeline stops here — next() is never called
}
await _next(context); // ✅ Valid key — continue the pipeline
}
```
---
### ⚠️ Important: Don't Write After `next()`
```csharp
// ❌ DANGEROUS: Response may already be started
await _next(context);
context.Response.Headers["X-Custom"] = "value"; // May throw!
// ✅ SAFE: Use OnStarting to modify headers
context.Response.OnStarting(() =>
{
context.Response.Headers["X-Custom"] = "value";
return Task.CompletedTask;
});
await _next(context);
```
---
## 8. Branching the Pipeline
### `Map` – Branch by path prefix (separate pipeline)
The branch is a **completely separate pipeline** — it does not rejoin the main pipeline:
```csharp
app.Map("/api", apiApp =>
{
apiApp.UseAuthentication();
apiApp.UseAuthorization();
apiApp.Run(async context =>
await context.Response.WriteAsync("API branch"));
});
// Requests to /api/* never reach middleware registered after this Map()
```
### `MapWhen` – Branch by any condition
```csharp
app.MapWhen(
context => context.Request.Headers.ContainsKey("X-Custom-Header"),
branch =>
{
branch.Run(async context =>
await context.Response.WriteAsync("Custom header branch"));
});
```
---
### `UseWhen` – Conditionally add middleware (rejoins pipeline!) ⭐
This is often what you actually want — add extra middleware only for certain requests, but **stay in
the main pipeline**:
```csharp
// Only validate API key for /secure/* routes
app.UseWhen(
context => context.Request.Path.StartsWithSegments("/secure"),
branch => branch.UseMiddleware());
// All requests (including /secure/*) continue here
app.MapControllers();
```
### Decision Guide
flowchart TD
Q{"Do you need a completely\nseparate pipeline?"}
Q -- Yes --> M["Use Map() or MapWhen()"]
Q -- No --> U["Use UseWhen()"]
---
## 9. Middleware vs Filters – When to Use What
ASP.NET Core also has **Action Filters**, **Endpoint Filters**, etc. When should you use middleware
vs filters?
| Aspect | Middleware | Filters |
| -------------- | --------------------------------------------------------- | -------------------------------------------------------------- |
| Scope | **All requests** (even non-endpoint) | Only matched endpoints |
| Access to | `HttpContext` only | `HttpContext` + MVC context (ModelState, ActionArguments) |
| Runs when | Every request, always | Only when an endpoint is matched |
| Best for | Cross-cutting concerns (logging, CORS, auth, compression) | Endpoint-specific logic (validation, caching, transformations) |
| Pipeline level | Outer (HTTP pipeline) | Inner (MVC/endpoint pipeline) |
### Rule of Thumb
> **Use middleware** when the behavior should apply to **all requests** (or broad categories). **Use
> filters** when the behavior is tied to **specific endpoints or controllers**.
---
## 10. Real-World Patterns (with code)
### Pattern 1: Correlation ID for Distributed Tracing
Attach a unique ID to every request so you can trace it across microservices:
```csharp
public async Task InvokeAsync(HttpContext context)
{
var correlationId = context.Request.Headers["X-Correlation-Id"].FirstOrDefault()
?? Guid.NewGuid().ToString();
context.Items["CorrelationId"] = correlationId;
context.Response.OnStarting(() =>
{
context.Response.Headers["X-Correlation-Id"] = correlationId;
return Task.CompletedTask;
});
using (_logger.BeginScope(new Dictionary { ["CorrelationId"] = correlationId }))
{
await _next(context);
}
}
```
---
### Pattern 2: Global Exception Handling
Return consistent JSON error responses instead of default HTML error pages:
```csharp
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception ex)
{
_logger.LogError(ex, "Unhandled exception");
context.Response.StatusCode = 500;
context.Response.ContentType = "application/json";
await context.Response.WriteAsJsonAsync(new
{
error = "Internal Server Error",
message = ex.Message, // Hide in production!
traceId = Activity.Current?.Id ?? context.TraceIdentifier
});
}
}
```
---
### Pattern 3: Request/Response Logging
Log every request body and response for debugging or auditing:
```csharp
public async Task InvokeAsync(HttpContext context)
{
// Enable buffering so we can read the request body multiple times
context.Request.EnableBuffering();
var requestBody = await new StreamReader(context.Request.Body).ReadToEndAsync();
context.Request.Body.Position = 0; // Reset for downstream middleware
_logger.LogInformation("Request: {Method} {Path} Body: {Body}",
context.Request.Method, context.Request.Path, requestBody);
await _next(context);
_logger.LogInformation("Response: {StatusCode}", context.Response.StatusCode);
}
```
---
### Pattern 4: Simple Rate Limiting (before .NET 7)
```csharp
// Using a ConcurrentDictionary to track request counts per IP
private static readonly ConcurrentDictionary _clients = new();
public async Task InvokeAsync(HttpContext context)
{
var ip = context.Connection.RemoteIpAddress?.ToString() ?? "unknown";
var now = DateTime.UtcNow;
var entry = _clients.AddOrUpdate(ip,
_ => (1, now.AddMinutes(1)),
(_, existing) => existing.Window > now
? (existing.Count + 1, existing.Window)
: (1, now.AddMinutes(1)));
if (entry.Count > 100)
{
context.Response.StatusCode = 429;
await context.Response.WriteAsync("Too many requests");
return;
}
await _next(context);
}
```
---
## 11. Common Pitfalls & Best Practices
### ❌ Pitfalls
| Pitfall | Why it's bad |
| --------------------------------------------------- | ------------------------------------------------------------------------- |
| Modifying `Response.Headers` after `next()` | Response may already be sent — throws `InvalidOperationException` |
| Injecting scoped services in middleware constructor | Middleware is singleton — scoped service becomes a **captive dependency** |
| Forgetting to call `next()` | Silently short-circuits the pipeline; downstream middleware never runs |
| Calling `next()` after writing to response body | May corrupt the response or throw |
| Heavy logic in middleware | Runs on **every** request — use filters for endpoint-specific logic |
---
### ✅ Best Practices
1. **Keep middleware focused** — one responsibility per middleware
2. **Use extension methods** — `app.UseMyMiddleware()` is cleaner than `app.UseMiddleware()`
3. **Handle exceptions carefully** — don't let middleware exceptions crash the pipeline
4. **Use `IMiddleware`** when you need scoped DI
5. **Prefer `UseWhen` over `MapWhen`** when you want to rejoin the main pipeline
6. **Test your middleware** — it's just a class, inject a mock `RequestDelegate`
7. **Use `context.Response.OnStarting()`** to safely modify response headers
8. **Measure performance** — middleware runs on every request; keep it fast
### Testing a Middleware
```csharp
[Fact]
public async Task RequestTimingMiddleware_LogsElapsedTime()
{
var logger = new FakeLogger();
var middleware = new RequestTimingMiddleware(
next: (context) => Task.CompletedTask, // Mock next delegate
logger: logger);
var context = new DefaultHttpContext();
await middleware.InvokeAsync(context);
Assert.Contains(logger.Messages, m => m.Contains("completed in"));
}
```
---
## 12. Key Takeaways
| # | Takeaway |
| --- | ------------------------------------------------------------------------------------------------------------ |
| 1 | Middleware forms a **bidirectional pipeline** — request goes in, response comes out |
| 2 | **Order is everything** — exception handling first, auth before authz, static files before routing |
| 3 | **`Use` / `Run` / `Map`** are the three fundamental building blocks |
| 4 | **Short-circuiting** is powerful — use it for validation, caching, health checks |
| 5 | Use **convention-based classes** for production middleware (testable, injectable) |
| 6 | Use **`IMiddleware`** when you need per-request (scoped) DI |
| 7 | Use **`UseWhen`** (not `MapWhen`) when you want conditional middleware that rejoins the pipeline |
| 8 | **Middleware ≠ Filters** — middleware is for cross-cutting concerns; filters are for endpoint-specific logic |
| 9 | Middleware is the backbone of **everything** in ASP.NET Core — even the framework features are middleware |
---
## Resources
- 📖
[ASP.NET Core Middleware Docs](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/)
- 📖
[Write Custom Middleware](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/write)
- 📖
[Middleware Ordering](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/#middleware-order)
- 📖
[Filters in ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/filters)
- 📦 [Demo Project → `./MiddlewareDemo`](./MiddlewareDemo/)
-