一、中间件

一、中间件的概念

ASP.NET Core的处理流程是一个管道,中间件是组装到应用程序管道中用来处理请求和响应的组件。 每个中间件可以:
  • 选择是否将请求传递给管道中的下一个组件。
  • 可以在调用管道中的下一个组件之前和之后执行业务逻辑。
中间件是一个请求委托( public delegate Task RequestDelegate(HttpContext context) )的实例,所以中间件的本质就是一个方法,方法的参数是HttpContext,返回Task。传入的HttpContext参数包含了请求和响应信息,我们可以在中间件中对这些信息就行修改。中间件的管道处理流程如下:

.Net Core--过滤器%26中间件 - 图1

我们知道中间件是配置请求处理管道的组件,那么谁来负责构建管道呢?负责构建管道的角色是ApplicationBuilder。ApplicationBuilder通过Use、Run、Map及MapWhen方法来注册中间件,构建请求管道。我们简单看下这几个方法。

1. Run

新建一个WebAPI项目,修改StartUp中的Configure方法如下,用Run方法注册的中间件可以叫做终端中间件,即该中间件执行完成后不再执行后续的中间件。
  1. public void Configure(IApplicationBuilder app, IHostingEnvironment env)
  2. {
  3. //第一个中间件
  4. app.Run(async (context) =>
  5. {
  6. context.Response.ContentType = "text/plain;charset=utf-8";//防止中文乱码
  7. await context.Response.WriteAsync("第一个中间件输出你好~");
  8. });
  9. //第二个中间件
  10. app.Run(async (context) =>
  11. {
  12. await context.Response.WriteAsync("第二个中间件输出你好~");
  13. });
  14. }

2. Use

Use方法的参数是一个委托实例,委托的第一个参数是HttpContext,这是待处理的请求上下文;第二个参数next是下一个中间件,我们可以通过next.Invoke()调用下一个中间件,并且可以在调用下一个中间件之前/之后对HttpContext做一个逻辑处理。
  1. public void Configure(IApplicationBuilder app, IHostingEnvironment env)
  2. {
  3. //第一个中间件
  4. app.Use(async (context, next) =>
  5. {
  6. context.Response.ContentType = "text/plain;charset=utf-8";//防止中文乱码
  7. await context.Response.WriteAsync($"第一个中间件输出你好~{Environment.NewLine}");
  8. await context.Response.WriteAsync($"下一个中间件执行前执行===>{Environment.NewLine}");
  9. await next.Invoke();
  10. await context.Response.WriteAsync($"下一个中间件执行后执行<==={Environment.NewLine}");
  11. });
  12. //第二个中间件
  13. app.Use(async (context,next) =>
  14. {
  15. await context.Response.WriteAsync($"第二个中间件输出你好~{Environment.NewLine}");
  16. });
  17. }
运行程序如下所示。注意如果我们没有调用next.Invoke()方法,会造成管道短路,后续的所有中间件都不再执行。

3. Map

在业务简单的情况下,使用一个请求处理管道来处理所有的请求就可以了,当业务复杂的时候, 我们可能考虑把不同业务的请求交给不同的管道中处理。 Map** 基于给定请求路径的匹配项来创建请求管道分支。 如果请求路径以给定路径开头,则执行分支**。看一个栗子,需求是/userinfo开头的请求使用用户分支管道来处理,/product开头的请求使用产品分支管道处理,代码如下:
  1. public class Startup
  2. {
  3. public Startup(IConfiguration configuration)
  4. {
  5. Configuration = configuration;
  6. }
  7. public IConfiguration Configuration { get; }
  8. // 依赖注入
  9. public void ConfigureServices(IServiceCollection services)
  10. {
  11. services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
  12. }
  13. /// <summary>
  14. /// 配置用户分支管道,处理以url以/userinfo开头的请求
  15. /// </summary>
  16. /// <param name="app"></param>
  17. private static void UserinfoConfigure(IApplicationBuilder app)
  18. {
  19. app.Use(async (context, next) =>
  20. {
  21. await context.Response.WriteAsync($"处理用户业务,{Environment.NewLine}");
  22. await next.Invoke();
  23. });
  24. app.Run(async (context) => { await context.Response.WriteAsync("用户业务处理完成~"); });
  25. }
  26. /// <summary>
  27. /// 配置产品分支管道,处理以url以/product开头的请求
  28. /// </summary>
  29. /// <param name="app"></param>
  30. private static void ProductConfigure(IApplicationBuilder app)
  31. {
  32. app.Use(async (context, next) =>
  33. {
  34. await context.Response.WriteAsync($"处理产品业务");
  35. await next.Invoke();
  36. });
  37. }
  38. // 配置请求处理管道
  39. public void Configure(IApplicationBuilder app, IHostingEnvironment env)
  40. {
  41. //防止中文乱码
  42. app.Use(async (context,next) =>
  43. {
  44. context.Response.ContentType = "text/plain;charset=utf-8";
  45. await next.Invoke();
  46. });
  47. app.Map("/userinfo", UserinfoConfigure);
  48. app.Map("/product", ProductConfigure);
  49. app.Run(async context =>
  50. {
  51. await context.Response.WriteAsync("主管道处理其他业务");
  52. });
  53. }
  54. }
