Asp.Net Core 轻松学-被低估的过滤器

时间:2024-01-22 11:43:18

前言

    过滤器,从我们开始开发 Asp.Net 应用程序开始,就一直伴随在我们左右;Asp.Net Core 提供多种类型的过滤器,以满足多种多样的业务应用场景;并且在 Asp.Net Core 本身,过滤器的应用也非常广泛;但是,在实际的业务场景中,大部分开发人员只使用到其中 1 到 2 种类型,当然,这其中大部分可能性是由于业务场景的适用性使然,本文尝试简单介绍 Asp.Net Core 中提供的各种过滤器,以及实际的应用场景,希望对您有所帮助。

1. 介绍

1.1 作用范围

过滤器的作用范围
每种不同的过滤器都有实际的作用范围,有一些全局过滤器还有作用域的限制,这取决于应用开发者在定义和初始化过滤器的时候的选择,每个过滤器本身处理任务的权限和功能都大不相同,但是他们都有一个共同点,就是通过特性标记的方式使用,比如以下代码,对一个 Action 使用了过滤器 CustomerActionFilter

        [CustomerActionFilter]
        public ActionResult<string> Get(int id)
        {
            return "value";
        }
1.2 过滤器的工作原理

原理解释
过滤器一般在 Asp.Net Core MVC 管道内运行,一般在操作执行之前(befor) 或者执行之后(after) 执行,以供开发者可以选择在不同的执行阶段介入处理

1.3 过滤器类型,看下图

类型介绍
上图既是 Asp.Net Core 内置的各种过滤器类型,也是其执行优先级顺序,相同类型的过滤器还可以定义在某个阶段执行的顺序

  1. 授权过滤器 AuthorizeAttribute
  2. 资源过滤器 IResourceFilter
  3. 异常过滤器 IExceptionFilter
  4. 操作过滤器 ActionFilterAttribute
  5. 结果过滤器 IResultFilter

3. 授权过滤器

3.1 使用介绍

在请求到达的时候最先执行,优先级最高,主要作用是提供用户请求权限过滤,对不满足权限的用户,可以在过滤器内执行拒绝操作,俗称“管道短路”
*注意:该过滤器只有执行之前(befor),没有执行之后(after)的方法
通常情况下,不需要自行编写过滤器,因为该过滤器在 Asp.Net Core 内部已经有了默认实现,我们需要做的就是配置授权策略或者实现自己的授权策略,然后由系统内置的授权过滤器调用授权策略即可
必须将该过滤器内部可能出现的异常全部处理,因为在授权过滤器之前,没有任何组件能够捕获授权过滤器的异常,一旦授权管理器内部发生异常,该异常将直接输出到结果中

3.2 应用场景

授权管理器 AuthorizeAttribute 位于 命名空间 Microsoft.AspNetCore.Authorization 内,使用方式非常简单,查看以下代码

    [Authorize]
    [Route("api/[controller]")]
    public class UserController : Controller
    {
        [AllowAnonymous]
        [HttpGet]
        public ActionResult<string> Get()
        {
            return "default";
        }

        [HttpPost]
        public ActionResult<string> Post()
        {
            return "default";
        }
    }

UserController 被应用了 Authorize 特性进行标记,表示对该控制器内的任意操作执行授权验证;但是单独对 Get 操作进行了授权通过对标记,即 AllowAnonymous ,表示允许匿名访问
这是非常常用的做法,在授权应用中,常常需要对部分操作进行单独的授权策略
关于授权过滤器,先介绍到这里,下一篇单独对授权过滤器进行演示,因为关于这块的内容,要讲的实在是太多了

4. 资源过滤器

但请求进入,通过授权过滤器后,接下来将执行资源过滤器(如果有定义),使用资源过滤器甚至可以改变绑定模型,还可以在资源过滤器中实现缓存以提高性能

4.1 资源管理器实现自接口 IResourceFilter 或者 IAsyncResourceFilter,现在我们来实现一个资源过滤器,输出一行信息,看看执行顺序

    public class CustomerResourceFilter : Attribute, IResourceFilter
    {
        public void OnResourceExecuted(ResourceExecutedContext context)
        {
            Console.WriteLine("==== OnResourceExecuted");
        }

        public void OnResourceExecuting(ResourceExecutingContext context)
        {
            Console.WriteLine("==== OnResourceExecuting");
        }
    }

