Creating Custom Middleware
🎯 What You'll Learn
- Three ways to create custom middleware
- Inline middleware with app.Use()
- Convention-based middleware classes
- Factory-based middleware with IMiddleware
- Dependency injection in middleware
- Extension methods for middleware
- Practical InvenTrack examples
- Best practices
Three Ways to Create Middleware
| Approach | When to Use | Complexity |
|---|---|---|
| Inline | Simple, one-off logic | Low |
| Convention-based | Most common, reusable | Medium |
| Factory-based | Complex DI scenarios | High |
1. Inline Middleware
Use app.Use() for simple, one-off middleware:
app.Use(async (context, next) =>
{
var stopwatch = Stopwatch.StartNew();
await next();
stopwatch.Stop();
var elapsed = stopwatch.ElapsedMilliseconds;
context.Response.Headers["X-Response-Time"] = $"{elapsed}ms";
});
API Key Validation
app.Use(async (context, next) =>
{
if (!context.Request.Headers.TryGetValue("X-API-Key", out var apiKey))
{
context.Response.StatusCode = 401;
await context.Response.WriteAsync("API Key missing");
return; // Short-circuit
}
if (apiKey != "secret-key-123")
{
context.Response.StatusCode = 403;
await context.Response.WriteAsync("Invalid API Key");
return;
}
await next();
});
Inline middleware is great for:
- Quick prototyping
- Simple logic that won't be reused
- Learning and experimentation
For production code, use convention-based or factory-based middleware.
2. Convention-Based Middleware
The most common approach. Create a class with a specific structure:
Basic Structure
public class RequestLoggingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RequestLoggingMiddleware> _logger;
// Constructor: Inject RequestDelegate and singleton services
public RequestLoggingMiddleware(
RequestDelegate next,
ILogger<RequestLoggingMiddleware> logger)
{
_next = next;
_logger = logger;
}
// InvokeAsync: Process the request
public async Task InvokeAsync(HttpContext context)
{
_logger.LogInformation(
"Request: {Method} {Path}",
context.Request.Method,
context.Request.Path);
await _next(context);
_logger.LogInformation(
"Response: {StatusCode}",
context.Response.StatusCode);
}
}
Registration
app.UseMiddleware<RequestLoggingMiddleware>();
Extension Method (Recommended)
public static class MiddlewareExtensions
{
public static IApplicationBuilder UseRequestLogging(
this IApplicationBuilder app)
{
return app.UseMiddleware<RequestLoggingMiddleware>();
}
}
app.UseRequestLogging();
Dependency Injection in Middleware
Constructor Injection (Singleton Services Only)
public class MyMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<MyMiddleware> _logger; // Singleton
public MyMiddleware(
RequestDelegate next,
ILogger<MyMiddleware> logger)
{
_next = next;
_logger = logger;
}
}
InvokeAsync Injection (Scoped Services)
public class DatabaseHealthCheckMiddleware
{
private readonly RequestDelegate _next;
public DatabaseHealthCheckMiddleware(RequestDelegate next)
{
_next = next;
}
// Inject scoped services here!
public async Task InvokeAsync(
HttpContext context,
InvenTrackDbContext dbContext) // Scoped!
{
if (context.Request.Path == "/health")
{
var canConnect = await dbContext.Database.CanConnectAsync();
context.Response.StatusCode = canConnect ? 200 : 503;
await context.Response.WriteAsync(
canConnect ? "Healthy" : "Unhealthy");
return;
}
await _next(context);
}
}
Constructor: Only inject singleton services (ILogger,
IConfiguration)
InvokeAsync: Can inject scoped services (DbContext,
repositories)
Why? Middleware instances are created once (singleton lifetime), but InvokeAsync is called per request.
3. Factory-Based Middleware
Implement IMiddleware for complex DI scenarios:
public class TenantMiddleware : IMiddleware
{
private readonly ITenantService _tenantService;
// Can inject scoped services in constructor!
public TenantMiddleware(ITenantService tenantService)
{
_tenantService = tenantService;
}
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
var tenantId = context.Request.Headers["X-Tenant-Id"].ToString();
if (string.IsNullOrEmpty(tenantId))
{
context.Response.StatusCode = 400;
await context.Response.WriteAsync("Tenant ID required");
return;
}
await _tenantService.SetCurrentTenantAsync(tenantId);
await next(context);
}
}
Registration
// Register as scoped
builder.Services.AddScoped<TenantMiddleware>();
// Use it
app.UseMiddleware<TenantMiddleware>();
Complete InvenTrack Examples
Example 1: Request Timing Middleware
public 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();
var elapsed = stopwatch.ElapsedMilliseconds;
_logger.LogInformation(
"{Method} {Path} completed in {Elapsed}ms with status {StatusCode}",
context.Request.Method,
context.Request.Path,
elapsed,
context.Response.StatusCode);
context.Response.Headers["X-Response-Time-Ms"] = elapsed.ToString();
}
}
}
Example 2: API Key Validation Middleware
public class ApiKeyMiddleware
{
private readonly RequestDelegate _next;
private readonly IConfiguration _configuration;
public ApiKeyMiddleware(
RequestDelegate next,
IConfiguration configuration)
{
_next = next;
_configuration = configuration;
}
public async Task InvokeAsync(HttpContext context)
{
// Skip API key check for health endpoint
if (context.Request.Path.StartsWithSegments("/health"))
{
await _next(context);
return;
}
if (!context.Request.Headers.TryGetValue("X-API-Key", out var providedKey))
{
context.Response.StatusCode = 401;
await context.Response.WriteAsJsonAsync(new
{
error = "API Key is missing"
});
return;
}
var validKey = _configuration["ApiKey"];
if (providedKey != validKey)
{
context.Response.StatusCode = 403;
await context.Response.WriteAsJsonAsync(new
{
error = "Invalid API Key"
});
return;
}
await _next(context);
}
}
Example 3: Correlation ID Middleware
public class CorrelationIdMiddleware
{
private readonly RequestDelegate _next;
private const string CorrelationIdHeader = "X-Correlation-Id";
public CorrelationIdMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
// Get or generate correlation ID
var correlationId = context.Request.Headers[CorrelationIdHeader].FirstOrDefault()
?? Guid.NewGuid().ToString();
// Store in HttpContext.Items for use in controllers/services
context.Items[CorrelationIdHeader] = correlationId;
// Add to response headers
context.Response.Headers[CorrelationIdHeader] = correlationId;
await _next(context);
}
}
Extension Methods
public static class MiddlewareExtensions
{
public static IApplicationBuilder UseRequestTiming(
this IApplicationBuilder app)
{
return app.UseMiddleware<RequestTimingMiddleware>();
}
public static IApplicationBuilder UseApiKey(
this IApplicationBuilder app)
{
return app.UseMiddleware<ApiKeyMiddleware>();
}
public static IApplicationBuilder UseCorrelationId(
this IApplicationBuilder app)
{
return app.UseMiddleware<CorrelationIdMiddleware>();
}
}
Usage in Program.cs
var app = builder.Build();
// Custom middleware
app.UseCorrelationId();
app.UseRequestTiming();
app.UseApiKey();
// Built-in middleware
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
Best Practices
- Use extension methods for clean registration
- Constructor: Only inject singleton services
- InvokeAsync: Can inject scoped services
- Always call next() unless short-circuiting intentionally
- Use try/finally for cleanup (like timing)
- Set response headers before calling next() if needed
- Check response.HasStarted before modifying response
- Use HttpContext.Items to pass data to later middleware
- Keep middleware focused on one responsibility
- Test middleware in isolation
Key Takeaways
- Three approaches: Inline, Convention-based, Factory-based
- Inline: Quick and simple with
app.Use() - Convention-based: Most common, reusable classes
- Factory-based: Implement
IMiddlewarefor complex DI - DI rules: Constructor (singleton), InvokeAsync (scoped)
- Extension methods make registration clean
- Middleware is created once (singleton lifetime)
- InvokeAsync is called per request
- Always use
try/finallyfor cleanup - Test middleware independently
You now know how to create custom middleware! In the next section, we'll explore Middleware Ordering—understanding why order matters, common ordering patterns, and how to avoid common mistakes.