【asp.net core 系列】10 实战之ActionFilter

0.前言

在上一篇中,我们提到了如何创建一个UnitOfWork并通过ActionFilter设置启用。这一篇我们将简单介绍一下ActionFilter以及如何利用ActionFilter,顺便补齐一下上一篇的工具类。

1. ActionFilter 介绍

ActionFilter全称是ActionFilterAttribute,我们根据微软的命名规范可以看出这是一个特性类,看一下它的声明:

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

这是一个允许标注在类和方法上的特性类,允许多个标记,标注之后子类会继承父类的特性。然后,这个类是一个抽象类,所以我们可以通过继承ActionFilterAttribute来编写自己的ActionFilter。

1.1 ActionFilter的四个方法

对于一个ActionFilter而言,最重要的是它的四个方法:

public virtual void OnActionExecuted(ActionExecutedContext context);
public virtual void OnActionExecuting(ActionExecutingContext context);

public virtual void OnResultExecuted(ResultExecutedContext context);
public virtual void OnResultExecuting(ResultExecutingContext context);
image-20200615231334442

上图是这四个方法在一次请求中执行的顺序。在一次请求真正执行之前,想要拦截这个请求,应该使用OnActionExecuting

为什么单独说这个呢?因为这个方法的出镜率很高,大多数时候都会使用这个方法进行请求过滤。

1.2 在ActionFilter中我们能做什么

我们来简单介绍一下,四个方法中的四种上下文类型,看一看里面有哪些我们可以利用的方法:

1.2.1 ActionExecutingContext

这是一个Action执行前的上下文,表示Action并未开始执行,但是已经获取到了控制器实例:

public class ActionExecutingContext : FilterContext
{
    public virtual IDictionary<string, object> ActionArguments { get; }
    public virtual object Controller { get; }
    public virtual IActionResult Result { get; set; }
}

ActionExecutingContext继承自FilterContext,我们暂且不关注它的父类,只看一下它自己的属性。

  • ActionArguments 表示Action的参数列表,这里面放着各种从用户接到请求参数以及其他中间处理程序添加的参数
  • Controller 表示执行该请求的控制器,在之前我们提过,asp.net core 对于控制器的限制很小,所以控制器什么类型都可能,故而这里使用object作为控制器类型
  • Result 执行结果,正常情况下,在此处获取这个属性的值没有意义。但是我们可以通过修改这个属性的值,来让我们拦截请求

1.2.2 ActionExecutedContext

ActionExecutedContext 表示Action执行完成后的上下文,这时候Action已经执行完成,我们可以通过这个获取Action执行结果:

public class ActionExecutedContext : FilterContext
{
    public virtual bool Canceled { get; set; }
    public virtual object Controller { get; }
    public virtual Exception Exception { get; set; }
    public virtual ExceptionDispatchInfo ExceptionDispatchInfo { get; set; }
    public virtual bool ExceptionHandled { get; set; }
    public virtual IActionResult Result { get; set; }
}

同样,继承自FilterContext,暂且忽略。

  • Canceled 表示是否被设置短路
  • Controller 处理请求的控制器
  • Exception 执行过程中是否发生异常,如果有异常则 有值,否则为Null
  • ExceptionHandled 异常是否被处理
  • Result 此处对Result进行修改不会屏蔽执行的ActionResult,但是可以向用户隐藏对应的实现

1.2.3 ResultExecutingContext

这是在Result渲染之前执行的上下文,这时候Action已经执行完毕,正准备渲染Result:

public class ResultExecutingContext : FilterContext
{
    public virtual bool Cancel { get; set; }
    public virtual object Controller { get; }
    public virtual IActionResult Result { get; set; }
}
  • Cancel 取消当前结果执行以及后续筛选器的执行
  • Controller 控制器
  • Result 处理结果

1.2.4 ResultExecutedContext

Result已经执行完成了,获取执行结果上下文:

public class ResultExecutedContext : FilterContext
{
    public virtual bool Canceled { get; set; }
    public virtual object Controller { get; }
    public virtual Exception Exception { get; set; }
    public virtual ExceptionDispatchInfo ExceptionDispatchInfo { get; set; }
    public virtual bool ExceptionHandled { get; set; }
    public virtual IActionResult Result { get; }
}

这个类与 ActionExecutedContext类似,就不做介绍了。

1.2.5 FilterContext

在上面的四个上下文都继承自 FilterContext,那么我们来看一下FilterContext中有哪些属性或者方法:

public abstract class FilterContext : ActionContext
{
    public virtual IList<IFilterMetadata> Filters { get; }
    public TMetadata FindEffectivePolicy<TMetadata>() where TMetadata : IFilterMetadata;
}