4.2 对 HomeController 的操作应用该资源过滤器,看看对一个操作同时应用 CustomerActionFilter 和 CustomerResourceFilter ,他们的执行顺序是什么

    [Route("api/[controller]")]
    [ApiController]
    public class HomeController : ControllerBase
    {
        [HttpGet]
        [CustomerActionFilter]
        [CustomerResourceFilter]
        public async Task<ActionResult<IEnumerable<string>>> Get()
        {
            return new string[] { "value1", "value2" };
        }
    }

4.3 启动程序,访问 http://localhost:5000/api/home,输出结果如下

可以看到,执行顺序和开篇的第一张图例一直,首先执行时资源过滤器的 OnResourceExecuting 方法,接着请求接入了 操作过滤器的 OnActionExecuting 方法,最后执行操作过滤器的 OnResultExecuting 方法,然后把请求交给资源过滤器的 OnResourceExecuted,最后返回到客户端
所以,从执行顺序可以看出,资源管理器的执行优先级总是高于操作过滤器
资源过滤器可以应用于控制器或者操作,然后基于其执行优先级的特点,开发员人员可以在资源过滤器中定义某些静态资源或者缓存直接将数据返回给客户端,并使其执行短路操作,减少后续管道请求步骤,以提高服务器响应性能

5. 异常过滤器

在服务器向客户端写入响应内容之前,如果系统引发了异常,异常过滤器可以捕获该异常,该过滤器作用于全局范围,这也是最常用的过滤器

5.1 创建一个异常过滤器

    public class CustomerExceptionFilter : Attribute, IExceptionFilter
    {
        public void OnException(ExceptionContext context)
        {
            Console.ForegroundColor = ConsoleColor.Red;
            Console.WriteLine("发生了异常:{0}", context.Exception.Message);
            Console.ForegroundColor = ConsoleColor.Gray;
        }
    }

5.2 将 CustomerExceptionFilter 应用到 HomeController 上

请注意,HomeController 上还同时应用了资源过滤器;现在要做到就是在资源过滤器内部抛出异常,看看 CustomerExceptionFilter 是否可以捕获该异常

    public class CustomerResourceFilter : Attribute, IResourceFilter
    {
        public void OnResourceExecuted(ResourceExecutedContext context)
        {
            Console.WriteLine("==== OnResourceExecuted");
        }

        public void OnResourceExecuting(ResourceExecutingContext context)
        {
            Console.WriteLine("==== OnResourceExecuting");
            throw new Exception("资源管理器发生了异常");
        }
    }

5.3 运行程序,访问 http://localhost:5000/api/home

可以看到,系统抛出了异常;但是,异常过滤器 CustomerExceptionFilter 并没有捕获该异常,事实证明资源过滤器的执行优先级还是高于异常过滤器,现在我们尝试在操作内部引发异常

    [Route("api/[controller]")]
    [ApiController]
    [CustomerResourceFilter]
    [CustomerExceptionFilter]
    public class HomeController : ControllerBase
    {
        // GET api/values
        [HttpGet]
        [CustomerActionFilter]
        public async Task<ActionResult<IEnumerable<string>>> Get()
        {
            throw new Exception("Get操作发生了异常");
            return new string[] { "value1", "value2" };
        }
    }

5.4 再次启动程序,访问 http://localhost:5000/api/home,控制台输出结果如下

5.5 客户端得到了一个友好的返回值

5.6 这是因为我们在异常过滤器内部将异常进行了出来,并通过设置 context.ExceptionHandled = true 来标记表示异常已经被处理,然后输出友好信息

    public class CustomerExceptionFilter : Attribute, IExceptionFilter
    {
        public void OnException(ExceptionContext context)
        {
            Console.ForegroundColor = ConsoleColor.Red;
            Console.WriteLine("发生了异常:{0}", context.Exception.Message);
            Console.ForegroundColor = ConsoleColor.Gray;

            context.Result = new JsonResult(new { code = 500, message = context.Exception.Message });
            context.ExceptionHandled = true;
        }
    }

