Asp.Net Core MVC中的过滤器

起因

想起原先学习Asp.Net MVC的过滤器(Filter,也有叫筛选器).

1. 过滤器运行的原理

前面一直在说中间件,Filter是在MVC中之后才执行的.具体我们看官方文档给的图.
过滤器在中间件执行之后到MVC中,可以通过 过滤器进行处理
写一个中间件,来验证请求是否先到中间件中还是先到MVC中.
 中间件和过滤器谁先执行

2. 过滤器的分类

Asp.Net Core MVC中过滤器的类型有哪些?
看完上图之后,我们先看看过滤器的执行顺序,这里先刨除异常过滤器.因为异常过滤器是在异常之后才会触发的.
Asp.Net Core MVC中排除异常过滤器,其他的过滤器执行顺序
根据上图执行的结果.我们在捋捋MVC中的过滤器的执行顺序:
  1. AuthorizationFilter OnAuthorization
  2. ResourceFilter  OnResourceExecuting
  3. Controller(控制器)初始化,构造函数调用
  4. ActionFilter  OnActionExecuting
  5. 进入到Action
  6. ActionFilter  OnActionExecuted
  7. ResultFilter  OnResultExecuting
  8. 进入View(视图)
  9. ResultFilter  OnResultExecuted
  10. ResourceFilter ResourceFilter

3. ExceptionFilter 过滤器的使用

后面可以上代码,正式进入过滤器的学习阶段.我们先看ExceptionFilter(异常处理过滤器):
异常处理过滤器,顾名思义,一定要有异常才能处理呀.于是我们在Index Action中要手动造一个异常.
public IActionResult Index()
{

    int i = 1;
    int j = 3;
    int m = i + j;

    int l = m - m;  //4-4 = 0 
    int k = m / l;  //4 / 0 //会有异常 

    Console.WriteLine("action:/home/index");

    return View();
}

先实现异常处理过滤器:

/// <summary>
/// 实现异常处理过滤
/// 1. 要实现ExceptionFilterAttribute
/// </summary>
public class CustomExceptionFilterAttribute : ExceptionFilterAttribute
{
    private ILogger<CustomExceptionFilterAttribute> _logger;

    public CustomExceptionFilterAttribute(ILogger<CustomExceptionFilterAttribute> logger)
    {
        _logger = logger;
    }

