Building Custom Middleware in ASP.NET Core
Introduction
In ASP.NET Core, middleware is the system that processes incoming HTTP requests and outgoing responses. Each middleware component can:
- Inspect the request
- Modify the request
- Call the next middleware
- Modify the response
Middleware runs in sequence, forming a structured pipeline. Understanding how to build custom middleware is important for tasks such as logging, authentication, performance tracking, and request transformation.
Understanding Middleware Basics
The request pipeline is made up of multiple middleware components. Each component can run:
- Before calling
next() - After the next component finishes
Here is the conceptual flow:
async Task ProcessRequest(HttpContext context)
{
// Middleware 1 - Before
await LoggingMiddleware(context, async () => {
// Middleware 2 - Before
await AuthenticationMiddleware(context, async () => {
// Middleware 3 - Before
await YourController(context);
// Middleware 3 - After
});
// Middleware 2 - After
});
// Middleware 1 - After
}
Implementation Methods
1. Request Delegates (Inline Middleware)
This method is simple and ideal for small features that do not require a separate class.
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.Use(async (context, next) =>
{
var timer = Stopwatch.StartNew();
var logger = context.RequestServices.GetService<ILogger<Program>>();
logger?.LogInformation("Processing {Method} {Path}",
context.Request.Method, context.Request.Path);
await next(context);
timer.Stop();
var elapsed = timer.ElapsedMilliseconds;
context.Response.Headers.Add("X-Response-Time", elapsed.ToString());
logger?.LogInformation("Completed {Method} {Path} in {ElapsedMs}ms",
context.Request.Method, context.Request.Path, elapsed);
});
app.Use(async (context, next) =>
{
if (!context.Request.Headers.ContainsKey("API-Version") &&
!context.Request.Query.ContainsKey("api-version"))
{
context.Request.Headers.Add("API-Version", "1.0");
}
var version = context.Request.Headers["API-Version"].FirstOrDefault() ??
context.Request.Query["api-version"].FirstOrDefault() ?? "1.0";
context.Items["API-Version"] = version;
await next(context);
});
app.Run();
}
}
When to use:
- Small features
- Quick checks
- Simple logic without many dependencies
2. Convention-Based Middleware (Class + Extension Method)
This method provides structure and reusable code. It supports dependency injection and is easy to test.
public class RequestLoggingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RequestLoggingMiddleware> _logger;
private readonly RequestLoggingOptions _options;
public RequestLoggingMiddleware(
RequestDelegate next,
ILogger<RequestLoggingMiddleware> logger,
IOptions<RequestLoggingOptions> options)
{
_next = next;
_logger = logger;
_options = options.Value;
}
public async Task InvokeAsync(HttpContext context)
{
var request = await FormatRequest(context.Request);
_logger.LogInformation($"Incoming Request: {request}");
await _next(context);
var response = await FormatResponse(context.Response);
_logger.LogInformation($"Outgoing Response: {response}");
}
private async Task<string> FormatRequest(HttpRequest request)
{
request.EnableBuffering();
var body = await new StreamReader(request.Body).ReadToEndAsync();
request.Body.Position = 0;
return $"{request.Method} {request.Path}{request.QueryString} {body}";
}
private async Task<string> FormatResponse(HttpResponse response)
{
response.Body.Seek(0, SeekOrigin.Begin);
var text = await new StreamReader(response.Body).ReadToEndAsync();
response.Body.Seek(0, SeekOrigin.Begin);
return $"{response.StatusCode}: {text}";
}
}
Extension Method
public static class RequestLoggingMiddlewareExtensions
{
public static IApplicationBuilder UseRequestLogging(
this IApplicationBuilder builder)
{
return builder.UseMiddleware<RequestLoggingMiddleware>();
}
}
Usage
app.UseRequestLogging();
When to use:
- Shared middleware
- Structured applications
- Need configuration support
3. Factory-Based Middleware (IMiddleware)
This is the most flexible and testable option. Each request gets its own instance.
public class PerformanceMiddleware : IMiddleware
{
private readonly ILogger<PerformanceMiddleware> _logger;
private readonly IMetricsService _metrics;
public PerformanceMiddleware(
ILogger<PerformanceMiddleware> logger,
IMetricsService metrics)
{
_logger = logger;
_metrics = metrics;
}
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
var timer = Stopwatch.StartNew();
var path = context.Request.Path;
try
{
await next(context);
timer.Stop();
await _metrics.RecordMetricAsync(new RequestMetric
{
Path = path,
Method = context.Request.Method,
Duration = timer.ElapsedMilliseconds,
StatusCode = context.Response.StatusCode
});
}
catch (Exception ex)
{
_logger.LogError(ex, "Request failed for {Path}", path);
throw;
}
}
}
Registration
services.AddTransient<PerformanceMiddleware>();
app.UseMiddleware<PerformanceMiddleware>();
When to use:
- Complex logic
- Full dependency injection
- Enterprise-scale apps
Best Practices
Error Handling
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
try
{
await next(context);
}
catch (Exception ex)
{
_logger.LogError(ex, "Unhandled exception");
throw;
}
}
Performance
- Avoid unnecessary buffering
- Use async operations
- Minimize allocations
Configuration Example
public class MiddlewareOptions
{
public bool EnableLogging { get; set; }
public string[] ExcludedPaths { get; set; }
public int TimeoutSeconds { get; set; }
}
services.Configure<MiddlewareOptions>(configuration.GetSection("Middleware"));
Advanced Scenarios
Conditional Middleware
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
if (!_options.ExcludedPaths.Contains(context.Request.Path))
{
await ProcessRequest(context);
}
await next(context);
}
Chaining Multiple Middleware
app.UseMiddleware<AuthenticationMiddleware>()
.UseMiddleware<LoggingMiddleware>()
.UseMiddleware<CompressionMiddleware>();
Branching
app.Map("/api", apiApp =>
{
apiApp.UseMiddleware<ApiVersionMiddleware>();
apiApp.UseMiddleware<ApiKeyMiddleware>();
});
Middleware Approach Comparison
Approach Complexity DI Support Best Use Case
---------------------------------------------------------------------------
Request Delegates Low Limited Small features, prototypes
Convention-Based Medium Good Reusable components
Factory-Based High Excellent Complex logic, enterprise apps