筛选器

1.筛选器(filter,也可以翻译为“过滤器”)是ASP.NET Core中提供的一种切面编程机制,它允许开发人员创建自定义筛选器来处理横切关注点,也就是在ASP.NET Core特定的位置执行我们自定义的代码,比如在控制器的操作方法之前执行数据检查的代码,或者在ActionResult执行的时候向响应报文头中写入自定义数据等。
2.ASP.NET Core中的筛选器有以下5种类型:授权筛选器(Authorization filter)、资源筛选器(Resource filter)、操作筛选器(Action filter)、异常筛选器(Exception filter)、结果筛选器(Result filter)。
3.所有筛选器一般有同步和异步两个版本,在大部分场景下,异步筛选器的性能更好,而且可以支持在实现类中编写异步调用的代码。

异常筛选器

当系统中出现未经处理的异常的时候,异常筛选器就会执行,我们可以在异常筛选器中对异常进行处理。
这样的异常信息只有客户端才知道,网站的运维人员不知道这个异常的存在,我们需要在程序中把未处理异常记录到日志中。

1.当系统中出现未处理异常的时候,我们需要统一给客户端返回如下格式的响应报文:{“code”:”500”,”message”:”异常信息”}。对于开发环境中message是异常堆栈,对于其他环境message用一个general的报错信息。
2.实现IAsyncExceptionFiler接口。注入IHostEnvironment得知运行环境。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class MyExceptionFilter : IAsyncExceptionFilter
{
private readonly ILogger<MyExceptionFilter> logger;
private readonly IHostEnvironment env;

public MyExceptionFilter(ILogger<MyExceptionFilter> logger, IHostEnvironment env)
{
this.logger = logger;
this.env = env;
}

public Task OnExceptionAsync(ExceptionContext context)
{
Exception exception = context.Exception;
logger.LogError(exception,"UnhandledException occured");
string message;
if(env.IsDevelopment())
{
message = exception.ToString();
}
else
{
message= "程序中出现未处理异常";
}
ObjectResult result = new ObjectResult(new { code = 500, message = message });
result.StatusCode = 500;
context.Result = result;
context.ExceptionHandled = true; // 标记异常已处理
return Task.CompletedTask;
}
}

设置context.ExceptionHandled = true;通过这样的方式来告知ASP.NET Core不再执行默认的异常响应逻辑。

我们在Program.cs的builder.Build之前添加全局的筛选器。

1
2
3
4
builder.Services.Configure<MvcOptions>(options =>
{
options.Filters.Add<MyExceptionFilter>();
});

MvcOptions是ASP.NET Core项目的主要配置对象,我们在第3行代码中向Filters注册全局的筛选器,这样,项目中所有的ASP.NET Core中的未处理异常都会被MyExxceptionFilter处理。用这种方式注入的筛选器是由依赖注入机制进行管理的,因此我们可以通过构造方法为筛选器注入其他的服务。

操作筛选器基础

每次ASP.NET Core中控制器的操作器的操作方法执行的时候,操作筛选器都会被执行,我们可以在操作方法执行之前和执行之后执行一些代码,完成特定的功能。
操作筛选器一般实现IAsyncActionFilter接口,这个接口定义OnActionExecutionAsync方法。

1
Task OnActionExecutionAsync (ActionExecutingContext context, ActionExecutionDelegate next)

其中,context参数代表Action执行的上下文对象,从context中我们可以获取请求的路径、参数值等信息;next参数代表下一个要执行的操作筛选器。一个项目中可以注册多个操作筛选器,这些操作筛选器组成一个链,上一个筛选器执行完了再执行下一个。next就是一个用来指向下一个筛选器的话,next就会执行要执行的操作方法。

1.编写一个实现了IAsyncActionFilter接口的类MyActionFilter1.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MyActionFilter1 : IAsyncActionFilter
{
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
Console.WriteLine("MyActionFilter1:开始执行");
ActionExecutedContext r = await next();
if(r.Exception != null)
{
Console.WriteLine("MyActionFilter1:发生异常");
}
else
{
Console.WriteLine("MyActionFilter1:执行结束");
}
}
}