    /// <summary>
    /// 2. 重写OnException
    /// </summary>
    /// <param name="context"></param>
    public override void OnException(ExceptionContext context)
    {
        //3. 异常处理
        //!context.ExceptionHandled 表示异常没有处理
        if (!context.ExceptionHandled)
        {
            //处理异常
            Console.WriteLine($"info:{context.Exception.Message}");
            _logger.LogError($" {context.HttpContext.Request.Path}  {context.Exception.Message}");
            //给一个返回结果,这里返回json. 
            //在.Net Core 3.0 Json引入新入的组件System.Text.Json 来代替Newtonsoft.Json ,下面的返回中文的会进行Unicode            //所以还是处理一下编码,当然如果使用Newtonsoft.Json,这里可以忽略了,在Asp.Net Core中的需要在Startup.cs全局处理,下面要说过滤器的全局注册.这里就先不说了
            context.Result = new JsonResult(new { Msg = "暂时无法处理", time = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff") });
            context.ExceptionHandled = true;
        }
    }
}

写完异常处理过滤器,这时候异常过滤器并没有生效.要注册之后,才能生效.先看一下全局注册.

在Startup.cs文件中,在ConfigureServices中AddControllersWithViews,通过MvcOptions.Filters添加全局过滤器.

services.AddControllersWithViews(options =>
{
    options.Filters.Add(typeof(CustomAuthorizationFilterAttribute)); //注册全局授权过滤器
    options.Filters.Add(typeof(CustomExceptionFilterAttribute));     //注册全局异常处理过滤器

}).AddJsonOptions(options =>
{
    //这里处理System.Text.Json 中文编码问题
    options.JsonSerializerOptions.Encoder = JavaScriptEncoder.Create(UnicodeRanges.All);
});

重新编译项目,我们通过浏览器访问一下项目,看看有能不能处理异常.

注册全局异常处理过滤器,之后通过有异常的Action,查看返回接口

在浏览器中看到结果,是我们在异常处理过滤器指定的返回值.符合我们预期的效果.然后我们查看一下控制台输出的内容.

注册全局异常处理过滤器,并记录错误日志

带着好奇心,我们看看ExceptionFilterAttribute内部是有什么?

/// <summary>
/// An abstract filter that runs asynchronously after an action has thrown an <see cref="Exception"/>. Subclasses
/// must override <see cref="OnException"/> or <see cref="OnExceptionAsync"/> but not both.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public abstract class ExceptionFilterAttribute : Attribute, IAsyncExceptionFilter, IExceptionFilter, IOrderedFilter
{
    /// <inheritdoc />
    public int Order { get; set; }

    /// <inheritdoc />
    public virtual Task OnExceptionAsync(ExceptionContext context)
    {
        if (context == null)
        {
            throw new ArgumentNullException(nameof(context));
        }

        OnException(context);
        return Task.CompletedTask;
    }

    /// <inheritdoc />
    public virtual void OnException(ExceptionContext context)
    {
    }
}

看到上边的源码,我们发现ExceptionFilterAttribute只要重写OnException,在内部处理异常就好了.

我们发现ExceptionFilterAttribute继承Attribute和实现IAsyncExceptionFilter, IExceptionFilter, IOrderedFilter这三个接口,既然看了源码,就顺便看这几个接口的源码:

/// <summary>
/// Marker interface for filters handled in the MVC request pipeline.
/// </summary>
public interface IFilterMetadata
{
}

/// <summary>
/// A filter that runs asynchronously after an action has thrown an <see cref="System.Exception"/>.
/// </summary>
public interface IAsyncExceptionFilter : IFilterMetadata
{
    /// <summary>
    /// Called after an action has thrown an <see cref="System.Exception"/>.
    /// </summary>
    /// <param name="context">The <see cref="ExceptionContext"/>.</param>
    /// <returns>A <see cref="Task"/> that on completion indicates the filter has executed.</returns>
    Task OnExceptionAsync(ExceptionContext context);
}

/// <summary>
/// A filter that runs after an action has thrown an <see cref="System.Exception"/>.
/// </summary>
public interface IExceptionFilter : IFilterMetadata
{
    /// <summary>
    /// Called after an action has thrown an <see cref="System.Exception"/>.
    /// </summary>
    /// <param name="context">The <see cref="ExceptionContext"/>.</param>
    void OnException(ExceptionContext context);
}

/// <summary>
/// A filter that specifies the relative order it should run.
/// </summary>
public interface IOrderedFilter : IFilterMetadata
{
    /// <summary>
    /// Gets the order value for determining the order of execution of filters. Filters execute in
    /// ascending numeric value of the <see cref="Order"/> property.
    /// </summary>
    /// <remarks>
    /// <para>
    /// Filters are executed in an ordering determined by an ascending sort of the <see cref="Order"/> property.
    /// </para>
    /// <para>
    /// Asynchronous filters, such as <see cref="IAsyncActionFilter"/>, surround the execution of subsequent
    /// filters of the same filter kind. An asynchronous filter with a lower numeric <see cref="Order"/>
    /// value will have its filter method, such as <see cref="IAsyncActionFilter.OnActionExecutionAsync"/>,
    /// executed before that of a filter with a higher value of <see cref="Order"/>.
    /// </para>
    /// <para>
    /// Synchronous filters, such as <see cref="IActionFilter"/>, have a before-method, such as
    /// <see cref="IActionFilter.OnActionExecuting"/>, and an after-method, such as
    /// <see cref="IActionFilter.OnActionExecuted"/>. A synchronous filter with a lower numeric <see cref="Order"/>
    /// value will have its before-method executed before that of a filter with a higher value of
    /// <see cref="Order"/>. During the after-stage of the filter, a synchronous filter with a lower
    /// numeric <see cref="Order"/> value will have its after-method executed after that of a filter with a higher
    /// value of <see cref="Order"/>.
    /// </para>
    /// <para>
    /// If two filters have the same numeric value of <see cref="Order"/>, then their relative execution order
    /// is determined by the filter scope.
    /// </para>
    /// </remarks>
    int Order { get; }
}

接口源码比较简单,如果不实现ExceptionFilterAttribute,我们自己也很好实现异常过滤器.如果不需要异步处理,只需要实现IExceptionFilter和IOrderedFilter

public class CustomExceptionFilter1 : IExceptionFilter, IOrderedFilter
{
    public int Order => 0;

    public void OnException(ExceptionContext context)
    {
        if (!context.ExceptionHandled)
        {
            //在控制台输出当前过滤器名称和异常信息
            Console.WriteLine($"info {typeof(CustomExceptionFilter1)} :: {context.Exception.Message}");

            //给一个返回结果,这里返回json. 
            //在.Net Core 3.0 Json引入新入的组件System.Text.Json 来代替Newtonsoft.Json ,下面的返回中文的会进行Unicode
            //所以还是处理一下编码,当然如果使用Newtonsoft.Json,这里可以忽略了,在Asp.Net Core中的需要在Startup.cs全局处理,下面要说过滤器的全局注册.这里就先不说了
            context.Result = new JsonResult(new { Msg = "暂时无法处理", time = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff") });
            context.ExceptionHandled = true;
        }
    }
}
//注册全局异常处理过滤器
options.Filters.Add(typeof(CustomExceptionFilter1));

看看效果如何:

通过实现IExceptionFilter实现异常过滤器

我们在实现IExceptionFilter的同时,也实现IOrderedFilter接口,IOrderedFilter接口内部只有一个Order属性,主要是重写过滤器的默认执行顺序.

4. 过滤器的使用范围

上面在ExceptionFilter的时候,已经使用过全局注册了,过滤器的注册有3种.

  1. 全局注册,作用域在整个项目中使用
  2. 在单个控制器上注册,作用域在单个控制器中
  3. 在Action上注册,作用域在单个Action中

第一种全局注册,在上面有代码.我们看在控制器上的注册.

[CustomResourceFilter]  // 这里使用过滤器要实现 Attribute
public class HomeController : Controller
{
    // 省略一些代码
}

最后一种作用域范围最小的

[CustomActionFilter]  //在Action上使用 
       
public IActionResult Index()
{

    int i = 1;
    int j = 3;
    int m = i + j;

    int l = m - m;  //4-4 = 0 
    int k = m / l;  //4 / 0 //会有异常 

    Console.WriteLine("action:/home/index");

    return View();
}

5. 需要那种过滤器,就实现对应的接口

使用那种过滤器就实现对应的接口,是用异步接口还是同步接口,都可以按自己需要去做
使用那种过滤器就实现对应的接口,是用异步接口还是同步接口,都可以按自己需要去做
秋风 2020-08-12