IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    How to create and use the custom Middleware in .Net Core

    Winson发表于 2023-09-05 07:24:08
    love 0

    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

    Handle the common errors

    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 ErrorHandlingMiddlewareMiddlewareExtensions
    {
        public static IApplicationBuilder UseErrorHandlingMiddlewareMiddleware(
            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 🙂

    Loading

    The post How to create and use the custom Middleware in .Net Core first appeared on Coder Blog.



沪ICP备19023445号-2号
友情链接