运行程序执行结果如下:

.Net Core--过滤器%26中间件 - 图2

4. MapWhen

MapWhen和Map的思想比较相似,MapWhen基于自定义条件来创建请求管道分支,并将请求映射到管道的新分支。看一个栗子就明白了,下边栗子的需求是查询参数包含name的请求交给一个分支管道处理,url包含/userinfo的请求交给用户分支来处理,代码如下:
  1. public class Startup
  2. {
  3. public Startup(IConfiguration configuration)
  4. {
  5. Configuration = configuration;
  6. }
  7. public IConfiguration Configuration { get; }
  8. // 依赖注入
  9. public void ConfigureServices(IServiceCollection services)
  10. {
  11. services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
  12. }
  13. /// <summary>
  14. /// 配置分支管道,处理以url中有包含/userinfo的请求
  15. /// </summary>
  16. /// <param name="app"></param>
  17. private static void UserinfoConfigure(IApplicationBuilder app)
  18. {
  19. app.Use(async (context, next) =>
  20. {
  21. await context.Response.WriteAsync($"处理用户业务,{Environment.NewLine}");
  22. await next.Invoke();
  23. });
  24. app.Run(async (context) => { await context.Response.WriteAsync("用户业务处理完成~"); });
  25. }
  26. /// <summary>
  27. /// 配置分支管道,处理以查询参数有name的请求
  28. /// </summary>
  29. /// <param name="app"></param>
  30. private static void HNameConfigure(IApplicationBuilder app)
  31. {
  32. app.Use(async (context, next) =>
  33. {
  34. await context.Response.WriteAsync($"查询参数包含name,值为:{context.Request.Query["name"]}");
  35. await next.Invoke();
  36. });
  37. }
  38. // 配置请求处理管道
  39. public void Configure(IApplicationBuilder app, IHostingEnvironment env)
  40. {
  41. //防止中文乱码
  42. app.Use(async (context,next) =>
  43. {
  44. context.Response.ContentType = "text/plain;charset=utf-8";
  45. await next.Invoke();
  46. });
  47. app.MapWhen(context => context.Request.Query.ContainsKey("name"), HNameConfigure);
  48. app.MapWhen(context => context.Request.Path.Value.ToString().Contains("/userinfo"), UserinfoConfigure);
  49. app.Run(async context =>
  50. {
  51. await context.Response.WriteAsync("主管道处理其他业务");
  52. });
  53. }
  54. }
程序执行结果如下:

.Net Core--过滤器%26中间件 - 图3

二、中间件(Middleware)的作用

我们知道,任何的一个web框架都是把http请求封装成一个管道,每一次的请求都是经过管道的一系列操作,最终到达我们写的代码中。那么中间件就是在应用程序管道中的一个组件,用来拦截请求过程进行一些其他处理和响应。中间件可以有很多个,每一个中间件都可以对管道中的请求进行拦截,它可以决定是否将请求转移给下一个中间件。 asp.net core 提供了IApplicationBuilder接口来让把中间件注册到asp.net的管道请求当中去,中间件是一个典型的AOP应用。 下面是一个微软官方的一个中间件管道请求图:
.Net Core--过滤器%26中间件 - 图4
可以看到,每一个中间件都都可以在请求之前和之后进行操作。请求处理完成之后传递给下一个请求。

三、中间件的运行方式

默认情况下,中间件的执行顺序根据Startup.cs文件中,在public void Configure(IApplicationBuilder app){} 方法中注册的先后顺序执行。
大概有3种方式可以在管道中注册”中间件”
  1. app.Use()IApplicationBuilder接口原生提供,注册等都用它。
  2. app.Run(),是一个扩展方法,它需要一个RequestDelegate委托,里面包含了Http的上下文信息,没有next参数,因为它总是在管道最后一步执行。
  3. app.Map(),也是一个扩展方法,类似于MVC的路由,用途一般是一些特殊请求路径的处理。如:www.example.com/token等。
上面的RunMap内部也是调用的Use,算是对IApplicationBuilder接口扩充,如果你觉得名字都不够准确,那么下面这个扩展方法就是正宗的注册中间件的了,也是功能最强大的。 app.UseMiddleware<>(),没错,就是这个了。 为什么说功能强大呢?是因为它不但提供了注册中间件的功能,还提供了依赖注入(DI)的功能,以后大部分情况就用它了。

