middleware-patterns
Custom middleware patterns for ASP.NET Core applications. Covers request/response pipeline, middleware ordering, conditional middleware, IMiddleware factory pattern, IExceptionHandler (.NET 8+), and reusable middleware components. Use when creating custom middleware in ASP.NET Core applications, understanding middleware pipeline ordering, or implementing cross-cutting concerns like logging, authentication, and caching.
What this skill does
# ASP.NET Core Middleware Patterns
## Rationale
Middleware is the backbone of ASP.NET Core request processing. Properly designed middleware enables cross-cutting concerns like logging, authentication, and caching. Understanding the pipeline order and middleware patterns is critical for building robust applications.
---
## Pipeline Ordering
Middleware executes in the order it is registered. The order is critical -- placing middleware in the wrong position causes subtle bugs.
### Recommended Order
```csharp
var app = builder.Build();
// 1. Exception handling (outermost -- catches everything below)
app.UseExceptionHandler("/error");
// 2. HSTS (before any response is sent)
if (!app.Environment.IsDevelopment())
{
app.UseHsts();
}
// 3. HTTPS redirection
app.UseHttpsRedirection();
// 4. Static files (short-circuits for static content before routing)
app.UseStaticFiles();
// 5. Routing (matches endpoints but does not execute them yet)
app.UseRouting();
// 6. CORS (must be after routing, before auth)
app.UseCors();
// 7. Authentication (identifies the user)
app.UseAuthentication();
// 8. Authorization (checks permissions against the matched endpoint)
app.UseAuthorization();
// 9. Custom middleware (runs after auth, before endpoint execution)
app.UseRequestLogging();
// 10. Endpoint execution (terminal -- executes the matched endpoint)
app.MapControllers();
app.MapRazorPages();
```
### Why Order Matters
| Mistake | Consequence |
|---------|-------------|
| `UseAuthorization()` before `UseRouting()` | Authorization has no endpoint metadata -- all requests pass |
| `UseCors()` after `UseAuthorization()` | Preflight requests fail because they lack auth tokens |
| `UseExceptionHandler()` after custom middleware | Exceptions in custom middleware are unhandled |
| `UseStaticFiles()` after `UseAuthorization()` | Static files require authentication unnecessarily |
---
## Pattern 1: Convention-Based Middleware
Convention-based middleware uses a constructor with `RequestDelegate` and an `InvokeAsync` method.
```csharp
public sealed class RequestTimingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RequestTimingMiddleware> _logger;
public RequestTimingMiddleware(
RequestDelegate next,
ILogger<RequestTimingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
var stopwatch = Stopwatch.StartNew();
try
{
await _next(context);
}
finally
{
stopwatch.Stop();
_logger.LogInformation(
"Request {Method} {Path} completed in {ElapsedMs}ms with status {StatusCode}",
context.Request.Method,
context.Request.Path,
stopwatch.ElapsedMilliseconds,
context.Response.StatusCode);
}
}
}
public static class RequestTimingMiddlewareExtensions
{
public static IApplicationBuilder UseRequestTiming(this IApplicationBuilder app)
=> app.UseMiddleware<RequestTimingMiddleware>();
}
// Usage in Program.cs
app.UseRequestTiming();
```
---
## Pattern 2: Factory-Based (IMiddleware)
For middleware that requires scoped services, implement `IMiddleware`. This uses DI to create middleware instances per-request:
```csharp
public sealed class TenantMiddleware : IMiddleware
{
private readonly TenantDbContext _db;
public TenantMiddleware(TenantDbContext db)
{
_db = db;
}
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
var tenantId = context.Request.Headers["X-Tenant-Id"].FirstOrDefault();
if (tenantId is not null)
{
var tenant = await _db.Tenants.FindAsync(tenantId);
context.Items["Tenant"] = tenant;
}
await next(context);
}
}
// IMiddleware requires explicit DI registration
builder.Services.AddScoped<TenantMiddleware>();
app.UseMiddleware<TenantMiddleware>();
```
### Convention-Based vs IMiddleware
| Aspect | Convention-based | `IMiddleware` |
|--------|-----------------|---------------|
| Lifetime | Singleton (created once) | Per-request (from DI) |
| Scoped services | Via `InvokeAsync` parameters only | Via constructor injection |
| Registration | `UseMiddleware<T>()` only | Requires `services.Add*<T>()` + `UseMiddleware<T>()` |
| Performance | Slightly faster | Resolved from DI each request |
---
## Pattern 3: Inline Middleware
For simple, one-off logic:
### app.Use -- Pass-Through
```csharp
app.Use(async (context, next) =>
{
context.Response.Headers["X-Request-Id"] = context.TraceIdentifier;
await next(context);
});
```
### app.Run -- Terminal
```csharp
app.Run(async context =>
{
await context.Response.WriteAsync("Fallback response");
});
```
### app.Map -- Branch by Path
```csharp
app.Map("/api/diagnostics", diagnosticApp =>
{
diagnosticApp.Run(async context =>
{
var data = new
{
MachineName = Environment.MachineName,
Timestamp = DateTimeOffset.UtcNow
};
await context.Response.WriteAsJsonAsync(data);
});
});
```
---
## Pattern 4: Short-Circuit Logic
Middleware can short-circuit the pipeline by not calling `next()`.
### Request Validation
```csharp
public sealed class ApiKeyMiddleware
{
private readonly RequestDelegate _next;
private readonly string _expectedKey;
public ApiKeyMiddleware(RequestDelegate next, IConfiguration config)
{
_next = next;
_expectedKey = config["ApiKey"]
?? throw new InvalidOperationException("ApiKey configuration is required");
}
public async Task InvokeAsync(HttpContext context)
{
if (!context.Request.Headers.TryGetValue("X-Api-Key", out var providedKey)
|| !string.Equals(providedKey, _expectedKey, StringComparison.Ordinal))
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
await context.Response.WriteAsJsonAsync(new
{
Error = "Invalid or missing API key"
});
return; // Short-circuit
}
await _next(context);
}
}
```
### Feature Flag Gate
```csharp
app.UseWhen(
context => context.Request.Path.StartsWithSegments("/beta"),
betaApp =>
{
betaApp.Use(async (context, next) =>
{
var featureManager = context.RequestServices
.GetRequiredService<IFeatureManager>();
if (!await featureManager.IsEnabledAsync("BetaFeatures"))
{
context.Response.StatusCode = StatusCodes.Status404NotFound;
return;
}
await next(context);
});
});
```
---
## Pattern 5: Request and Response Manipulation
### Reading the Request Body
```csharp
public sealed class RequestLoggingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RequestLoggingMiddleware> _logger;
public RequestLoggingMiddleware(RequestDelegate next, ILogger<RequestLoggingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
context.Request.EnableBuffering();
if (context.Request.ContentLength > 0 && context.Request.ContentLength < 64_000)
{
context.Request.Body.Position = 0;
using var reader = new StreamReader(context.Request.Body, leaveOpen: true);
var body = await reader.ReadToEndAsync();
_logger.LogDebug("Request body for {Path}: {Body}", context.Request.Path, body);
context.Request.Body.Position = 0;
}
await _next(context);
}
}
```
### Modifying the Response
```csharp
public async Task InvokeAsync(HttpContext context)
{
var originalBodyStream = context.Response.Body;
using var reRelated in General
modeling-omnistudio-epc-catalog
IncludedSalesforce Industries CME EPC product-modeling skill for Product2-based catalog creation. Use when creating EPC products, configuring product attributes, building offer bundles with Product Child Items, or reviewing EPC DataPack JSON metadata for product catalog changes. TRIGGER when: user creates or updates Product2 EPC records, AttributeAssignment payloads, AttributeMetadata/AttributeDefaultValues, Offer bundles, or ProductChildItem relationships. DO NOT TRIGGER when: designing OmniScripts/FlexCards/Integration Procedures (use building-omnistudio-omniscript, building-omnistudio-flexcard, or building-omnistudio-integration-procedure), implementing Apex business logic (use generating-apex), or troubleshooting deployment pipelines (use deploying-metadata).
relationship-science-coach
IncludedUse this skill for direct, practical adult relationship coaching: couples conflict, repair, trust, marriage, dating, flirting, attachment patterns, emotional connection, sex, desire differences, eroticism, kink negotiation, affection, love languages, breakups, and long-term passion. Draw on Gottman, EFT and Hold Me Tight, attachment science, modern sex research, Perel, Nagoski, Kerner, Schnarch, Love and Stosny, and flexible love-language tools. Be concrete and low-hedge. Redirect only for imminent danger, abuse, coercive control, minors, non-consent, self-harm, stalking, or medical/legal/psychiatric decisions.
building-sf-integrations
IncludedSalesforce integration architecture and runtime plumbing with 120-point scoring. Use this skill to set up Named Credentials, External Credentials, External Services, REST/SOAP callout patterns, Platform Events, and Change Data Capture. TRIGGER when: user sets up Named Credentials, External Services, REST/SOAP callouts, Platform Events, CDC, or touches .namedCredential-meta.xml files. DO NOT TRIGGER when: Connected App/OAuth config (use configuring-connected-apps), Apex-only logic (use generating-apex), or data import/export (use handling-sf-data).
venue-templates
IncludedAccess comprehensive LaTeX templates, formatting requirements, and submission guidelines for major scientific publication venues (Nature, Science, PLOS, IEEE, ACM), academic conferences (NeurIPS, ICML, CVPR, CHI), research posters, and grant proposals (NSF, NIH, DOE, DARPA). This skill should be used when preparing manuscripts for journal submission, conference papers, research posters, or grant proposals and need venue-specific formatting requirements and templates.
let-fate-decide
IncludedDraws the 12 Houses of the Zodiac Tarot spread to inject entropy into planning when prompts are vague, ambiguous, or casually delegated. Interprets the spread to guide next steps. Use when the user says 'let fate decide', 'YOLO', 'whatever', 'idk', or other nonchalant phrases, makes Yu-Gi-Oh references, or when you are about to arbitrarily pick between multiple reasonable approaches. Prefer over ask-questions-if-underspecified when the user's tone is casual or playful rather than precision-seeking.
net-ops
IncludedCross-platform network troubleshooting (Windows, macOS, Linux) via local or remote shell. Use for: DNS broken, can't resolve hostnames, nslookup/dig works but apps fail, NRPT, WFP, scutil, /etc/resolver, systemd-resolved, /etc/resolv.conf, NetworkManager, VPN DNS leak residue (ProtonVPN/Mullvad/WireGuard/AnyConnect), AV/firewall blocking DNS or DoH, Tailscale DNS interaction, intermittent connectivity, remote diagnostics over SSH.