next的返回值是操作方法的执行结果,返回值是ActionExecutedContext类型的。如果操作方法执行的时候出现了未处理异常,那么ActionExecutedContext的Exception属性就是异常对象,ActionExecutedContext的Result属性就是操作方法的执行结果。

2.编写一个和MyActionFilter1类似的类MyActionFilter2.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MyActionFilter2 : IAsyncActionFilter
{
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
Console.WriteLine("MyActionFilter2:开始执行");
ActionExecutedContext r = await next();
if(r.Exception != null)
{
Console.WriteLine("MyActionFilter2:发生异常");
}
else
{
Console.WriteLine("MyActionFilter2:执行结束");
}
}
}

3.在Program.cs中注册这两个操作筛选器。

1
2
3
4
5
6
builder.Services.Configure<MvcOptions>(options =>
{
// 添加全局筛选器
options.Filters.Add(new MyActionFilter1());
options.Filters.Add(new MyActionFilter2());
});

4.在控制其中增加一个要测试的操作方法。

1
2
3
4
5
6
[HttpGet]
public string Get()
{
Console.WriteLine("TextController:Get方法被调用");
return "Hello, World!";
}

运行

案例:自动启动事务的操作筛选器

1.数据库事务:要么全部成功、要么全部失败。
2.自动化:启动、提交以及回滚事务。
3.当一段使用EF Core进行数据库操作的代码放到TransactionScope声明的范围中的时候,这段代码就会自动被标记为“支持事务”。
4.TransactionScope实现了IDisposale接口,如果一个TransactionScope的对象没有调用Conmplete()就执行了Dispose()方法,则事务会被回滚,否则事务就会被提交。
5.TransactionScope还支持嵌套式事务。
6..NET Core中的TransactionScope不像.NET FX一样有MSDTC分布式事务提升的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class DemoController : ControllerBase
{
private MyDbContext ctx;

public DemoController(MyDbContext ctx)
{
this.ctx = ctx;
}
[HttpGet]
public async Task<string> Test1()
{
using (TransactionScope tx=new TransactionScope(/*TransactionScopeAsyncFlowOption.Enabled*/))
{
ctx.Books.Add(new Book { Name = ".Net1", Price = 1 });
await ctx.SaveChangesAsync(); // 这里会自动启用事务
ctx.Personcs.Add(new Personcs { Name = "YOUXIANYU", Age = 19 });
await ctx.SaveChangesAsync(); // 这里会自动启用事务
return "ok";
}
}
}

我们编写的操作方法中,可能不希望有的方法自动启用事务控制,可以给这些操作方法添加一个自定义的NotTransactionlAttribute。

1
2
3
4
[AttributeUsage(AttributeTargets.Method)]
public class NotTransactionalAttribute:Attribute
{
}

然后,开发筛选器TransactionScopeFilter,其OnActionExecutionAsync方法的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class TransactionScopeFilt : IAsyncActionFilter
{
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
bool hasNotTransactionalAttribute = false;
if (context.ActionDescriptor is ControllerActionDescriptor)
{
var actionDesc = (ControllerActionDescriptor)context.ActionDescriptor;
hasNotTransactionalAttribute = actionDesc.MethodInfo.IsDefined(typeof(NotTransactionalAttribute), false);
}
if (hasNotTransactionalAttribute)
{
await next();
return;
}
using var txScope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled);
var result = await next();
if (result.Exception == null)
{
txScope.Complete();
}
}
}

配置Program.cs程序集

1
2
3
4
builder.Services.Configure<MvcOptions>(options =>
{
options.Filters.Add(typeof(TransactionScopeFilt));
});

案例:开发请求限流器

