筛选器(filter,也可以翻译为”过滤器”)是APS.NET Core中提供的一种切面编程机制。
它允许开发人员创建自定义筛选器来处理横切关注点,也就是在ASP.NET Core特定的位置执行我们自定义的代码。
比如在控制器的操作方法之前执行数据检查的代码,或者在ActionResult执行的时候向响应报文头中写入自定义数据等。
ASP.NET Core中的筛选器有以下5种类型:授权筛选器、资源筛选器、操作筛选器、异常筛选器和结果筛选器。
在进行项目开发的时候,我们一般配置授权策略或编写自定义授权策略,而不是编写自定义授权筛选器,只有在需要自定义授权框架时才会用到自定义授权筛选器。
类似的道理也使用于资源筛选器和结果筛选器,因此本章重点讲解异常筛选器和操作筛选器。
所有筛选器一般都有同步和异步两个版本,比如同步操作筛选器实现IActionFilter接口,而异步操作筛选器实现IAsyncActionFilter接口。
在大部分场景下,异步筛选器的性能更好,而且可以支持在实现类中编写异步调用的代码,因此我们将主要讲解异步筛选器。
1. 异常筛选器
当系统中出现未经处理的异常的时候,异常筛选器就会执行,我们可以在异常筛选器中对异常进行处理。
我们知道,在ASP.NET Core Web API中,如果程序中出现未处理异常,就会生成如下图所示的响应报文:
![](https://ichiblog.cn/wp-content/uploads/2024/08/图片-1024x421.png)
这样的异常信息只有客户端才知道,网站的运维人员和开发人员不知道这个异常的存在,我们需要在程序中把未处理异常记录到日志中。
为了规范化接口的格式,当系统中出现未处理异常的时候,我们需要统一给客户端返回如下格式的响应报文:{“code”:”500″,”message”:”异常信息”}。
如果程序是在开发阶段运行,则异常信息的内容为全部异常堆栈,否则异常信息的内容固定为程序中出现未处理异常。
下面我们实现自定义异常筛选器来实现这两个功能。
首先,我们编写自定义的异常筛选器。
如以下代码所示:
public class ExceptionFilter(ILogger<ExceptionFilter> _Log,IHostEnvironment _Env) : IAsyncExceptionFilter
{
public Task OnExceptionAsync(ExceptionContext context)
{
Exception exception = context.Exception;
_Log.LogError(exception, "UnhandleException occured");
string message;
if (_Env.IsDevelopment())
{
message = exception.ToString();
}
else
{
message = "程序中未处理异常";
}
ObjectResult result = new(new{ code = 500,message = message }) ;
result.StatusCode = 500;
context.Result = result ;
context.ExceptionHandled = true ;
return Task.CompletedTask;
}
}
异步异常筛选器要实现IAsyncExceptionFilter接口。
由于筛选器中需要把异常信息记录到日志中并且判断程序的执行环境,因此筛选器需要注入ILogger和IHostEnvironment这两个服务。
在第7行代码中,我们使用context.Exception获取异常对象,然后在第8行代码中,把异常写入日志。
在13到20行代码中,我们检测程序的运行环境来决定message的值中是否显示异常堆栈。
很显然,在生产环境中,我们不能显示异常堆栈,以避免泄露程序的机密信息。
在22-26行代码中,我们设置响应报文的内容。
在第28行代码中,我们设置context.ExceptionHandled的值为true,通过这样的方式来告知ASP.NET Core不再执行默认的异常响应逻辑。
然后,我们在Program.cs的builder.Builder之前添加以下代码,设置全局的筛选器。
builder.Services.Configure<MvcOptions>(option =>
{
option.Filters.Add<ExceptionFilter>();
});
MvcOptions是ASP.NET Core项目的主要配置对象,我们在第3行代码中向Filters注册全局的筛选器,这样,项目中所有的ASP.NET Core的未处理异常都会被MyExceptionFilter处理。
用这种方式注入的筛选器是由依赖注入机制进行管理的,因此我们可以通过构造方法为筛选器注入其他的服务。
如上设置后,当控制器中Action出现未处理异常时,就会出现下图所示的响应报文。
![](https://ichiblog.cn/wp-content/uploads/2024/08/图片-1-1024x228.png)
需要注意的是,只有ASP.NET Core线程中的未处理异常才会被异常筛选器处理,后台线程中的异常不会被异常筛选器处理。
2. 操作筛选器基础
每次ASP.NET Core中控制器的操作方法执行的时候,操作筛选器都会被执行,我们可以在操作方法执行之前和执行之后执行一些代码,完成特定的功能。
操作筛选器一般实现IAsyncActionFilter接口,这个接口中定义了OnActionExecutionAsync方法,方法的声明如以下代码所示:
Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
其中,context参数代表Action执行的上下文对象,从context中我们可以获取请求的路径、参数值等信息;
next参数代表下一个要执行的操作筛选器。
一个项目中可以注册多个操作筛选器,这些操作筛选器组成一个链,上一个筛选器执行完了再执行下一个。
如下图所示,next就是一个用来指向下一个操作筛选器的委托,如果当前操作筛选器是最后一个筛选器的话,next就会执行要执行的操作方法。
![](https://ichiblog.cn/wp-content/uploads/2024/08/图片-2-1024x241.png)
下面来编写两个操作筛选器,以便演示操作筛选器的用法。
第1步,编写一个实现了IAsyncActionFilter接口的类ActionFilter1,如以下代码所示:
public class ActionFilter1 : IAsyncActionFilter
{
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
Console.WriteLine("ActionFilter1:开始执行");
ActionExecutedContext re = await next();
if (re.Exception != null)
{
Console.WriteLine("ActionFilter1:执行失败");
}
else
{
Console.WriteLine("ActionFilter1:执行成功");
}
}
}
第8行代码用next来执行下一个操作筛选器,如果这是最后一个操作筛选器,它就会执行实际的操作方法。
next之前的代码是在操作方法执行之前要执行的代码,而next之后的代码则是在操作方法执行之后要执行的代码。
next的返回值是操作方法的执行结果,返回值是ActionExecutedContext类型的。
如果操作方法执行的时候出现了未处理异常,那么ActionExecutedContext的Excetion属性就是异常对象,ActionExecutedContext的Result属性就是操作方法的执行结果。
第2步,编写一个和ActionFilter1类似的类ActionFilter2,如以下代码所示:
public class ActionFilter2 : IAsyncActionFilter
{
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
Console.WriteLine("ActionFilter2:开始执行");
ActionExecutedContext re = await next();
if (re.Exception != null)
{
Console.WriteLine("ActionFilter2:执行失败");
}
else
{
Console.WriteLine("ActionFilter2:执行成功");
}
}
}
第3步,在Program.cs中注册这两个操作筛选器,如下面代码所示:
builder.Services.Configure<MvcOptions>(option =>
{
option.Filters.Add<ActionFilter1>();
option.Filters.Add<ActionFilter2>();
});
第4步,在控制器中增加一个要测试的操作方法,如以下代码:
[HttpGet]
public string GetData()
{
Console.WriteLine("执行GETDATA");
return "HelloWorld";
}
第5步,启动项目,访问操作方法的路径,程序运行结果如图:
![](https://ichiblog.cn/wp-content/uploads/2024/08/图片-3-1024x525.png)
从程序运行结果可以看出,多个操作筛选器和操作方法的执行顺序和我们的分析结果一致。
需要特别说明的是,虽然操作筛选器实现的是IAsyncActionFilter接口,但是并不是说操作筛选器只能处理异步操作方法。
无论是同步的操作筛选器还是异步的操作筛选器,都可以处理同步和异步的操作方法,区别在于操作筛选器的实现代码是同步代码还是异步代码。
3. 案例:自动启用事务的操作筛选器
我们知道,数据库事务有一个非常重要的特性,那就是”原子性”,它保证了我们对数据库的多个操作要么全部成功、要么全部失败,进而帮助我们保证业务数据的正确性。
但是,事务的使用是比较麻烦的,需要我们手工启用、提交及回滚事务,我们的业务代码中会充斥着事务管理的代码。
这里将实现一个对于数据库操作自动启用事务的操作筛选器。
我们可以使用TransactionScope简化事务代码的编写。
TransactionScope是.NET中用来标记一段支持事务的代码的类。
EF Core对TransactionScope提供了天然的支持,当一段使用EF Core进行数据库操作的代码放到TransactionScope声明的范围中的时候,这段代码就会自动被标记为”支持事务”。
TransactionScope实现了IDisposable接口,如果一个TransactionScope的对象没有调用Complete就执行了Dispose方法,则事务会被回滚,否则事务就会被提交。
TransactionScope还支持嵌套式事务,也就是多个TransationScope嵌套,只有最外层的TransactionScope提交了事务,所有的操作才生效;
如果最外层的TransactionScope回滚了事务,那么即使内层的TransactionScope提交了事务,最终所有的操作仍然会被回滚。
.NET Framework中也有TransactionScope类,它的行为和.NET Core中的TransactionScope类的行为类似,不过.NET Framework中的TransactionScope类会使用Windows特有的MSDTC技术支持分布式事务,而.NET Core由于是跨平台的,因此不支持分布式事务。
MSDTC实现的分布式事务是强一致性的事务,尽管很简单易用,但是会带来性能等问题,不符合现在主流的”最终一致性事务”方案。
因此,我们在.NET Core中使用TransactionScope的时候不用担心MSDTC事务提升的问题,当需要进行分布式事务处理的时候,请使用最终一致性事务。
在同步代码中,TransactionScope使用ThreadLocal关联事务信息;
在异步代码中,TransactionScope使用AsyncLocal关联事务信息。
我们编写的操作方法中,可能不希望有的方法自动启用事务控制,可以给这些操作方法添加一个自定义的NotTransactionAttribute,如以下代码:
[AttributeUsage(AttributeTargets.Method)]
public class NotTransactionAttribute:Attribute
{
}
然后开发筛选器TransactionScopeFilter,其OnActionExecutionAsync方法的实现如下面代码所示:
public class TransactionScopeFilter : IAsyncActionFilter
{
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
bool hasNotTransactionlAttribute = false;
if(context.ActionDescriptor is ControllerActionDescriptor)
{
var actionDesc = (ControllerActionDescriptor)context.ActionDescriptor;
hasNotTransactionlAttribute = actionDesc.MethodInfo.IsDefined(typeof(NotTransactionAttribute),true);
}
if (hasNotTransactionlAttribute)
{
await next();
return;
}
using TransactionScope txScope = new(TransactionScopeAsyncFlowOption.Enabled);
var result = await next();
if(result.Exception == null) txScope.Complete();
}
}
第6~15行代码中,判断操作方法是否标注了NotTransactionAttribute;
第17~22行代码中,判断操作方法上如果标注了NotTransactionalAttribute,则直接执行 await next(),因为OnActionExecutionAsync方法的代码是异步的,因此在第24行代码中创建TransactionScope对象的时候,需要设定TransactionScopeAsyncFlowOption.Enable这个构造方法的参数;
在第28行代码中,如果我们发现操作方法执行时没有出现异常,就调用Complete最终提交事务。
不要忘了把TransactionScopeFilter注册到Program.cs中,然后再编写插入数据的操作方法,如以下代码所示:
[HttpPost]
public async Task Save()
{
_context.Songs.Add(new() { Song_Id = Guid.NewGuid().ToString(),Artist_Id = Guid.NewGuid().ToString() });
await _context.SaveChangesAsync();
_context.UserActions.Add(new() { Action_Type = Guid.NewGuid().ToString(), Song_Id = Guid.NewGuid().ToString() });
await _context.SaveChangesAsync();
}
上面的代码能偶正确地插入两条数据。
如果我们在第8行中加入一行throw new Exception()抛出异常,再次执行Save方法之后,我们会发现数据库中没有插入一条数据,这说明第7行代码虽然实现了插入数据,但是由于事务回滚,因此第7行代码插入的数据也被回滚了。