异常过滤器的应用非常简单,你可以在其内部将异常写入日志,或者执行其它需要处理的逻辑

6. 操作过滤器 ActionFilterAttribute 和 结果过滤器 IResultFilter

  1. 操作过滤器:当请求进入 API 接口的时候,操作过滤器提供了一个进入之前(before)和进入之后(after)介入功能,可以使用该过滤器对进入 API 的参数和结果进行干预
  2. 结果过滤器:这个过滤器的作用和操作过滤器非常相似,主要其作用范围是有微小区别的,结果过滤器是在操作即将返回结果到客户端之前(before)或者之后(after)执行干预,比如你可以在返回结果之后(after)去渲染视图

6.1 之所以将这两个过滤器放在一起讲,是因为,这两个过滤器就像一对孪生兄弟一样,正所谓有始有终,首先来看操作过滤器

    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
    public abstract class ActionFilterAttribute : Attribute, IActionFilter, IFilterMetadata, IAsyncActionFilter, IResultFilter, IAsyncResultFilter, IOrderedFilter
    {
        protected ActionFilterAttribute();

        //
        public int Order { get; set; }

        //
        public virtual void OnActionExecuted(ActionExecutedContext context);
        //
        public virtual void OnActionExecuting(ActionExecutingContext context);
        //
        [AsyncStateMachine(typeof(<OnActionExecutionAsync>d__6))]
        public virtual Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next);
        //
        public virtual void OnResultExecuted(ResultExecutedContext context);
        //
        public virtual void OnResultExecuting(ResultExecutingContext context);
        //
        [AsyncStateMachine(typeof(<OnResultExecutionAsync>d__9))]
        public virtual Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next);
    }

操作过滤器包含 6 个基础方法,分别是执行前(before)执行后(after),写入结果前(before)写入后(after)
为什么会这样呢,因为操作过滤器实现的接口中包含了结果过滤器的接口
根据官方的提示,如果你需要重写 ActionFilterAttribute 的方法以处理自定义的业务逻辑,那么 OnActionExecutionAsync 这个异步方法不应该和 执行前(before)执行后(after)同时共存
同样,写入结果前(before)写入后(after)和 OnResultExecutionAsync 也是一样

6.2 操作过滤器包含了 写入结果前(before)写入后(after)的方法,这使得我们可以不用去定义结果过滤器就可以实现对写入结果的管理

当然,最好的做法是定义结果过滤器,这有助于业务分类,且逻辑清晰明了,但是如果你希望可以使用异步操作,很遗憾,结果过滤器不支持该方法

6.3 下面来看结果过滤的定义

    public class CustomerResultFilter : Attribute, IResultFilter
    {
        public void OnResultExecuted(ResultExecutedContext context)
        {
            Console.WriteLine("OnResultExecuted");
        }

        public void OnResultExecuting(ResultExecutingContext context)
        {
            Console.WriteLine("OnResultExecuting");
        }
    }

代码非常简单,就是实现接口 IResultFilter
IResultFilter 的工作原理和操作过滤器的写入结果前(before)写入后(after)的方法执行一致,可以看到,他们两个方法和参数名称都是一致的,因为他们都是实现同一个接口 IResultFilter

6.4 利用结果过滤器实现对输出结果的干预

下面就简单在结果过滤器内部去对已经组织好的数据进行干预,HomeController.Get 方法本应该输出 一个数组,我们在Header 中增加一项输出:Author=From Ron.liang

    public class CustomerResultFilter : Attribute, IResultFilter
    {
        public void OnResultExecuted(ResultExecutedContext context)
        {
            // ToDo
        }

        public void OnResultExecuting(ResultExecutingContext context)
        {
            // 干预结果
            context.HttpContext.Response.Headers.Add("Author", "From Ron.liang");
        }
    }

6.5 输出结果

7.在过滤器中使用依赖注入

在上面介绍的各种各样的过滤器中,有时候我们可能需要读取程序运行环境的信息,根据不同的环境做出不同的响应内容
比如,上面的结果过滤器写入作者信息,可能我们只希望在开发环境输出,而在产品环境忽略