1.Action Filter可以在满足条件的时候终止操作方法的执行。
2.在Action Filter中,如果我们不调用await next(),就可以终止Action方法的执行了。
3.为了避免而已客户端频繁发送大量请求消耗服务器资源,我们要实现“一秒钟内只允许最多有一个来自同一个IP地址的请求”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class RateLimitFilter : IAsyncActionFilter
{
private readonly IMemoryCache memCache; //注入内存缓存

public RateLimitFilter(IMemoryCache memCache)
{
this.memCache = memCache;
}

public Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
string removeIP = context.HttpContext.Connection.RemoteIpAddress.ToString(); //获取请求IP
string cacheKey = $"LastVisitTick_{removeIP}"; //缓存键
long? lastTick = memCache.Get<long?>(cacheKey); //获取上次请求时间
if (lastTick == null || Environment.TickCount64 - lastTick > 1000) //请求间隔大于1秒
{
memCache.Set(cacheKey, Environment.TickCount64, TimeSpan.FromSeconds(10)); //设置缓存时间为10秒
return next(); //继续执行请求
}
else
{
context.Result = new ContentResult { StatusCode = 429, Content = "请求过快,请稍后再试" }; //返回429状态码
return Task.CompletedTask; //结束请求
}
}
}

启用内存缓存

1
2
3
4
builder.Services.Configure<MvcOptions>(options =>
{
options.Filters.Add(typeof(RateLimitFilter));
});

中间件

中间件是ASP.NET Core的核心组件,MVC框架、相应缓存、身份验证、CORS、Swagger等都是内置中间件。

什么是中间件

1.广义上来讲:Tomcat、WebLogic、Redis、ISS;狭义上来讲,ASP.NET Core中的中间件指ASP.ENT Core中的一个组件。
2.中间件由前逻辑、next、后逻辑3部分组成,前逻辑为第一段要执行的逻辑代码、next为指向下一个中间件的调用、后逻辑为从下一个中间件执行返回所执行的逻辑代码。每个HTTP请求都要经历一个系列中间件的处理,每个中间件对于请求进行特定的处理后,再转到下一个中间件,最终的业务逻辑代码执行完成后,相应的内容也会按照处理的相反顺序进行处理,然后形成HTTP响应报文返回给客户端。
3.中间件组成一个管道,整个ASP.ENT Core的执行过程就是HTTP请求和相应按照中间件组装的顺序在中间件之间流转的过程。开发人员可以对组成管道的中间件按照需要进行自由组合。

中间件的三个概念

Map、Use和Run。Map用来定义一个管道可以处理那些请求,Use和Run用来定义管道,一个管道由若干个Use和一个Run组成,每个Use引入一个中间件,而Run是用来执行最终的核心应用逻辑。

基本中间件使用

创建一个ASP.NET Core空项目。

简单的自定义中间件
为了能够更清晰地了解中间件,我们创建一个空的ASP.NET Core项目,然后手动添加中间件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
app.Map("/test", async (pipeBuilder) =>
{
pipeBuilder.Use(async (context, next) =>
{
context.Response.ContentType = "text/html";
await context.Response.WriteAsync("1 start<br/>");
await next.Invoke();
await context.Response.WriteAsync("1 end<br/>");
});
pipeBuilder.Use(async (context, next) =>
{
await context.Response.WriteAsync("2 start<br/>");
await next.Invoke();
await context.Response.WriteAsync("2 end<br/>");
});
pipeBuilder.Run(async context =>
{
await context.Response.WriteAsync("Run<br/>");
});
});

需要注意的是,按照微软的建议,如果我们在一个中间件中使用ctx.Response.WriteAsync等方式向客户端发送响应,我们就不能再执行next.Invoke把请求转到其他中间件了。因为其他中间件有可能对Response进行了更改,比如修改响应状态码、修改报文头或者向响应报文中写入其他数据,这样就会造成响应报文体被损坏的问题。因此,在代码中的中间件中,我们在向报文体中写入内容后,又执行next.Invoke是不推荐的行为。