四、什么情况我们需要中间件

那么,何时使用中间件呢?我的理解是在我们的应用程序当中和业务关系不大的一些需要在管道中做的事情可以使用,比如身份验证,Session存储,日志记录等。其实我们的 asp.net core项目中本身已经包含了很多个中间件。 举例,我们在新建一个 asp.net core应用程序的时候,默认生成的模板当中
  1. public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
  2. {
  3. app.UseDeveloperExceptionPage();
  4. app.UseStaticFiles();
  5. loggerFactory.AddConsole();
  6. app.UseMvc(routes =>
  7. {
  8. routes.MapRoute(
  9. name: "default",
  10. template: "{controller=Home}/{action=Index}/{id?}");
  11. });
  12. }
懒得去下载源码了,我们使用Reflector去查看源码:
  1. //扩展方法`app.UseDeveloperExceptionPage();`
  2. public static class DeveloperExceptionPageExtensions
  3. {
  4. // Methods
  5. public static IApplicationBuilder UseDeveloperExceptionPage(this IApplicationBuilder app)
  6. {
  7. if (app == null)
  8. {
  9. throw new ArgumentNullException("app");
  10. }
  11. return UseMiddlewareExtensions.UseMiddleware<DeveloperExceptionPageMiddleware>(app, Array.Empty<object>());
  12. }
  13. }
  14. //扩展方法`app.UseStaticFiles();`
  15. public static class StaticFileExtensions
  16. {
  17. // Methods
  18. public static IApplicationBuilder UseStaticFiles(this IApplicationBuilder app)
  19. {
  20. if (app == null)
  21. {
  22. throw new ArgumentNullException("app");
  23. }
  24. return UseMiddlewareExtensions.UseMiddleware<StaticFileMiddleware>(app, Array.Empty<object>());
  25. }
  26. }
可以看到 app.UseDeveloperExceptionPage(),app.UseStaticFiles()等等都是通过中间件实现的。

五、如何自定义自己的中间件

背景:我们项目使用到中间件的情景是,需要和其他部门进行用户(User)信息的共享。 以平台和子系统举例,我们正在开发一个子系统,其中用户信息,登录,注册等功能是放在平台上的,这是一个跨多语言的系统,平台是Java语言开发,用户在访问子系统的一些页面的时候需要验证是否登录,另外一些页面是不需要验证是否登录的,所以需要一个身份验证系统来代替Identity的功能。 幸运的是微软已经给我们提供了一套身份验证的中间件,在Microsoft.AspNetCore.Authentication命名空间下,我们只需要拓展,添加自己的功能就行了 。具体怎么做呢?直接看代码吧。 根据约定俗成,中间件类需要有一个Invoke方法,签名是public async Task Invoke(HttpContext context){},下面是一个中间件的示例类:
  1. public class RequestLoggerMiddleware
  2. {
  3. private readonly RequestDelegate _next;
  4. private readonly ILogger _logger;
  5. public RequestLoggerMiddleware(RequestDelegate next, ILoggerFactory loggerFactory)
  6. {
  7. _next = next;
  8. _logger = loggerFactory.CreateLogger<RequestLoggerMiddleware>();
  9. }
  10. public async Task Invoke(HttpContext context)
  11. {
  12. _logger.LogInformation("Handling request: " + context.Request.Path);
  13. await _next.Invoke(context);
  14. _logger.LogInformation("Finished handling request.");
  15. }
  16. }
了解了上面的约定之后,我们就开始定义我们自己的中间件Class 我们需要一个流程图来理清逻辑思路,以便于写代码的时候思路更加的清晰。
.Net Core--过滤器%26中间件 - 图5
平台有一个要求就是,用户在我们子系统退出之后,要调用平台的一个接口通知他们,他们要做一些后续的业务。 OK,开始撸码。
  • 首先创建一个PlatformAuthoricationMiddleware,它继承于Microsoft.AspNetCore.Authentication下的类AuthenticationMiddleware,由于AuthenticationMiddleware已经实现了Invoke功能,所以我们只需要重写(override)它里面的一些方法就可以了。等等,我们好像还需要一些配置,比如流程图中的ReturnUrl,平台的CookieKey值,平台验证用户合法性的接口地址等参数。
  • 建立一个Options类进行配置的设置,我们取名字为:PlatformAuthenticationOptions,继承AuthenticationOptions,并且实现掉IOptions接口,这样子就能在Startup中直接配置了。
  • 我们只需要重写AuthenticationMiddleware中的CreateHandler方法就行了,在Handler中可以实现掉我们中间件的功能。
  • 然后创建一个处理的Handler类,取名为PlatformAuthenticationHandler,继承于AuthenticationHandler用来处理请求中的调用。
