.NET Middleware
Introduction
In ASP.NET Core applications, middleware is a crucial concept that forms the backbone of the request processing pipeline. When a client sends a request to an ASP.NET Core application, the request passes through a series of middleware components before generating a response that is sent back to the client.
Think of middleware as a pipeline of components, where each component can:
- Process incoming HTTP requests
- Modify requests and responses
- Pass the request to the next component in the pipeline
- Short-circuit the pipeline by not calling the next component
This article will explore what middleware is, how it works in ASP.NET Core applications, and how you can build custom middleware to extend your application's functionality.
Understanding the Middleware Pipeline
When an ASP.NET Core application starts, it configures a request processing pipeline that handles all incoming HTTP requests. This pipeline consists of middleware components arranged in a specific order.
How the Pipeline Works
- The server receives an HTTP request
- The request enters the pipeline at the first middleware
- Each middleware can perform operations before and after the next middleware
- The last middleware generates a response
- The response travels back through the pipeline in reverse order
- The server sends the response to the client
Here's a visual representation of how the pipeline works:
Client Request → [Middleware 1] → [Middleware 2] → [Middleware 3] → Application Logic
↓
Client Response ← [Middleware 1] ← [Middleware 2] ← [Middleware 3] ← Response Generation
Configuring Middleware
Middleware components are configured in the Program.cs
file (or Startup.cs
in older versions) using the WebApplication
builder. Here's a basic example of middleware configuration:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// Configure the middleware pipeline
app.UseExceptionHandler("/Error");
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapRazorPages();
app.Run();
The order in which middleware is added is critical because it determines the sequence of request processing and the capabilities available at each stage of the pipeline.
Built-in Middleware Components
ASP.NET Core comes with several built-in middleware components that provide common functionalities:
1. Exception Handler Middleware
Catches exceptions thrown in the application and returns an appropriate error response.
app.UseExceptionHandler("/Error");
2. HTTPS Redirection Middleware
Redirects HTTP requests to HTTPS.
app.UseHttpsRedirection();
3. Static Files Middleware
Serves static files like JavaScript, CSS, and images directly without processing.
app.UseStaticFiles();
4. Routing Middleware
Determines which endpoint handler should process the request.
app.UseRouting();
5. Authentication and Authorization Middleware
Verifies user identity and ensures users are authorized to access resources.
app.UseAuthentication();
app.UseAuthorization();
6. CORS Middleware
Enables cross-origin resource sharing for your application.
app.UseCors();
Creating Custom Middleware
There are three ways to build custom middleware in ASP.NET Core:
1. Using In-line Anonymous Functions (Use Delegates)
The simplest way to create middleware is with an in-line anonymous function:
app.Use(async (context, next) =>
{
// Do something before the next middleware
Console.WriteLine($"Request started at: {DateTime.Now}");
// Call the next middleware in the pipeline
await next.Invoke();
// Do something after the next middleware
Console.WriteLine($"Request completed at: {DateTime.Now}");
});
2. Creating Middleware Classes
For more complex middleware, you can create a dedicated middleware class:
public class RequestLoggingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger _logger;
public RequestLoggingMiddleware(RequestDelegate next, ILogger<RequestLoggingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
_logger.LogInformation($"Handling request: {context.Request.Method} {context.Request.Path}");
// Call the next middleware
await _next(context);
_logger.LogInformation($"Request completed with status code: {context.Response.StatusCode}");
}
}
// Extension method to make it easier to use in Program.cs
public static class RequestLoggingMiddlewareExtensions
{
public static IApplicationBuilder UseRequestLogging(this IApplicationBuilder builder)
{
return builder.UseMiddleware<RequestLoggingMiddleware>();
}
}
And then use it in your application like this:
// In Program.cs
app.UseRequestLogging();
3. Using Factory-based Middleware Activation
A third approach using middleware factory methods:
app.UseMiddleware<RequestLoggingMiddleware>();
Real-World Example: Request Timing Middleware
Let's create a practical example of middleware that measures and logs the execution time of each request:
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 sw = Stopwatch.StartNew();
try
{
// Call the next middleware
await _next(context);
}
finally
{
sw.Stop();
var path = context.Request.Path;
var method = context.Request.Method;
var statusCode = context.Response.StatusCode;
var elapsed = sw.ElapsedMilliseconds;
_logger.LogInformation(
"Request {Method} {Path} completed with {StatusCode} in {ElapsedMs}ms",
method, path, statusCode, elapsed);
// Add response header with timing information
context.Response.Headers.Add("X-Request-Timing", $"{elapsed}ms");
}
}
}
// Extension method
public static class RequestTimingMiddlewareExtensions
{
public static IApplicationBuilder UseRequestTiming(this IApplicationBuilder builder)
{
return builder.UseMiddleware<RequestTimingMiddleware>();
}
}
You can use this middleware in your application:
// Add at the beginning of the pipeline to capture the full request timing
app.UseRequestTiming();
// Other middleware follows
app.UseHttpsRedirection();
app.UseStaticFiles();
// etc.
When a request comes in, this middleware will:
- Start a stopwatch
- Pass the request to the next middleware
- When the response comes back, calculate the elapsed time
- Log the information and add a custom response header
Short-Circuiting the Pipeline
Sometimes you want middleware to handle a request completely without passing it to the next component. This is called "short-circuiting" the pipeline:
app.Use(async (context, next) =>
{
// Check if this is a health check request
if (context.Request.Path.StartsWithSegments("/health"))
{
context.Response.StatusCode = 200;
await context.Response.WriteAsync("Healthy");
// Notice we're NOT calling next() here, effectively short-circuiting the pipeline
return;
}
// For all other requests, continue the pipeline
await next();
});
Terminal Middleware
Terminal middleware is the last middleware in the pipeline and doesn't call the next delegate. The Run
method is used for terminal middleware:
app.Run(async context =>
{
await context.Response.WriteAsync("Hello from terminal middleware!");
});
Any middleware added after a Run
call will never be reached for that route.
Branching the Pipeline with Map
ASP.NET Core allows branching the middleware pipeline based on request paths using Map
:
app.Map("/api", apiApp =>
{
apiApp.Use(async (context, next) =>
{
// API-specific middleware
context.Response.Headers.Add("X-API-Version", "1.0");
await next();
});
// More API-specific middleware
});
// Middleware here will only run for non-API routes
MapWhen for Conditional Branching
For more complex conditions, you can use MapWhen
:
app.MapWhen(
context => context.Request.Query.ContainsKey("debug"),
debugApp =>
{
debugApp.Use(async (context, next) =>
{
// Only runs when the request has a "debug" query parameter
context.Response.Headers.Add("X-Debug-Mode", "Enabled");
await next();
});
}
);
Practical Example: Cultural Middleware
Here's a practical example of middleware that sets the current culture based on a query string parameter:
public class CultureMiddleware
{
private readonly RequestDelegate _next;
public CultureMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
var cultureQuery = context.Request.Query["culture"];
if (!string.IsNullOrWhiteSpace(cultureQuery))
{
var culture = new CultureInfo(cultureQuery);
CultureInfo.CurrentCulture = culture;
CultureInfo.CurrentUICulture = culture;
}
// Call the next delegate/middleware in the pipeline
await _next(context);
}
}
// Extension method
public static class CultureMiddlewareExtensions
{
public static IApplicationBuilder UseCultureSwitcher(this IApplicationBuilder builder)
{
return builder.UseMiddleware<CultureMiddleware>();
}
}
Usage in Program.cs:
app.UseCultureSwitcher();
Now, users can switch cultures by adding a culture
query parameter to their requests:
https://yourapp.com/page?culture=fr-FR
https://yourapp.com/page?culture=es-ES
Best Practices for Middleware
-
Order matters: Carefully consider the order in which middleware is added to the pipeline. Some middleware depends on others being executed first.
-
Exception handling: Place exception handling middleware early in the pipeline to catch exceptions from subsequent middleware.
-
Security middleware: Authentication and authorization middleware should come before endpoint execution but after middleware that doesn't need authentication.
-
Consider performance: Keep middleware lightweight and efficient, especially for operations that run on every request.
-
Reusability: Design middleware to be reusable across different applications when possible.
-
Separation of concerns: Each middleware component should have a single responsibility.
-
Use dependency injection: Leverage the built-in dependency injection for services your middleware needs.
Summary
Middleware in ASP.NET Core provides a powerful way to handle HTTP requests and responses in your web application. By understanding how middleware works and how to create custom components, you can build flexible and maintainable web applications.
Key points to remember:
- Middleware forms a pipeline that processes HTTP requests and responses
- Built-in middleware provides common functionality like authentication and static file serving
- Custom middleware can extend your application with specialized behavior
- The order of middleware registration is crucial
- Middleware can short-circuit the pipeline when needed
- You can branch the pipeline based on request characteristics
Additional Resources
- ASP.NET Core Middleware Official Documentation
- Writing Clean ASP.NET Core Middleware
- Deep Dive into ASP.NET Core Middleware
Exercises
-
Create a middleware that adds a custom header with the server's current time to each response.
-
Implement a middleware that blocks requests from certain IP addresses.
-
Build a middleware component that logs all form data submitted to your application (be mindful of sensitive information).
-
Create a caching middleware that stores responses for GET requests and serves them from cache for subsequent identical requests.
-
Implement a middleware that tracks and limits the number of requests from a single IP address within a time window (rate limiting).
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)