ASP.NET Core 实现MVC filter“就近原则”

Intro

在我们的日常开发中,我们可能会用到 MVC 里的 Filter 来实现一些切面逻辑,有一些 filter 可能只希望执行一次,对于这样的 filter 我们需要怎么做呢,下面就是一个示例.

Sample

Filter 示例,这里我们以 AuthorizationFilter 为例,ActionFilter 也是一样的

public class TestAuthFilter : AuthorizationFilterAttribute
{
    public string Role { get; set; }

    public override void OnAuthorization(AuthorizationFilterContext context)
    {
        Console.WriteLine($"{nameof(TestAuthFilter)}({Role}) is executing");
    }
}

AuthorizationFilterAttribute 是自己为 AuthorizationFilter 定义的一个类似于 ActionFilterAttribute 的东西,稍微简化一些用作 Attribute 的 filter 定义,实现代码如下:

public abstract class AuthorizationFilterAttribute : Attribute, IAuthorizationFilter, IAsyncAuthorizationFilter
{
    public virtual void OnAuthorization(AuthorizationFilterContext context)
    {
    }

    public virtual Task OnAuthorizationAsync(AuthorizationFilterContext context)
    {
        Guard.NotNull(context);
        OnAuthorization(context);
        return Task.CompletedTask;
    }
}

再来看在 controller 代码中的使用示例:

[TestAuthFilter(Role = "Admin")]
[Route("api/authTest")]
public class AuthTestController : ControllerBase
{
    [TestAuthFilter(Role = "User")]
    [HttpGet]
    public IActionResult Index()
    {
        return Ok();
    }
}

这个示例中,我们在 controller 和 action 上都加了 Filter,那么实际执行的时候会怎么样呢?

可以访问一下这个接口来测试一下,控制台输出结果如下:

TestAuthFilter(Admin) is executing 
TestAuthFilter(User) is executing 

ASP.NET Core 实现MVC filter“就近原则”

可以看到这个 filter 执行了两次,先执行了 controller 定义的,然后执行了 action 上定义的

但是其实我期望的是如果 action  上有定义的话只执行 action 上的 filter,离方法最近的 filter 才生效,其他的被覆盖掉,不生效,我称它为 filter 的 “就近原则”,怎么实现呢?实现起来其实也非常的简单,改造我们的 filter 在执行 filter 的逻辑之前可以加一个判断,修改后的 filter 如下:

public class TestAuthFilter : AuthorizationFilterAttribute
{
    public string Role { get; set; }

    public override void OnAuthorization(AuthorizationFilterContext context)
    {
+       if (!context.IsEffectivePolicy(this)) return;
        Console.WriteLine($"{nameof(TestAuthFilter)}({Role}) is executing");
    }
}

然后我们再次访问我们的接口,输出结果如下:

TestAuthFilter(User) is executing 

ASP.NET Core 实现MVC filter“就近原则”

可以看到 controller 上定义已经没有执行了,只执行 action 上定义的 filter 了。

IsEffectivePolicy 是 FilterContext 中的一个方法,是所有 filter 的上下文都会继承的一个基类,无论是 AuthorizationFilter 还是 ActionFilterResourceFilterResultFilterExceptionFilter 都是可以像上面这样用的,那它又是怎么实现的呢?

What's inside

我们可以反编译或者直接去 Github 上看源代码:https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.Abstractions/src/Filters/FilterContext.cs

public virtual IList<IFilterMetadata> Filters { get; }

public bool IsEffectivePolicy<TMetadata>(TMetadata policy) where TMetadata : IFilterMetadata
{
    if (policy == null)
    {
        throw new ArgumentNullException(nameof(policy));
    }

    var effective = FindEffectivePolicy<TMetadata>();
    return ReferenceEquals(policy, effective);
}

public TMetadata FindEffectivePolicy<TMetadata>() where TMetadata : IFilterMetadata
{
    // The most specific policy is the one closest to the action (nearest the end of the list).
    for (var i = Filters.Count - 1; i >= 0; i--)
    {
        var filter = Filters[i];
        if (filter is TMetadata match)
        {
            return match;
        }
    }

    return default;
}

Filters 是当前请求对应的 action 所涉及到的所有 filter 的集合,离 action 最近的 filter 也就是从 filter 集合中倒序找,倒数第一个就是最近的,就是优先级最高的,在 filter 中就可以判断如果当前 filter 对象是否是最近的一个从而判断是否要执行 filter 中的逻辑。