至此,我们的核心需要的类已经建立完了,剩下的就是填充代码。
  1. PlatformAuthenticationHandler中重写HandleAuthenticateAsync()方法 ,
    进行主流程的控制。
  2. PlatformAuthenticationHandler中重写FinishResponseAsync()方法,进行Session的存储操作。
  3. PlatformAuthenticationHandler中重写HandleSignOutAsync()方法,进行登出的控制,因为用户登出之后我们要通知平台做一些其他操作。
  4. PlatformAuthenticationHandler中重写HandleUnauthorizedAsync()方法,进行未认证操作。
最后,我们需要一个扩展类来把我们的中间件以扩展方法注册到管道当中去 。
  1. public static class MiddlewareExtensions
  2. {
  3. public static IApplicationBuilder UsePlatformAuthentication(this IApplicationBuilder app) {
  4. if (app == null) {
  5. throw new ArgumentNullException(nameof(app));
  6. }
  7. return app.UseMiddleware<PlatformAuthenticationMiddleware>();
  8. }
  9. public static IApplicationBuilder UsePlatformAuthentication(this IApplicationBuilder app, CookieAuthenticationOptions options) {
  10. if (app == null) {
  11. throw new ArgumentNullException(nameof(app));
  12. }
  13. if (options == null) {
  14. throw new ArgumentNullException(nameof(options));
  15. }
  16. return app.UseMiddleware<PlatformAuthenticationMiddleware>(Options.Create(options));
  17. }
  18. }
Startup中就是app.UsePlatformAuthentication()
  1. public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) {
  2. loggerFactory.AddConsole(Configuration.GetSection("Logging"));
  3. //注册PlatformAuthentication中间件
  4. app.UsePlatformAuthentication(new PlatformAuthenticationOptions() {
  5. UserSessionStore = new UserSessionStore(),
  6. });
  7. app.UseMvc();
  8. }
示例源码:https://github.com/yuleyule66/PlatformAuthMiddleware ### 六、中间件的使用 #### 1. 使用中间件记录错误日志 这里使用的日志组件为nlog,首先创建一个WebAPI项目,添加一个自定义日志处理中间件CostomErrorMiddleware,当程序出错时会记录日志,同时开发环境下会把异常的详细信息打印在页面上,非开发环境隐藏详细信息,代码如下:
  1. /// <summary>
  2. /// 自定义的错误处理类
  3. /// </summary>
  4. public class CostomErrorMiddleware
  5. {
  6. private readonly RequestDelegate next;
  7. private readonly ILogger logger;
  8. private IHostingEnvironment environment;
  9. /// <summary>
  10. /// DI,注入logger和环境变量
  11. /// </summary>
  12. /// <param name="next"></param>
  13. /// <param name="logger"></param>
  14. /// <param name="environment"></param>
  15. public CostomErrorMiddleware(RequestDelegate next, ILogger<CostomErrorMiddleware> logger, IHostingEnvironment environment)
  16. {
  17. this.next = next;
  18. this.logger = logger;
  19. this.environment = environment;
  20. }
  21. /// <summary>
  22. /// 实现Invoke方法
  23. /// </summary>
  24. /// <param name="context"></param>
  25. /// <returns></returns>
  26. public async Task Invoke(HttpContext context)
  27. {
  28. try
  29. {
  30. await next.Invoke(context);
  31. }
  32. catch (Exception ex)
  33. {
  34. await HandleError(context, ex);
  35. }
  36. }
  37. /// <summary>
  38. /// 错误信息处理方法
  39. /// </summary>
  40. /// <param name="context"></param>
  41. /// <param name="ex"></param>
  42. /// <returns></returns>
  43. private async Task HandleError(HttpContext context, Exception ex)
  44. {
  45. context.Response.StatusCode = 500;
  46. context.Response.ContentType = "text/json;charset=utf-8;";
  47. string errorMsg = $"错误消息:{ex.Message}{Environment.NewLine}错误追踪:{ex.StackTrace}";
  48. //无论是否为开发环境都记录错误日志
  49. logger.LogError(errorMsg);
  50. //浏览器在开发环境显示详细错误信息,其他环境隐藏错误信息
  51. if (environment.IsDevelopment())
  52. {
  53. await context.Response.WriteAsync(errorMsg);
  54. }
  55. else
  56. {
  57. await context.Response.WriteAsync("抱歉,服务端出错了");
  58. }
  59. }
  60. }
