如何在 .Net Core 里创建和使用中间件

如何在 .Net Core 里创建和使用中间件

1. 介绍

ASP.NET Core 中的中间件是指组装到应用程序管道中以处理请求和响应的软件组件。它们在 Startup.cs 文件中配置为请求处理管道的一部分。中间件可以帮助将横切关注点与应用程序逻辑分开,以实现模块化和可维护的代码,因此它非常适合日志记录、身份验证、缓存、压缩、安全性等。

ASP.NET Core 包含许多内置中间件,如静态文件中间件、路由中间件、端点路由中间件等。但在本文中,我们将创建自己的中间件

2. 创建和使用中间件

可以通过以下 2 步进行创建中间件:

1)创建一个构造函数,为下一个中间件注入 RequestDelegate
2)实现 InvokeAsync 方法,在其中传递业务对象,然后你就可以处理你想要的一切了!

例如,我们创建一个请求日志中间件来记录每个请求,那么我们需要创建一个构造函数,如下所示

private readonly RequestDelegate _next;
public RequestLogMiddleware(RequestDelegate next)
{
    _next = next;
}

因为我们需要记录消息,所以需要在 InvokeAsync 中传递 logger 对象,并在全局中定义记录器

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);
}

因此,以下是完整代码:

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);
    }
}

然后我们可以创建一个扩展方法以便更容易地使用它

public static class RequestLogMiddlewareExtensions
{
    public static IApplicationBuilder UseRequestLogMiddleware(
        this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<RequestLogMiddleware>();
    }
}

最后我们可以在 program.cs 中使用此中间件

var app = builder.Build();
app.UseRequestLogMiddleware();

3. 其他有用的中间件

中间件是很容易创建和使用的,因此我觉得最主要的是要知道我们能创建什么样的中间件,或者说有什么相关的想法可以用,所以我将在下面介绍其中一些有用的中间件和使用场景 🙂

3.1. 错误处理中间件

处理通用的错误

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>();
    }
}

此中间件将所有后续处理程序包装在 try-catch 块中。如果在后续中间件中发生任何未处理的异常,它将在这里被捕获。

3.2. 响应压缩中间件

压缩响应以减少带宽使用

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>();
    }
}

该中间件会检查请求标头,如果客户端接受 gzip ,则使用 GZipStream 压缩响应,并用压缩的响应替换未压缩的响应。这可以减少带宽使用量。

3.3. 异步中间件

使用 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>();
    }
}

这个中间件可以执行异步操作,如数据库查询、网络调用、长时间运行的 CPU 任务等,而不会阻塞请求线程。异步工作完成后,将调用下一个中间件。

3.4. 安全中间件

验证用户权限和角色是否正确

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>();
    }
}

此中间件将所有安全和验证逻辑封装到一个全局应用的中间件中。应用代码仅关注业务逻辑。

3.5. 本地化中间件

根据请求属性设置文化、本地化或时区。

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>();
    }
}

该中间件检查请求标头中用户的首选语言和时区。它根据 Accept-Language 标头设置 CurrentCultureCurrentUICulture。它还根据 TimeZone 标头设置 Local TimeZone。这可用于通过查找每个用户的标头来动态本地化响应。该应用将为用户适当地格式化日期、数字和货币。在生成 “可本地化” 内容之前,必须在 program.cs 中尽早配置中间件。

3.6. 会话中间件

操纵会话状态并管理 cookie

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>();
    }
}

会话状态和 Cookie 通常用于需要在多个请求中保留的用户数据、偏好设置、购物车、令牌等。此中间件提供了一个集中位置来管理应用程序中的会话和 Cookie。

3.7. RateLimit 中间件

这个中间件,用于限制客户端在一段时间内的请求数量,以防止 滥用/DDoS 。它会跟踪请求,如果超出限制,则会以 429 Too Many Requests 做出响应。

 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>();
    }
}

这允许限制来自客户端 IP 每分钟的总请求数。可以根据你的需要配置限制和时间。

3.8. URL 重写中间件

处理 URL 重写

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>();
    }
}

该中间件检查请求路径 - 如果以 /old 开头,则用 /new 替换该部分并重定向到新 URL。

例如,对 /old/page 的请求将被重定向到 /new/page

旧到新 URL 映射可以扩展为:

var rewrites = new Dictionary<string, string>
{
  {"/old/page1", "/new/page1"},
  {"/old/page2", "/new/page2"}
};

var newPath = rewrites[requestPath];

这样你就可以在一个地方集中处理整个应用程序的 URL 重写。在重新组织 URL 时很有用。

可以将中间件放置在管道的早期,以便在请求进一步进行之前重定向旧 URL

3.9. HTTPS 重定向中间件

重定向所有 HTTP 请求到 HTTPS 以强制使用 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>();
    }
}

此中间件检查请求协议是否为 HTTPS。如果不是,它会使用 HTTPS 协议重建 URL 并重定向到该协议。

例如,对 http://example.com/page 的请求将被重定向到 https://example.com/page

通过在管道早期添加此中间件,可用于为整个应用程序强制实施 HTTPSSSL

3.10. 标头注入中间件

一个中间件,它注入有用的标头,如 X-Request-Id,用于跨微服务跟踪请求。

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>();
    }
}

此中间件会在每个请求上生成一个唯一的 GUID,并将其作为自定义标头 X-Request-ID 插入到响应中。

这可用于:

1) 跨微服务跟踪和关联请求

2) 通过跟踪带有请求 ID 的日志来调试问题

3) 分析请求的指标

可以在发送标头之前在管道中尽早配置此中间件。可以以类似的方式注入多个标头。

4. 结论

中间件很有用,而且非常容易创建,我在本文中分享了一些关于如何创建和使用它的想法,希望可以帮助你解决问题 🙂

代码部落

免费订阅以得到最新文章发布的通知

请放心,这个绝对不会是垃圾邮件
而且您随时也可以取消的

版权声明:
作者:winson
链接:https://www.coderblog.cc/2024/06/how-to-use-and-create-middleware-in-dot-net-core/
来源:代码部落中文站
文章版权归作者所有,未经允许请勿转载。

THE END
分享
二维码
< <上一篇
下一篇>>
文章目录
关闭
目 录