1. Introduction
Middleware in ASP.NET Core refers to software components that are assembled into an application pipeline to handle requests and responses. They are configured as part of the request processing pipeline in the Startup.cs file. Middleware can help separate cross-cutting concerns from app logic for modular and maintainable code, so it is good for logging, authentication, caching, compression, security, and more.
ASP.NET Core includes many built-in middleware like Static File Middleware, Routing Middleware, Endpoint Routing Middleware, etc. But in this article, we will create our middleware
2. Create and use the Middleware
For creating a middleware, there are two things that need to be done:
1) Create a constructor to inject the RequestDelegate
for the next middleware
2) Implement the InvokeAsync
method, pass the business object in it, and then you can handle everything that you want!
For example, we create a request log middleware to log each request, then we need to create a constructor as below
private readonly RequestDelegate _next;
public RequestLogMiddleware(RequestDelegate next)
{
_next = next;
}
because we need to log the message, so it needs to pass the logger
object in InvokeAsync
, and define the logger in global
private readonly ILogger<RequestLogMiddleware> _logger;
public async Task InvokeAsync(HttpContext context, ILogger<RequestLogMiddleware> logger)
{
logger.LogDebug("Request received: {Method} {Path}", context.Request.Method, context.Request.Path);
await _next(context);
logger.LogDebug("Response sent: {StatusCode}", context.Response.StatusCode);
}
So, the complete code is below
public class RequestLogMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RequestLogMiddleware> _logger;
public RequestLogMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context, ILogger<RequestLogMiddleware> logger)
{
logger.LogDebug("Request received: {Method} {Path}", context.Request.Method, context.Request.Path);
await _next(context);
logger.LogDebug("Response sent: {StatusCode}", context.Response.StatusCode);
}
}
after that, we can create an extension method for easy use it
public static class RequestLogMiddlewareExtensions
{
public static IApplicationBuilder UseRequestLogMiddleware(
this IApplicationBuilder builder)
{
return builder.UseMiddleware<RequestLogMiddleware>();
}
}
in the end, we can use the middleware in program.cs
var app = builder.Build();
app.UseRequestLogMiddleware();
3. Other Useful Middlewares
The middleware is easy to create and use, so I think the main point is what middleware (or ideas) can we create. I will show you some of them below 🙂
3.1. Error Handling Middleware
public class ErrorHandlingMiddleware
{
private readonly RequestDelegate _next;
public ErrorHandlingMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception ex)
{
// Log error
if (context.Response.StatusCode == 404)
{
// Redirect to 404 page
context.Response.Redirect("/error/404");
}
else
{
// Handle other errors
context.Response.StatusCode = 500;
context.Response.ContentType = "text/plain";
await context.Response.WriteAsync("An unexpected error occurred!");
}
}
}
}
public static class ErrorHandlingMiddlewareExtensions
{
public static IApplicationBuilder UseErrorHandlingMiddleware(
this IApplicationBuilder builder)
{
return builder.UseMiddleware<ErrorHandlingMiddleware>();
}
}
This middleware wraps all subsequent handlers in a try-catch block. If any unhandled exception occurs in later middleware, it will be caught here.
3.2. Response Compression Middleware
Compress responses to reduce bandwidth usage.
public class ResponseCompressionMiddleware
{
private readonly RequestDelegate _next;
public ResponseCompressionMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
// Check if client supports compression
if(context.Request.Headers["Accept-Encoding"].Contains("gzip"))
{
// Stream to hold compressed response
MemoryStream compressedStream = new MemoryStream();
// Compress stream
using (GZipStream gzipStream = new GZipStream(compressedStream, CompressionMode.Compress))
{
await _next(context);
context.Response.Body.CopyTo(gzipStream);
}
// Replace uncompressed response with compressed one
context.Response.Body = compressedStream;
// Header to indicate compressed response
context.Response.Headers.Add("Content-Encoding", "gzip");
}
else
{
await _next(context);
}
}
}
public static class ResponseCompressionMiddlewareExtensions
{
public static IApplicationBuilder UseResponseCompressionMiddleware(
this IApplicationBuilder builder)
{
return builder.UseMiddleware<ResponseCompressionMiddleware>();
}
}
This middleware checks the request headers, compresses the response using GZipStream if the client accepts gzip, and replaces the uncompressed response with the compressed one. This reduces bandwidth usage.
3.3. Async Middleware
Implementing asynchronous logic and database calls in a middleware using async/await.
public class AsyncMiddleware
{
private readonly RequestDelegate _next;
public AsyncMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
// Call database async
var data = await FetchFromDatabase();
// Network call
var response = await CallExternalService();
// Set context info
context.Items["data"] = data;
// Call next middleware
await _next(context);
}
private async Task<Data> FetchFromDatabase()
{
// Fetch data from database
}
private async Task<Response> CallExternalService()
{
// Call API
}
}
public static class AsyncMiddlewareExtensions
{
public static IApplicationBuilder UseAsyncMiddleware(
this IApplicationBuilder builder)
{
return builder.UseMiddleware<AsyncMiddleware>();
}
}
This allows the middleware to perform asynchronous operations like database queries, network calls, long-running CPU tasks, etc. without blocking the request thread. The next middleware is invoked once the async work is completed.
3.4. Security Middleware
Verify user rights and role whether is correct
public class SecurityMiddleware
{
public async Task Invoke(HttpContext context)
{
// Check authentication
if(!context.User.Identity.IsAuthenticated)
{
context.Response.Redirect("/account/login");
return;
}
// Check authorization for admin page
if(context.Request.Path.Value.StartsWith("/admin") && !context.User.HasRequiredRole("Admin"))
{
context.Response.StatusCode = 403;
return;
}
// Validate business rules
if(!IsValidRequest(context.Request))
{
context.Response.StatusCode = 400;
return;
}
await _next(context);
}
private bool IsValidRequest(HttpRequest request)
{
// Check headers, params, business rules etc.
return true;
}
}
public static class SecurityMiddlewareExtensions
{
public static IApplicationBuilder UseSecurityMiddleware(
this IApplicationBuilder builder)
{
return builder.UseMiddleware<SecurityMiddleware>();
}
}
This middleware encapsulates all security and validation logic into a single middleware that is applied globally. The app code only focuses on business logic.
3.5. Localization Middleware
Set culture, localization, or time zones based on request attributes.
public class LocalizationMiddleware
{
public async Task Invoke(HttpContext context)
{
var userLanguage = context.Request.Headers["Accept-Language"].FirstOrDefault();
// Set culture from header
CultureInfo culture;
if(!string.IsNullOrEmpty(userLanguage))
{
culture = new CultureInfo(userLanguage);
}
else
{
culture = new CultureInfo("en-US");
}
CultureInfo.CurrentCulture = culture;
CultureInfo.CurrentUICulture = culture;
// Set timezone
var timezone = context.Request.Headers["TimeZone"].FirstOrDefault();
if(!string.IsNullOrEmpty(timezone))
{
TimeZoneInfo timeZone = TimeZoneInfo.FindSystemTimeZoneById(timezone);
TimeZoneInfo.Local = timeZone;
}
await _next(context);
}
}
public static class LocalizationMiddlewareExtensions
{
public static IApplicationBuilder UseLocalizationMiddleware(
this IApplicationBuilder builder)
{
return builder.UseMiddleware<LocalizationMiddleware>();
}
}
This middleware checks request headers for the user’s preferred language and timezone. It sets CurrentCulture and CurrentUICulture based on Accept-Language header. It also sets the Local TimeZone based on the TimeZone header. This can be used to dynamically localize responses for each user by looking up their headers. The app will format dates, numbers, and currencies appropriately for the user. The middleware must be configured early in program.cs
before localizable
content is generated.
3.6. Session Middleware
Manipulate session state and manage cookies
public class SessionMiddleware
{
public async Task Invoke(HttpContext context)
{
// Get session object
var session = context.Session;
// Set value in session
session.SetString("Key", "Value");
// Get value from session
var value = session.GetString("Key");
// Create cookie
context.Response.Cookies.Append("CookieName", "CookieValue");
// Delete cookie
context.Response.Cookies.Delete("CookieName");
// Set cookie expiration
context.Response.Cookies.Append("CookieName", "Value", new CookieOptions
{
Expires = DateTime.Now.AddDays(1)
});
await _next(context);
}
}
public static class SessionMiddlewareExtensions
{
public static IApplicationBuilder UseSessionMiddleware(
this IApplicationBuilder builder)
{
return builder.UseMiddleware<SessionMiddleware>();
}
}
Session state and cookies are often used for user data, preferences, shopping carts, tokens etc. that need to persist across multiple requests. This middleware provides a centralized place to manage both sessions and cookies in your application.
3.7. RateLimit Middleware
A middleware that limits the number of requests from a client in a time period to prevent abuse/DDoS
. It would keep track of requests and respond with 429 Too Many Requests if the limit exceeds.
public class RateLimitMiddleware
{
private const int PerMinuteLimit = 10;
private static Dictionary<string, int> _requests = new Dictionary<string, int>();
public async Task Invoke(HttpContext context)
{
var clientIp = context.Connection.RemoteIpAddress.ToString();
// Increment counter for client
if(_requests.ContainsKey(clientIp))
{
_requests[clientIp]++;
}
else
{
_requests.Add(clientIp, 1);
}
// Check if over limit
if(_requests[clientIp] > PerMinuteLimit)
{
context.Response.StatusCode = 429; // Too many requests
return;
}
await _next.Invoke(context);
// Remove counter after request is complete
_requests.Remove(clientIp);
}
}
public static class RateLimitMiddlewareExtensions
{
public static IApplicationBuilder UseRateLimitMiddleware(
this IApplicationBuilder builder)
{
return builder.UseMiddleware<RateLimitMiddleware>();
}
}
This allows limiting total requests per minute from a client IP. The limit and time window can be configured as per your needs.
3.8. URL Rewrite Middleware
Handle URL rewriting
public class RewriteMiddleware
{
public async Task Invoke(HttpContext context)
{
var requestPath = context.Request.Path.Value;
if(requestPath.StartsWith("/old"))
{
var newPath = requestPath.Replace("/old", "/new");
context.Response.Redirect(newPath);
return;
}
await _next(context);
}
}
public static class RewriteMiddlewareExtensions
{
public static IApplicationBuilder UseRewriteMiddleware(
this IApplicationBuilder builder)
{
return builder.UseMiddleware<RewriteMiddleware>();
}
}
This middleware checks the request path – if it starts with /old
, it replaces that part with /new
and redirects to the new URL.
For example, a request to /old/page
will be redirected to /new/page
.
The old to new URL mapping can be extended as:
var rewrites = new Dictionary<string, string>
{
{"/old/page1", "/new/page1"},
{"/old/page2", "/new/page2"}
};
var newPath = rewrites[requestPath];
This allows you to centrally handle URL rewrites for your entire application in a single place. Useful when reorganizing URLs.
The middleware can be placed early in the pipeline to redirect old URLs before the request goes further.
3.9. Https Redirect Middleware
Redirect all HTTP requests to https to enforce site-wide SSL.
public class HttpsRedirectMiddleware
{
public async Task Invoke(HttpContext context)
{
if (!context.Request.IsHttps)
{
var host = context.Request.Host.ToString();
var redirectUrl = "https://" + host + context.Request.Path;
context.Response.Redirect(redirectUrl);
return;
}
await _next(context);
}
}
public static class HttpsRedirectMiddlewareExtensions
{
public static IApplicationBuilder UseHttpsRedirectMiddleware(
this IApplicationBuilder builder)
{
return builder.UseMiddleware<HttpsRedirectMiddleware>();
}
}
This middleware checks if the request scheme is HTTPS. If not, it reconstructs the URL with an HTTPS scheme and redirects to it.
For example, a request to http://example.com/page
will be redirected to https://example.com/page
This can be used to enforce HTTPS and SSL for your entire application by adding this middleware early in the pipeline.
3.10. Header injection Middleware
A middleware that injects useful headers like X-Request-Id
for tracing requests across microservices.
public class HeaderInjectionMiddleware
{
public async Task Invoke(HttpContext context)
{
// Generate unique request ID
var requestId = Guid.NewGuid().ToString();
// Inject X-Request-ID header
context.Response.Headers.Add("X-Request-ID", requestId);
await _next(context);
}
}
public static class HeaderInjectionMiddlewareExtensions
{
public static IApplicationBuilder UseHeaderInjectionMiddleware(
this IApplicationBuilder builder)
{
return builder.UseMiddleware<HeaderInjectionMiddleware>();
}
}
This middleware generates a unique GUID on each request and inserts it as a custom header X-Request-ID in the response.
This can be useful to:
1) Track and correlate requests across microservices
2) Debug issues by tracing logs with request ID
3) Analyze metrics for requests
This middleware can be configured early in the pipeline before headers are sent. Multiple headers can be injected in a similar way.
4. Conclusion
The middleware is useful and very easy to create, I shared some ideas on how to create and use it in this article, hope can help you solve the problems 🙂