修改StartUp类中的Configure方法如下,注入nlog 需要先安装 NLog.Web.AspNetCore ,使用app.UseMiddleware()注册我们自定义的中间件,代码如下:
  1. /// 配置请求管道
  2. public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory factory)
  3. {
  4. //添加nlog
  5. factory.AddNLog();
  6. env.ConfigureNLog("nlog.config");
  7. //泛型方法添加中间件
  8. app.UseMiddleware<CostomErrorMiddleware>();
  9. app.UseMvc();
  10. }
nlog.config:
  1. <?xml version="1.0" encoding="utf-8" ?>
  2. <nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4. autoReload="true"
  5. internalLogLevel="Info"
  6. internalLogFile="D:\LogDemoOfWebapi\internal-nlog.txt">
  7. <!-- enable asp.net core layout renderers -->
  8. <extensions>
  9. <add assembly="NLog.Web.AspNetCore"/>
  10. </extensions>
  11. <targets>
  12. <target xsi:type="File" name="errorLog" fileName="D:/logs/AT___${shortdate}.log"
  13. layout="----------------日志记录开始----------------${newline}【日志时间】:${longdate} ${newline}【日志级别】:${level:uppercase=true}${newline}【异常相关信息】${newline}${message}${newline}${newline}${newline}" />
  14. </targets>
  15. <rules>
  16. <logger name="*" minlevel="Error" writeTo="errorLog" />
  17. </rules>
  18. </nlog>
到这里异常处理中间件就注册完成了,修改ValueController自己制造一个异常来测试一下,代码如下:
  1. [Route("api/[controller]")]
  2. [ApiController]
  3. public class ValuesController : ControllerBase
  4. {
  5. private ILogger<ValuesController> _logger;
  6. public ValuesController(ILogger<ValuesController> logger)
  7. {
  8. _logger = logger;
  9. }
  10. // GET api/values
  11. [HttpGet]
  12. public ActionResult<IEnumerable<string>> Get()
  13. {
  14. return new string[] { "value1", "value2" };
  15. }
  16. // GET api/values/5
  17. [HttpGet("{id}")]
  18. public ActionResult<string> Get(int id)
  19. {
  20. throw new Exception("有一个错误发生了..");
  21. return "value";
  22. }
  23. }
运行程序,在开发环境下访问/Values/1,显示结果如下,同时这些错误信息也会通过nlog写入到错误日志中:

.Net Core--过滤器%26中间件 - 图6

非开发环境下,访问/values/1,显示如下: 如果我们想使.Net Core--过滤器%26中间件 - 图7用类似app.UseMvc()这样的形式来使用我们自定义的中间件的话,就需要给ApplicationBuilder添加一个扩展方法,首先添加一个静态类CostomMiddleware,代码如下:
  1. /// <summary>
  2. /// 扩展方法
  3. /// </summary>
  4. public static class CostomMiddleware
  5. {
  6. public static IApplicationBuilder UseCostomError(this IApplicationBuilder app)
  7. {
  8. return app.UseMiddleware<CostomErrorMiddleware>();
  9. }
  10. }
然后修改Configure方法即可:
  1. public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory factory)
  2. {
  3. //添加nlog
  4. factory.AddNLog();
  5. env.ConfigureNLog("nlog.config");
  6. //使用扩展方法
  7. app.UseCostomError();
  8. app.UseMvc();
  9. }

2. 通过中间件实现全局异常处理

  1. 建立一个自定义的全局异常处理中间件
  1. public class ExceptionMiddleware
  2. {
  3. private readonly RequestDelegate next;
  4. private readonly ILogger logger;
  5. private IHostingEnvironment environment;
  6. public ExceptionMiddleware(RequestDelegate next, ILogger<ExceptionMiddleware> logger, IHostingEnvironment environment)
  7. {
  8. this.next = next;
  9. this.logger = logger;
  10. this.environment = environment;
  11. }
  12. public async Task Invoke(HttpContext context)
  13. {
  14. try
  15. {
  16. await next.Invoke(context);
  17. var features = context.Features;
  18. }
  19. catch (Exception e)
  20. {
  21. await HandleException(context, e);
  22. }
  23. }
  24. private async Task HandleException(HttpContext context, Exception e)
  25. {
  26. context.Response.StatusCode = 500;
  27. context.Response.ContentType = "text/json;charset=utf-8;";
  28. string error = "";
  29. void ReadException(Exception ex)
  30. {
  31. error += string.Format("{0} | {1} | {2}", ex.Message, ex.StackTrace, ex.InnerException);
  32. if (ex.InnerException != null)
  33. {
  34. ReadException(ex.InnerException);
  35. }
  36. }
  37. ReadException(e);
  38. if (environment.IsDevelopment())
  39. {
  40. var json = new { message = e.Message, detail = error };
  41. error = JsonConvert.SerializeObject(json);
  42. }
  43. else
  44. error = "抱歉,出错了";
  45. await context.Response.WriteAsync(error);
  46. }
  47. }
  1. 在管道中加入自定义中间件

