-
ASP.NET Core MVC过滤器初探
本章将和大家分享ASP.NET Core MVC中的过滤器。下面我们将结合具体的例子来分享请求的整个生命周期中各种各样的AOP扩展。
首先我们先来看下什么是过滤器?
.NET中的过滤器(Filter)是 AOP(面向切面编程) 思想的一种实现,供我们在执行管道的特定阶段执行代码,通过使用过滤器可以实现短路请求、缓存请求结果、日志统一记录、参数合法性验证、异常统一处理、返回值格式化等等,同时使业务代码更加简洁单纯,避免很多重复代码。
新版管道处理模型如下所示:
中间件和过滤器可以实现各种各样的AOP扩展 ,也正因为如此,所以我们才能做到 “pay for what you use” ,在代码开发过程中我们只需要关注核心的业务逻辑。
从新版管道处理模型中可以看出我们的请求首先先到达中间件,然后才会进入MVC流程,下面我们通过代码来看下:
自定义防盗链中间件:
using Microsoft.AspNetCore.Http; using System; using System.IO; using System.Threading.Tasks; namespace MyMiddlewareAndFilter.Middlewares { /// <summary> /// 防盗链中间件(自定义) /// </summary> public class RefuseStealingMiddleware { private readonly RequestDelegate _next; public RefuseStealingMiddleware(RequestDelegate next) { _next = next; } public async Task Invoke(HttpContext context) { string url = context.Request.Path.Value; if (!url.Contains(".jpg")) { await Task.Run(() => { Console.WriteLine(""); Console.WriteLine("===================================Middleware==================================="); Console.WriteLine($"This is {typeof(RefuseStealingMiddleware)} begin"); }); await _next.Invoke(context);//走正常流程 await Task.Run(() => { Console.WriteLine($"This is {typeof(RefuseStealingMiddleware)} end"); Console.WriteLine("===================================Middleware==================================="); }); return; } string urlReferrer = context.Request.Headers["Referer"]; if (string.IsNullOrWhiteSpace(urlReferrer))//直接访问 { await this.SetForbiddenImage(context);//返回404图片 } else if (!urlReferrer.Contains("localhost"))//非当前域名 { await this.SetForbiddenImage(context);//返回404图片 } else { await _next.Invoke(context);//走正常流程 } } /// <summary> /// 设置拒绝图片 /// </summary> /// <param name="context">请求上下文</param> /// <returns></returns> private async Task SetForbiddenImage(HttpContext context) { string defaultImagePath = "wwwroot/image/forbidden.jpg"; string path = Path.Combine(Directory.GetCurrentDirectory(), defaultImagePath); FileStream fs = File.OpenRead(path); byte[] bytes = new byte[fs.Length]; await fs.ReadAsync(bytes, 0, bytes.Length); await context.Response.Body.WriteAsync(bytes, 0, bytes.Length); } } }
修改Startup.cs文件如下:
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.AspNetCore.Http; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using MyMiddlewareAndFilter.Middlewares; using MyMiddlewareAndFilter.Filter; namespace MyMiddlewareAndFilter { public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { services.AddControllersWithViews(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler("/Home/Error"); } app.UseMiddleware<RefuseStealingMiddleware>(); //防盗链中间件 app.Use(next => //中间件1 { Console.WriteLine("middleware 1"); return async c => { await Task.Run(() => Console.WriteLine($"This is middleware 1 Start")); await next.Invoke(c); await Task.Run(() => Console.WriteLine($"This is middleware 1 End")); }; }); app.Use(next => //中间件2 { Console.WriteLine("middleware 2"); return async c => { await Task.Run(() => Console.WriteLine($"This is middleware 2 Start")); await next.Invoke(c); await Task.Run(() => Console.WriteLine($"This is middleware 2 End")); }; }); app.Use(next => //中间件3 { Console.WriteLine("middleware 3"); return async c => { await Task.Run(() => Console.WriteLine($"This is middleware 3 Start")); await next.Invoke(c); await Task.Run(() => Console.WriteLine($"This is middleware 3 End")); }; }); app.UseStaticFiles(); app.UseRouting(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); }); } } }
接下来我们来看下Home控制器:
using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using System; using MyMiddlewareAndFilter.Filter; namespace MyMiddlewareAndFilter.Controllers { public class HomeController : Controller { private readonly ILogger<HomeController> _logger; public HomeController(ILogger<HomeController> logger) { _logger = logger; } public IActionResult Index() { Console.WriteLine($"This is {typeof(HomeController)} Index"); return View(); } } }
最后我们使用命令行方式启动应用程序并访问“/Home/Index”,控制台输出结果如下所示:
从运行结果可以看出我们的请求首先是先到达我们的中间件的,请求经过中间件之后才会进入MVC流程:
过滤器作用域:
1、全局有效(整个MVC应用程序下的每一个Action);
2、仅对某些Controller有效(控制器内所有的Action);
3、仅对某些Action有效;
过滤器的工作原理:
过滤器一般在Asp.Net Core MVC管道内运行,一般在操作执行之前(befor)或者执行之后(after)执行,以供开发者可以选择在不同的执行阶段介入处理。
过滤器类型:
授权过滤器 AuthorizeAttribute
资源过滤器 IResourceFilter
异常过滤器 IExceptionFilter
操作过滤器 ActionFilterAttribute
结果过滤器 IResultFilter
不同过滤器的执行顺序:
1、授权过滤器
授权过滤器在过滤器管道中第一个被执行,通常用于验证请求的合法性(通过实现接口 IAuthorizationFilter or IAsyncAuthorizationFilter)。
在请求到达的时候最先执行,优先级最高,主要作用是提供用户请求权限过滤,对不满足权限的用户,可以在过滤器内执行拒绝操作,俗称“管道短路”。
注意:该过滤器只有执行之前(befor),没有执行之后(after)的方法。
通常情况下,不需要自行编写过滤器,因为该过滤器在 Asp.Net Core 内部已经有了默认实现,我们需要做的就是配置授权策略或者实现自己的授权策略,然后由系统内置的授权过滤器调用授权策略即可。
PS:必须将该过滤器内部可能出现的异常全部处理,因为从过滤器的执行顺序中可以发现授权过滤器是在异常过滤器之前执行的,故异常过滤器是没有办法捕获到授权过滤器内部发生的异常,一旦授权过滤器内部发生异常,该异常将直接输出到结果中。
using Microsoft.AspNetCore.Mvc.Filters; using System; namespace MyMiddlewareAndFilter.Filter { /// <summary> /// Authorization授权过滤器 /// </summary> public class CustomerAuthorizationFilterAttribute : Attribute, IAuthorizationFilter { public void OnAuthorization(AuthorizationFilterContext context) { try { Console.WriteLine($"This is {typeof(CustomerAuthorizationFilterAttribute)} OnAuthorization"); } catch (Exception ex) { Console.WriteLine($"{typeof(CustomerAuthorizationFilterAttribute)} OnAuthorization发生异常:{ex}"); //异常处理 } } } }
2、资源过滤器
资源过滤器在过滤器管道中第二个被执行,通常用于请求结果的缓存和短路过滤器管道(通过实现接口 IResourceFilter or IAsyncResourceFilter)。
资源过滤器的应用场景:在性能方面,资源过滤器在实现缓存或短路过滤器管道尤其有用。
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using System; namespace MyMiddlewareAndFilter.Filter { /// <summary> /// Resource资源过滤器 /// </summary> public class CustomerResourceFilterAttribute : Attribute, IResourceFilter, IFilterMetadata, IOrderedFilter { public int Order { get; set; } public void OnResourceExecuted(ResourceExecutedContext context) { Console.WriteLine($"This is {typeof(CustomerResourceFilterAttribute)} OnResourceExecuted"); if (context.HttpContext.Request.Query["type"].Equals("6")) { ////这里无效,已经来不及了 //context.Result = new JsonResult(new //{ // Remark = $"This is {typeof(CustomerResourceFilterAttribute)} OnResourceExecuted", // Type = context.HttpContext.Request.Query["type"] //}); } Console.WriteLine("****************************************************"); Console.WriteLine(""); } public void OnResourceExecuting(ResourceExecutingContext context) { Console.WriteLine(""); Console.WriteLine("****************************************************"); Console.WriteLine($"This is {typeof(CustomerResourceFilterAttribute)} OnResourceExecuting"); if (context.HttpContext.Request.Query["type"].Equals("5")) { context.Result = new JsonResult(new { Remark = $"This is {typeof(CustomerResourceFilterAttribute)} OnResourceExecuting", Type = context.HttpContext.Request.Query["type"] }); //连控制器都没进去 } } } }
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using System; using System.Collections.Generic; namespace MyMiddlewareAndFilter.Filter { /// <summary> /// 资源过滤器(用于页面缓存) /// </summary> public class CustomerCacheResourceFilterAttribute : Attribute, IResourceFilter, IFilterMetadata, IOrderedFilter { public int Order { get; set; } private static Dictionary<string, IActionResult> _customerCacheResourceFilterAttributeDictionary = new Dictionary<string, IActionResult>(); public void OnResourceExecuting(ResourceExecutingContext context) { Console.WriteLine($"This is {typeof(CustomerCacheResourceFilterAttribute)} OnResourceExecuting"); string key = context.HttpContext.Request.Path; if (_customerCacheResourceFilterAttributeDictionary.ContainsKey(key)) { context.Result = _customerCacheResourceFilterAttributeDictionary[key]; //短路器 } } public void OnResourceExecuted(ResourceExecutedContext context) { Console.WriteLine($"This is {typeof(CustomerCacheResourceFilterAttribute)} OnResourceExecuted"); string key = context.HttpContext.Request.Path; _customerCacheResourceFilterAttributeDictionary.Add(key, context.Result); } } }
3、Action过滤器
Action过滤器要么实现IActionFilter接口,要么实现IAsyncActionFilter接口,它们可以在Action方法执行的前后被执行。另外,Action过滤器可以查看并直接修改Action方法的结果(即,可以对输出结果进行修改)。
OnActionExecuting方法在action方法执行之前运行,因此它可以通过改变ActionExecutingContext.ActionArguments来控制action的输入(即获取参数),或是通过ActionExecutingContext.Controller控制控制器(Controller)。
OnActionExecuting方法可以通过设置ActionExecutingContext.Result来短路action方法的操作及其后续的过滤器。OnActionExecuting方法通过抛出异常也可以阻止action方法和后续过滤器的处理,但会当做失败(而不是成功)的结果来处理。
OnActionExecuted方法在action方法执行之后才执行,并且可以通过ActionExecutedContext.Result属性查看或控制action的结果。如果action在执行时被其它过滤器短路,则ActionExecutedContext.Canceled将会被置为true。如果action或后续的action过滤器抛出异常,则ActionExecutedContext.Exception会被设置为一个非空值。有效「处理」完异常后把ActionExecutedContext.Exception设置为null(即异常处理),那么ActionExectedContext.Result会像从action方法正常返回值那样被处理。
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using System; namespace MyMiddlewareAndFilter.Filter { /// <summary> /// Action操作过滤器 /// </summary> public class CustomerActionFilterAttribute : Attribute, IActionFilter, IFilterMetadata, IOrderedFilter { public int Order { get; set; } public void OnActionExecuted(ActionExecutedContext context) { Console.WriteLine($"This is {typeof(CustomerActionFilterAttribute)} OnActionExecuted"); if (context.HttpContext.Request.Query["type"].Equals("8")) { context.Result = new JsonResult(new { Remark = $"This is {typeof(CustomerActionFilterAttribute)} OnActionExecuted", Type = context.HttpContext.Request.Query["type"] }); } } public void OnActionExecuting(ActionExecutingContext context) { Console.WriteLine($"This is {typeof(CustomerActionFilterAttribute)} OnActionExecuting"); if (context.HttpContext.Request.Query["type"].Equals("7")) { context.Result = new JsonResult(new { Remark = $"This is {typeof(CustomerActionFilterAttribute)} OnActionExecuting", Type = context.HttpContext.Request.Query["type"] }); } } } }
4、异常过滤器
说明:用于实现常见的错误处理策略,没有之前和之后事件,处理Razor页面或控制器创建、模型绑定、操作过滤器或操作方法中发生的未经处理的异常。
PS:无法捕获授权过滤器、资源过滤器、结果过滤器或MVC结果执行中发生的异常。(这点从过滤器的执行顺序就可以看出)
实现:继承Attribute类,实现IExceptionFilter接口,重写OnException方法。 或者直接继承ExceptionFilterAttribute类,观察源码可知,该类继承了Attribute类,而且还实现IExceptionFilter接口。(异步的话实现IAsyncExceptionFilter接口,重写OnExceptionAsync方法)
用途:全局捕获异常,进行相关处理。
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ViewFeatures; using System; namespace MyMiddlewareAndFilter.Filter { /// <summary> /// Exception异常过滤器 /// </summary> public class CustomerExceptionFilterAttribute : Attribute, IExceptionFilter { private readonly IModelMetadataProvider _modelMetadataProvider; public CustomerExceptionFilterAttribute(IModelMetadataProvider modelMetadataProvider) { _modelMetadataProvider = modelMetadataProvider; } public void OnException(ExceptionContext context) { //获取异常信息 string ex = context.Exception.ToString(); //这边可以处理异常信息的记录 Console.WriteLine($"This is {typeof(CustomerExceptionFilterAttribute)} OnException"); //跳转到自定义异常页面 var result = new ViewResult { ViewName = "Error" }; result.ViewData = new ViewDataDictionary(_modelMetadataProvider, context.ModelState); result.ViewData.Add("Exception", context.Exception); //视图包参数 context.ExceptionHandled = false;//表示异常已经被处理过。 context.Result = result; //返回自定义的视图内容 } } }
5、结果过滤器
说明:在Action过滤器之后,结果(如:页面渲染)的前后运行。
实现:继承Attribute类,实现IResultFilter接口,重写OnResultExecuting和OnResultExecuted方法。或者直接继承ResultFilterAttribute类,(或ActionFilterAttribute类),观察源码可知,该类继承了Attribute类,而且还实现IResultFilter接口。(异步的话实现IAsyncActionFilter接口,重写OnActionExecutionAsync方法)还可以实现:IAlwaysRunResultFilter 或 IAsyncAlwaysRunResultFilter 接口。
用途:可以获取action的返回结果,进行一些处理。
实现了IResultFilter或IAsyncResultFilter接口的结果过滤器在Action Result执行体的周围执行。当Action或Action过滤器产生Action结果时,只有成功运行的才会执行结果过滤器。如果异常过滤器处理了异常,那么结果过滤器就不会运行,除非异常过滤器将异常设置为null(Exception = null)。
OnResultExecuting方法运行于Action结果执行之前,故其可通过ResultExecutingContext.Result操作Action结果。如果将ResultExecutingContext.Cancel设置为 true,则OnResultExecuting方法可短路Action结果以及后续结果过滤器的执行。如果发生了短路,MVC将不会修改响应,所以当发生短路时,为避免生成空响应,你一般应该直接去修改响应对象。如果在OnResultExecuting方法内抛出异常,那么也将阻止Action结果以及后续过滤器的执行,但会被当做失败结果(而非成功结果)。
OnResultExecuted方法运行于Action结果执行之后。也就是说,如果没有抛出异常,响应可能就会被发送到客户端且不可再修改。如果Action结果在执行中被其它过滤器短路,则ResultExecutedContext.Canceled将被置为true。如果Action结果或后续结果过滤器抛出异常,则ResultExecutedContext.Exception将被置为非空值(non-null value)。把ResultExecutedContext.Exception设置为null后会影响到异常的“处理”,这将阻止异常在之后的管道内被MVC重新抛出。如果在结果过滤器内处理异常,需要确定此处是否适合将某些数据写入响应中。如果Action结果在执行中途抛出异常,而header也已被更新到客户端,那么将没有任何可靠的机制来发送失败代码。
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using System; namespace MyMiddlewareAndFilter.Filter { /// <summary> /// Result结果过滤器 /// </summary> public class CustomerResultFilterAttribute : Attribute, IResultFilter, IFilterMetadata, IOrderedFilter { public int Order { get; set; } public void OnResultExecuted(ResultExecutedContext context) { Console.WriteLine($"This is {typeof(CustomerResultFilterAttribute)} OnResultExecuted"); if (context.HttpContext.Request.Query["type"].Equals("4")) { //context.Result= //只读了 } } public void OnResultExecuting(ResultExecutingContext context) { Console.WriteLine($"This is {typeof(CustomerResultFilterAttribute)} OnResultExecuting"); if (context.HttpContext.Request.Query["type"].Equals("3")) { context.Result = new JsonResult(new { Remark = $"This is {typeof(CustomerResultFilterAttribute)} OnResultExecuting", Type = context.HttpContext.Request.Query["type"] }); } } } }
6、取消和短路
除了授权过滤器之外的所有过滤器执行顺序如下(没有异常的情况):OnResourceExecuting(资源ing)-->OnActionExecuting(行为ing)-->控制器中的action-->OnActionExecuted(行为ed)-->OnResultExecuting(结果ing)-->OnResultExecuted(结果ed)-->OnResourceExecuted(资源ed)
当action中抛出异常时,result过滤器是被短路了,不会执行:OnResourceExecuting-->OnActionExecuting-->控制器中的action-->OnActionExecuted-->OnException-->OnResourceExecuted
设置Result结果造成短路:通过设置传入过滤器方法的上下文参数中的 Result 属性,可以在过滤器管道的任意一点短路管道。该方式适用所有过滤器,在控制器中短路,不影响过滤器执行。
结果短路情况如下所示:(其中0表示不执行,1表示执行)
异常短路情况如下所示:(其中0表示不执行,1表示执行)
7、同一种过滤器的执行顺序(自定义)
8、过滤器中获取路由信息
context.ActionDescriptor.RouteValues["area"].ToString();
context.ActionDescriptor.RouteValues["controller"].ToString();
context.ActionDescriptor.RouteValues["action"].ToString();
context.RouteData.Values["controller"].ToString();
context.RouteData.Values["action"].ToString();
9、过滤器的使用方式
普通过滤器:
构造函数中带参数的过滤器:
构造函数中带参数的过滤器有2种处理方式:
方式1:TypeFilterAttribute用于处理含构造函数的自定义过滤器,不需要注册。
方式2:ServiceFilterAttribute用于处理含构造函数的自定义过滤器,但需要先注册。
首先需要先在Startup.cs中注入:
然后再标记:
当然除了以上2种方式外还可以直接进行全局注册:
最后我们再来看下整体的运行效果:
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.AspNetCore.Http; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using MyMiddlewareAndFilter.Middlewares; using MyMiddlewareAndFilter.Filter; namespace MyMiddlewareAndFilter { public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { services.AddControllersWithViews(options => { options.Filters.Add<CustomerResourceFilterAttribute>(); //注册全局过滤器 任意控制器-Action options.Filters.Add<CustomerExceptionFilterAttribute>(); }); //services.AddScoped<CustomerExceptionFilterAttribute>(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler("/Home/Error"); } app.UseMiddleware<RefuseStealingMiddleware>(); //防盗链中间件 app.Use(next => //中间件1 { Console.WriteLine("middleware 1"); return async c => { await Task.Run(() => Console.WriteLine($"This is middleware 1 Start")); await next.Invoke(c); await Task.Run(() => Console.WriteLine($"This is middleware 1 End")); }; }); app.Use(next => //中间件2 { Console.WriteLine("middleware 2"); return async c => { await Task.Run(() => Console.WriteLine($"This is middleware 2 Start")); await next.Invoke(c); await Task.Run(() => Console.WriteLine($"This is middleware 2 End")); }; }); app.Use(next => //中间件3 { Console.WriteLine("middleware 3"); return async c => { await Task.Run(() => Console.WriteLine($"This is middleware 3 Start")); await next.Invoke(c); await Task.Run(() => Console.WriteLine($"This is middleware 3 End")); }; }); app.UseStaticFiles(); app.UseRouting(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); }); } } }
using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using System; using MyMiddlewareAndFilter.Filter; namespace MyMiddlewareAndFilter.Controllers { [CustomerAuthorizationFilter] public class HomeController : Controller { private readonly ILogger<HomeController> _logger; public HomeController(ILogger<HomeController> logger) { _logger = logger; } [CustomerAction2Filter(Order = 2)] [CustomerActionFilter(Order = 1)] [CustomerResultFilter] public IActionResult Index() { Console.WriteLine($"This is {typeof(HomeController)} Index"); return View(); } //[TypeFilter(typeof(CustomerExceptionFilterAttribute))] //[ServiceFilter(typeof(CustomerExceptionFilterAttribute))] public IActionResult Test() { Console.WriteLine($"This is {typeof(HomeController)} Test"); throw new Exception("参数错误"); } } }
访问“/Home/Index”控制台输出如下:
访问“/Home/Test”控制台输出如下:
至此本文就全部介绍完了,如果觉得对您有所启发请记得点个赞哦!!!
Demo源码:
链接:https://pan.baidu.com/s/1OTr6zqqUv4Rpn5fYLN7Mug 提取码:leqf
此文由博主精心撰写转载请保留此原文链接:https://www.cnblogs.com/xyh9039/p/14359665.html
版权声明:如有雷同纯属巧合,如有侵权请及时联系本人修改,谢谢!!!