7.1 使用 GetService,以支持依赖注入

        public void OnResultExecuting(ResultExecutingContext context)
        {
            var env = (IHostingEnvironment)context.HttpContext.RequestServices.GetService(typeof(IHostingEnvironment));

            Console.ForegroundColor = ConsoleColor.Blue;
            Console.WriteLine("OnResultExecuting,{0}", env.EnvironmentName);
            Console.ForegroundColor = ConsoleColor.Gray;

            // 干预结果
            if (env.IsDevelopment())
                context.HttpContext.Response.Headers.Add("Author", "From Ron.liang");
        }

上面的从 context.HttpContext.RequestServices.GetService(typeof(IHostingEnvironment)) 获取了环境变量,并判断在开发环境下为响应头添加内容

7.2 在过滤器中使用中间件

Asp.Net Core 提供了一个功能,使得我们在过滤器中可以使用中间件,实际上,这两者的使用方式非常类似
如果你希望这么做,可以定义一个包含 Configure(IApplicationBuilder applicationBuilder) 方法的类,在控制器或者操作中使用它

7.3 定义注册管理管道类

    public class RegisterManagerPipeline
    {
        public void Configure(IApplicationBuilder applicationBuilder)
        {
            CookieAuthenticationOptions options = new CookieAuthenticationOptions();

            applicationBuilder.UseCors(config =>
            {
                config.AllowAnyOrigin();
            });
        }
    }

RegisterManagerPipeline 定义了一个 Configure 方法,在该方法内部执行一个跨域设置,表示允许任何来源访问该站点;然后,我们在 UserController 中应用该管道

    [Authorize]
    [Route("api/[controller]")]
    [MiddlewareFilter(typeof(RegisterManagerPipeline))]
    public class UserController : Controller
    {
        // GET: api/<controller>
        [AllowAnonymous]
        [HttpGet]
        public ActionResult<string> Get()
        {
            return "default";
        }
    }

应用方式非常简单,就像使用普通过滤器一样对控制器进行特性标记即可
所不同的是,这里使用的是 MiddlewareFilter 进行注册 RegisterManagerPipeline
管道式过滤器的优先级非常高,甚至比授权过滤器的优先级还高,在使用的时候需要特别注意应用场景

8. 过滤器的执行顺序

相同类型的过滤器其执行顺序可以使用 Order 字段进行指定,该值为一个 int32 类型,值越小表示优先级越高,该值只能作用于相同类型的过滤器
比如,定义了两个 ActionFilter ,UserNameActionFilter,UserAgeActionFilter,分别制定其 Order 字段值为 10,5,那么 UserAgeActionFilter 将会在调用 ,UserNameActionFilter 之前执行
但是,即使指定了 Order ,ActionFilter 的执行优先级也不会超越授权管理器 AuthorizeAttribute,这是设计上的不同

8.1 Order 演示代码

        [HttpPost]
        [UserNameActionFilter(Order = 10)]
        [UserAgeActionFilter(Order = 5)]
        public void Post([FromBody] UserModel value)
        {
        }

8.2 输出结果

上图输出的黄色部分文字清晰的说明了过滤器的执行顺序
显示执行了资源过滤器,接着执行了 Order=5 的 UserAgeActionFilter ,最后执行了 Order=10 的 UserNameActionFilter 过滤器
可以看到,虽然操作过滤器设置了 Order=5,但其执行优先级仍然不能超越授权过滤器,甚至无法超越资源过滤器

结束语

  • 本文简单介绍了 Asp.Net Core 下系统内置的各种各样的过滤器,分别是

    1. 授权过滤器 AuthorizeAttribute
    2. 资源过滤器 IResourceFilter
    3. 异常过滤器 IExceptionFilter
    4. 操作过滤器 ActionFilterAttribute
    5. 结果过滤器 IResultFilter
  • 还通过一些简单说实例演示了过滤器的执行过程
  • 最后介绍了如何在过滤器中使用中间件,以及对过滤器的执行顺序进行了详细的演示

演示代码下载

https://files.cnblogs.com/files/viter/Ron.FilterDemo.zip