app.UseMiddleware();

  1. 在管道中通过try catch进行异常捕获 这个中间件后面的所有代码都在 try catch里面 只要出发了异常 就会给当前中间件捕获
注意 在某个中间件中发生了异常 但是他抛出的时候 在当前中间件就处理掉了 没有继续往上抛出 这时候就捕获不到

3. 中间件接口校验

  1. //使用方式不一样了 添加的位置要改变了
  2. /// <summary>
  3. /// 中间件启动类
  4. /// </summary>
  5. /// <param name="app"></param>
  6. /// <param name="env"></param>
  7. // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
  8. public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
  9. {
  10. //拦截器中间件
  11. app.UseMiddleware<ValidateMiddleware>();
  12. }
  1. /// <summary>
  2. /// 中间件校验信息 类似Framework中的HttpModel 管道模型
  3. /// </summary>
  4. public class ValidateMiddleware
  5. {
  6. private readonly RequestDelegate _nextDelegate;
  7. /// <summary>
  8. ///
  9. /// </summary>
  10. /// <param name="nextDelegate"></param>
  11. public ValidateMiddleware(RequestDelegate nextDelegate)
  12. {
  13. _nextDelegate = nextDelegate;
  14. }
  15. /// <summary>
  16. /// 处理对应的参数信息 进行拦截校验信息
  17. /// </summary>
  18. /// <param name="httpContext">拦截HTTP请求处理</param>
  19. /// <returns></returns>
  20. public async Task Invoke(HttpContext httpContext)
  21. {
  22. //这个地方可以获取到HttpContext 这个时候 你想干啥都可以 做自己想要实现的功能就好了
  23. //返回执行下一个中间件的处理 不能去掉 去掉参数就会丢失
  24. await _nextDelegate.Invoke(httpContext);
  25. }
  26. }

二、过滤器

  1. 一共有五类过滤器IAsyncAuthorizationFilter IAsyncResourceFilter IAsyncActonFilter IAsyncExceptionFilter IAsyncResultFilter 去掉Async就是同步的
  2. 注册过滤器 全局注册和Attribute注册 用在特定的Action上

一、中间件(Middleware)和过滤器(Filter)的区别

熟悉MVC框架的同学应该知道,MVC也提供了5大过滤器供我们用来处理请求前后需要执行的代码。分别是<font style="color:rgb(199, 37, 78);background-color:rgb(249, 242, 244);">AuthenticationFilter,AuthorizationFilter,ActionFilter,ExceptionFilter,ResultFilter</font> 根据描述,可以看出中间件和过滤器的功能类似,那么他们有什么区别?为什么又要搞一个中间件呢?
其实,过滤器和中间件他们的关注点是不一样的,也就是说职责不一样,干的事情就不一样。 同作为两个AOP利器,过滤器更贴合业务,它关注于应用程序本身,比如你看ActionFilterResultFilter,它都直接和你的ActionActionResult交互了,是不是离你很近的感觉,那我有一些比如对我的输出结果进行格式化啦,对我的请求的ViewModel进行数据验证啦,肯定就是用Filter无疑了。它是MVC的一部分,它可以拦截到你Action上下文的一些信息,而中间件是没有这个能力的。
过滤器和中间件一样,都可以用来拦截用户请求和请求数据的。控制器目前用到的过滤器种类有
.Net Core--过滤器%26中间件 - 图8 它们的主要用途有错误处理,缓存,配置,授权,日志记录,主要的好处就是避免重复代码。 一个请求经过服务器管道处理的流程
.Net Core--过滤器%26中间件 - 图9
根据上图所示,过滤器的事件是在各种中间件运行过后,到Action截面处才执行。它先执行全局过滤器的,然后才是类和方法过滤器
过滤器在处理上述提到的重复性操作的需求有优势,它也有局限性。对于不属于ASP.NET范畴的静态页面,就不能用过滤器,而是用中间件来处理,因为中间件一旦被注册,它将拦截一切向服务器发起的请求,解析HttpContext可以实现一些过滤器难以实现的业务逻辑 把ASP.NET Core处理请求的方式看做是一个管道,中间件是组装到应用程序管道中用来处理请求和响应的组件。 每个中间件可以:
  • 选择是否将请求传递给管道中的下一个组件。
  • 可以在调用管道中的下一个组件之前和之后执行业务逻辑。

它们之间的主要区别在于它们的范围。过滤器是MVC的一部分,因此它们的范围完全限于MVC中间件。中间件只能访问HttpContext和之前的中间件添加的内容。相反,过滤器可以访问更广泛的MVC上下文,因此可以访问路由数据和模型绑定信息。

二、过滤器的使用