中间件类

1.如果定义中间件的代码比较复杂,或者需要重复使用一个中间件的话,最好把中间的代码放到一个单独的类中,这样的类我们称之为“中间件类”。
2.中间件类是一个普通的.NET类,它不需要继承任何父类或者实现任何接口,但是这个类需要有一个构造方法,构造方法至少有一个RequertDelegate类型的参数,这个参数用来指向下一个中间件。这个类还需要定义一个名字为Invoke或InvokeAsync的方法,方法至少有一个HttpContext类型的参数,方法的返回值必须是Task类型。中间件类的构造方法和Invoke(或InvokeAsync)方法还可以定义其他参数,其他参数的值会通过依赖注入自动赋值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class TestIMddleware
{
private readonly RequestDelegate next;
public TestIMddleware(RequestDelegate next)
{
this.next = next;
}
public async Task InvokeAsync(HttpContext context)
{
await context.Response.WriteAsync("TestIMddleware start<br/>");
await next.Invoke(context);
await context.Response.WriteAsync("TestIMddleware end<br/>");
}
}

开发一个简单的中间件类,这个中间件类会检查请求中是否有password为123的查询字符串,而且会把请求报文体按照JSON格式尝试解析为dynamic类型的对象,并且把pynamic对象放入context.Items中供后续的中间件或者Run使用。

JsonSerializer.Deserialize<dynamic>
把json反序列化为dynamic类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class CheckMiddleware
{
private readonly RequestDelegate next;
public CheckMiddleware(RequestDelegate next)
{
this.next = next;
}
public async Task InvokeAsync(HttpContext context)
{
string pwd = context.Request.Query["passwork"];
if (pwd == "123")
{
if (context.Request.HasJsonContentType())
{
var reqStream = context.Request.BodyReader.AsStream();
var obj = JsonSerializer.Deserialize<dynamic>(reqStream);
context.Items["BodyJson"] = obj;
}
await next(context);
}
else
{
context.Response.StatusCode = 401;
await context.Response.WriteAsync("没有权限访问");
}
}
}

使用中间件类CheckAndParsingMiddleware,修改后的Program.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
app.Map("/test", async (pipeBuilder) =>
{
pipeBuilder.Use(async (context, next) =>
{
context.Response.ContentType = "text/html";
await context.Response.WriteAsync("1 start<br/>");
await next.Invoke();
await context.Response.WriteAsync("1 end<br/>");
});
pipeBuilder.Use(async (context, next) =>
{
await context.Response.WriteAsync("2 start<br/>");
await next.Invoke();
await context.Response.WriteAsync("2 end<br/>");
});
pipeBuilder.UseMiddleware<TestIMddleware>();
pipeBuilder.Run(async context =>
{
await context.Response.WriteAsync("Run<br/>");
dynamic obj = context.Items["BodyJson"];
if(obj != null)
{
await context.Response.WriteAsync($"BodyJson: {obj}<br/>");
}
});
pipeBuilder.Run(async context =>
{
await context.Response.WriteAsync("Run<br/>");
});
});

Filter(筛选器)和Middileware(中间件)的区别

中间件是ASP.NET Core这个基础提供的功能,而Filter是ASP.NET Core WVC中提供的功能。ASP.NET Core MVC是由MVC中间件提供的框架,而Filter属于MVC中间件提供的功能。

区别

  1. 中间件可以处理所有的请求,而Filter只能处理对控制器的请求;中间件运行在一个更底层、更抽象的级别,因此在中间件中无法处理MVC中间件特有的概念。
  2. 中间件和Filter可以完成很多相似的功能。“未处理异常中间件”和“未处理异常Filter”;“请求限流中间件”和“请求限流Filter”的区别。
  3. 优先选择使用中间件;但是如果这个组件只针对MVC或者需要调用一些MVC相关的类的时候,我们就只能选择Filter。