可以看到FilterContext继承了另一个ActionContext的类。小伙伴们应该对这个类要有一定的概念,这个类是Action的上下文类。它完整存在于一个Action的生命周期,所以有时候可以通过ActionContext进行Action级的数据传递(不推荐)。

那么,继续让我们回过头来看看ActionContext里有什么:

public class ActionContext
{
    public ActionDescriptor ActionDescriptor { get; set; }
    public HttpContext HttpContext { get; set; }
    public ModelStateDictionary ModelState { get; }
    public RouteData RouteData { get; set; }
}
  • ActionDescriptor 执行的Action描述信息,包括Action的显示名称、一些参数等,具体用到的时候,再为大伙详细说
  • HttpContext 可以通过这个属性获取此次请求的Request和Response对象
  • ModelState 模型校验信息, 这部分在后续再为小伙伴们细说
  • RouteData 路由信息,asp.net core 在处理请求时解析出来的路由信息,包括在程序中修改的路由信息

2. 使用ActionFilter

在《【asp.net core 系列】9 实战之 UnitOfWork以及自定义代码生成》也就是上一篇中,介绍到了ActionFilter与普通特性类一致,可以通过标注控制器然后启用该ActionFilter。

因为大多数情况下,一个ActionFilter并不会仅仅局限于一个控制器,而是应用于多个控制器。所以这时候,我们通常会设置一个基础控制器,在这个控制器上进行标注,然后让子类继承这个控制器。通过这种方式来实现一次声明多次使用。

当然,在asp.net core 中添加了另外的一种使用ActionFilter的方式,Setup.cs中

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews();
}

默认是这样的,我们可以通过设置参数来添加一个全局应用的Filter,例如说我们上一篇中创建的 UnitOfWorkFilterAttribute:

services.AddControllersWithViews(options=>
{
    options.Filters.Add<UnitOfWorkFilterAttribute>();
});

通过这种方式可以启用一个全局ActionFilter。如果需要使用asp.net core的默认依赖注入可以使用 AddService进行配置。(依赖注入的内容在后续会讲解)。

3. 工具类生成

继续上一篇遗留的内容:

public static void CreateEntityTypeConfig(Type type)
{
    var targetNamespace = type.Namespace.Replace("Data.Models", "");
    if (targetNamespace.StartsWith("."))
    {
        targetNamespace = targetNamespace.Remove(0);
    }
    var targetDir = Path.Combine(new[] { CurrentDirect, "Domain.Implements", "EntityConfigures" }.Concat(
        targetNamespace.Split('.')).ToArray());

    if (!Directory.Exists(targetDir))
    {
        Directory.CreateDirectory(targetDir);
    }
    var baseName = type.Name.Replace("Entity", "");
    if (!string.IsNullOrEmpty(targetNamespace))
    {
        targetNamespace = $".{targetNamespace}";
    }

    var file = $"using {type.Namespace};" +
        $"\r\nusing Microsoft.EntityFrameworkCore;" +
        $"\r\nusing Microsoft.EntityFrameworkCore.Metadata.Builders;" +
        $"\r\nnamespace Domain.Implements.EntityConfigures{targetNamespace}" +
        "\r\n{" +
        $"\r\n\tpublic class {baseName}Config : IEntityTypeConfiguration<{type.Name}>" +
        "\r\n\t{" +
        "\r\n\t\tpublic void Configure(EntityTypeBuilder<SysUser> builder)" +
        "\r\n\t\t{" +
        $"\r\n\t\t\tbuilder.ToTable(\"{baseName}\");" +
        $"\r\n\t\t\tbuilder.HasKey(p => p.Id);" +
        "\r\n\t\t}\r\n\t}\r\n}";
    File.WriteAllText(Path.Combine(targetDir, $"{baseName}Config.cs"), file);
}

工具类其实本质上就是一次文件写入的方法,本身没什么难度。

不过,这里还有有个小问题,每次调用都会覆盖原有的文件,还有就是这里面有很多可以优化的地方,小伙伴们可以自己试试去优化一下,让代码更好看一点。

4 总结

到目前为止,实战系列也有了几篇,很多小伙伴问我能提供一下源码吗?当然,能呀。不过不是现在,容我留个谜底。当主要框架功能完成之后,我就会给小伙伴们发代码的。

其实也是因为现在还没个完整的,开放给小伙伴们也没啥意义。当然了,跟着一块敲,也是能实现的哈。关键地方的代码都有。

更多内容烦请关注我的博客《高先生小屋》

file