1. 使用过滤器记录错误日志

过滤器大家应该都很熟悉,在ASP.NET Core中过滤器的使用没有太大的变化,这里也实现一个使用过滤器记录错误日志的栗子,直接看代码吧,首先创建一个过滤器,代码如下:
  1. /// <summary>
  2. /// 自定义的错误处理过滤器
  3. /// </summary>
  4. public class CustomErrorFilter :Attribute, IExceptionFilter
  5. {
  6. private readonly ILogger _logger;
  7. private IHostingEnvironment _environment;
  8. public CustomErrorFilter(ILogger<CustomErrorFilter> logger,IHostingEnvironment environment)
  9. {
  10. _logger = logger;
  11. _environment = environment;
  12. }
  13. public void OnException(ExceptionContext context)
  14. {
  15. Exception ex = context.Exception;
  16. string errorMsg = $"错误消息:{ex.Message}{Environment.NewLine}错误追踪:{ex.StackTrace}";
  17. ContentResult result = new ContentResult
  18. {
  19. ContentType = "text/json;charset=utf-8;",
  20. StatusCode = 500
  21. };
  22. //无论是否为开发环境都记录错误日志
  23. _logger.LogError(errorMsg);
  24. //浏览器在开发环境显示详细错误信息,其他环境隐藏错误信息
  25. if (_environment.IsDevelopment())
  26. {
  27. result.Content = $"错误消息:{ex.Message}{Environment.NewLine}错误追踪:{ex.StackTrace}";
  28. }
  29. else
  30. {
  31. result.Content = "抱歉,服务端出错了";
  32. }
  33. context.Result = result;
  34. context.ExceptionHandled = true;
  35. }
  36. }
修改StartUp类,注入nlog,配置全局过滤器,代码如下,其中nlog.config和中间件栗子中一样:
  1. public class Startup
  2. {
  3. public Startup(IConfiguration configuration)
  4. {
  5. Configuration = configuration;
  6. }
  7. public IConfiguration Configuration { get; }
  8. // 依赖注入
  9. public void ConfigureServices(IServiceCollection services)
  10. {
  11. services.AddMvc(
  12. configure =>
  13. {
  14. configure.Filters.Add<CustomErrorFilter>();//全局过滤器,不用添加特性头
  15. }//全局过滤器,不用添加特性头
  16. ).SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
  17. //services.AddScoped<CustomErrorFilter>();//局部过滤器,需要在Controller/Action添加特性头 [ServiceFilter(typeof(CustomErrorFilter))]
  18. }
  19. // 配置管道
  20. public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory factory)
  21. {
  22. factory.AddNLog();
  23. env.ConfigureNLog("nlog.config");
  24. app.UseMvc();
  25. }
  26. }
然后修改ValuesController,设置错误和上边中间件的栗子一样,运行代码访问/values/1时,在开发环境中显示如下,同时错误信息也会写入错误日志中:

.Net Core--过滤器%26中间件 - 图10

在生产环境中访问/values/1的话,错误详细也会写入错误日志中,浏览器显示如下:

.Net Core--过滤器%26中间件 - 图11

本文介绍了中间件的基本使用,同时使用中间件和过滤器两种方式实现了异常日志的记录,如果文中有错误的地方希望大家可以指出,我会及时改正。

2. 通过过滤器实现全局异常处理

  1. 建立自己的一个过滤器
  1. public class CustomerExceptionFilter : Attribute, IExceptionFilter
  2. {
  3. private readonly ILogger logger = null;
  4. private readonly IHostingEnvironment environment = null;
  5. public CustomerExceptionFilter(ILogger<CustomerExceptionFilter> logger, IHostingEnvironment environment)
  6. {
  7. this.logger = logger;
  8. this.environment = environment;
  9. }
  10. public void OnException(ExceptionContext context)
  11. {
  12. Exception exception = context.Exception;
  13. string error = string.Empty;
  14. void ReadException(Exception ex)
  15. {
  16. error += string.Format("{0} | {1} | {2}", ex.Message, ex.StackTrace, ex.InnerException);
  17. if (ex.InnerException != null)
  18. {
  19. ReadException(ex.InnerException);
  20. }
  21. }
  22. ReadException(context.Exception);
  23. logger.LogError(error);
  24. ContentResult result = new ContentResult
  25. {
  26. StatusCode = 500,
  27. ContentType = "text/json;charset=utf-8;"
  28. };
  29. if (environment.IsDevelopment())
  30. {
  31. var json = new { message = exception.Message, detail = error };
  32. result.Content = JsonConvert.SerializeObject(json);
  33. }
  34. else
  35. {
  36. result.Content = "抱歉,出错了";
  37. }
  38. context.Result = result;
  39. context.ExceptionHandled = true;
  40. }
  41. }
  1. 添加Nugut包 NLog.Extensions.Logging NLog.Web.AspNetCore ,并在 Startup.cs 文件的 Configure 方法中添加扩展
  1. public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory factory)
  2. {
  3. // 将 NLog
  4. factory.AddConsole(Configuration.GetSection("Logging"))
  5. .AddNLog()
  6. .AddDebug();
  7. var nlogFile = System.IO.Path.Combine(env.ContentRootPath, "nlog.config");
  8. env.ConfigureNLog(nlogFile);
  9. if (env.IsDevelopment())
  10. {
  11. app.UseDeveloperExceptionPage();
  12. }
  13. app.UseMvc();
  14. }
  1. 日志配置文件信息
  1. <?xml version="1.0" encoding="utf-8" ?>
  2. <nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" autoReload="true" internalLogLevel="info">
  3. <!-- Load the ASP.NET Core plugin -->
  4. <extensions>
  5. <add assembly="NLog.Web.AspNetCore"/>
  6. </extensions>
  7. <!-- Layout: https://github.com/NLog/NLog/wiki/Layout%20Renderers -->
  8. <targets>
  9. <target xsi:type="File" name="errorfile" fileName="/data/logs/logfilter/error-${shortdate}.log" layout="${longdate}|${logger}|${uppercase:${level}}| ${message} ${exception}|${aspnet-Request-Url}" />
  10. <target xsi:type="Null" name="blackhole" />
  11. </targets>
  12. <rules>
  13. <logger name="Microsoft.*" minlevel="Error" writeTo="blackhole" final="true" />
  14. <logger name="*" minlevel="Error" writeTo="errorfile" />
  15. </rules>
  16. </nlog>
  1. 把这个过滤器注入到容器中
  1. services.AddMvc(
  2. options =>
  3. {
  4. options.Filters.Add(typeof(CustomerExceptionFilter));
  5. })
  6. .SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

只要请求进入到了MVC中间件中之后抛的异常 都会进到自定义的Filter中。

3. ActionFilter

  1. public void ConfigureServices(IServiceCollection services)
  2. {
  3. //添加对应的拦截器信息
  4. services.AddMvc(options =>
  5. {
  6. options.Filters.Add<ActionFilter>();
  7. });
  8. //.....
  9. }
  1. /// <summary>
  2. /// MVC中的Action过滤器 这个类的位置可以按照你的规则放
  3. /// 这个地方进行数据格式的校验 一般检查参数是否规范
  4. /// </summary>
  5. public class ActionFilter : IActionFilter
  6. {
  7. /// <summary>
  8. /// 验证对应的信息
  9. /// </summary>
  10. /// <param name="context"></param>
  11. public void OnActionExecuting(ActionExecutingContext context)
  12. {
  13. //校验接口参数是否有传递
  14. if (!context.ModelState.IsValid)
  15. {
  16. ActionResult result = new ActionResult() { code = 0, msg="Action验证失败" };
  17. foreach (var item in context.ModelState.Values)
  18. {
  19. foreach (var error in item.Errors)
  20. {
  21. result.msg += error.ErrorMessage + "|";
  22. }
  23. }
  24. context.Result = new JsonResult(result);
  25. }
  26. }
  27. public void OnActionExecuted(ActionExecutedContext context)
  28. {
  29. }
  30. }

4. ApiAuthorizeFilter

  1. //使用方式类似anction拦截器
  2. public void ConfigureServices(IServiceCollection services)
  3. {
  4. //添加对应的拦截器信息
  5. services.AddMvc(options =>
  6. {
  7. options.Filters.Add<ApiAuthorizeFilter>();
  8. });
  9. //.....
  10. }
  1. /// <summary>
  2. /// Api接口拦截器
  3. /// </summary>
  4. public class CUMSApiAuthorizeFilter : IAuthorizationFilter
  5. {
  6. public CUMSApiAuthorizeFilter()
  7. {
  8. }
  9. /// <summary>
  10. /// 只判断参数是否正确,不判断权限
  11. /// </summary>
  12. /// <param name="context"></param>
  13. public void OnAuthorization(AuthorizationFilterContext context)
  14. {
  15. #region 校验参数是否正确
  16. ApiResult result = new ApiResult() { code = 0, msg = "Api验证失败" };
  17. #region API接口校验 校验信息 校验token,或者用户id
  18. //根据对应的信息进行一个判断处理 逻辑暂时不写了
  19. #endregion
  20. return;
  21. #endregion
  22. }
  23. }

三、参考文章

【1】https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/logging/?view=aspnetcore-3.0

【2】https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/middleware/?view=aspnetcore-3.0

【3】https://www.cnblogs.com/wyy1234/p/11373999.html