第四十四节:再探过滤器基础、异步写法、限流案例、自动事务案例和中间件的使用

一. 过滤器复习

 (详细说明可参考:https://www.cnblogs.com/yaopengfei/p/11232921.html)

1. 过滤器的种类

  有5种过滤器,分别是授权、资源、操作、结果、异常五大过滤器

2. 过滤器执行顺序

(1).四个过滤器的执行顺序

    异常过滤器不参与测试,测试剩余四个过滤器的执行顺序,将四个过滤器在下面Index2方法上,经断点测试执行顺序如下:

  OnAuthorization→OnResourceExecuting→创建控制器→OnActionExecuting→执行action业务→OnActionExecuted→OnResultExecuting→页面渲染加载→OnResultExecuted→OnResourceExecuted

 

(2). 相同类型过滤器不同作用域的执行顺序

   经测试,将操作过滤器分别作用在 action、Controller、全局,通过加断点测试执行顺序如下:

   OnActionExecuting(全局)→OnActionExecuting(Controller)→OnActionExecuting(action)→接口中的业务→OnActionExecuted(action)→OnActionExecuted(Controller)→OnActionExecuted(全局)

   注意:可以通过修改order属性,自定义这个顺序

 

二. 过滤器实操

1. 重写异步方法

  这里以ActionFilter为例,实现异步接口IAsyncActionFilter, 然后实现OnActionExecutionAsync方法, 通过  await next(); 表示调用action中的方法,该代码之前表示调用action之前的,该代码之后, 则表示调用action方法之后的业务(PS:在过滤器中,可以拿到 await next()的返回值, 从而判断action执行业务是否有异常)

   注:这里实现同步接口有区别, 同步接口实现的方法 OnActionExecuting表示执行前,OnActionExecuting表示执行后

代码分享

public class MyActionFilter1 : IAsyncActionFilter
    {
        public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
        {
            Console.WriteLine("MyActionFilter1: 我是业务执行之前的代码");
            await next();
            Console.WriteLine("MyActionFilter1: 我是业务执行之后的代码");
        }
    }
public class MyActionFilter2 : IAsyncActionFilter
    {
        public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
        {
            Console.WriteLine("MyActionFilter2: 我是业务执行之前的代码");
            ActionExecutedContext result = await next();
            if (result.Exception == null)
            {
                Console.WriteLine("MyActionFilter2: 我是业务执行之后的代码");
            }
            else
            {
                Console.WriteLine("MyActionFilter2: 我是业务执行之后的代码,action中出错了");
            }
        }
    }

2. 注册的写法

(1). 全局注册

  A. 可以写在 builder.Services.AddControllers() 里

  B. 也可以写在 builder.Services.Configure<MvcOptions>()里

详见:Program中MyActionFilter2的注册

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers(option =>
{
    //全局注册过滤器位置1
    option.Filters.Add<MyActionFilter2>();
});

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

//全局注册过滤器位置2
//builder.Services.Configure<MvcOptions>(option =>
//{
//    option.Filters.Add<MyActionFilter2>();
//});

(2). 局部注册

  直接通过 [TypeFilter(typeof(MyActionFilter1))]加在action或Controller上即可

详见:Test1Controller/getMsg方法 

        /// <summary>
        /// 测试Action过滤器异步重写的方式
        /// </summary>
        [TypeFilter(typeof(MyActionFilter1))]
        [HttpGet]
        public string GetMsg()
        {
            Console.WriteLine("我是getMsg内部业务");
            return "获取成功";
        }

3. 运行效果

  如图所示, 从而也说明 全局注册的过滤器 优先于 Action级别注册的过滤器 先执行

 

三. 过滤器案例

1. 限流案例

(1). 需求

  为了避免恶意客户端频繁发送大量请求消耗服务器资源,我们要实现“一秒钟内只允许最多有一个来自同一个IP地址的请求任何一个接口”

(注:这里不区分哪个接口,即1s内,同一个ip只能请求一个接口)

(2). 解决思路

  A. 获取请求的ip地址, 和lastVisitTick_组装成key,

  B. 缓存中获取,如果不存在,或者当前时间-缓存中存放的时间 大于1s, 则将当前时间重新存入缓存,并设置一个较短的过期时间,然后执行action中的业务

  C. 反之则直接截断,提示被限流了

(3). 测试

  这里为了方便测试,通过 [TypeFilter(typeof(RateLimitFilter))] 单纯的加个一个action上进行测试,并不全局注册,然后快速请求接口,会发现被限流了

代码分享:

    /// <summary>
    /// 限流过滤器
    /// </summary>
    public class RateLimitFilter : IAsyncActionFilter
    {
        private readonly IMemoryCache cache;
        public RateLimitFilter(IMemoryCache cache)
        {
            this.cache = cache;
        }
        public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
        {
            var remoteIp = context.HttpContext.Connection.RemoteIpAddress;
            string remoteIpStr;
            if (remoteIp == null)
            {
                remoteIpStr = "___";
            }
            else
            {
                remoteIpStr = remoteIp.ToString();
            }
            string cacheKey = $"lastVisitTick_{remoteIpStr}";
            long? lastTick = cache.Get<long>(cacheKey);
            if (lastTick == null || Environment.TickCount64 - lastTick > 1000)
            {
                cache.Set(cacheKey, Environment.TickCount64, TimeSpan.FromSeconds(15));
                await next();
            }
            else
            {
                context.Result = new ContentResult { StatusCode = 429, Content="the request is limited" };
                await Task.CompletedTask;
            }
        }
    }
        /// <summary>
        /// 测试限流
        /// </summary>
        /// <returns></returns>
        [TypeFilter(typeof(RateLimitFilter))]
        [HttpPost]
        public string TestLimite()
        {
            return "请求成功";
        }

运行结果:

2. 自动事务案例

(1). 需求

  针对控制器中的action,存在多个saveChange的情况自动开启事务

(事务的知识参考:https://www.cnblogs.com/yaopengfei/p/11387935.html)

(2). 解决思路

  A. 这里采用的是TransactionScope事务,  原理:TransactionScope实现了IDisposable接口,如果一个TransactionScope的对象没有调用Complete(),就执行了Dispose()方法,则事务会被回滚,否则事务就会被提交。也就是说只有执行了 Complete()方法,事务才会提交。

  TransactionScope还支持嵌套式事务

  B. 首先先判断有没有 noTrans 的标签,有的话则跨过,不开启事务

  C. 如果没有 noTrans 标签, 则在 action 执行之前开启事务 → 执行action业务 await next() → 拿到返回值看是否有异常

    a. 如果没有异常,则 complete提交

    b. 有异常则无需操作,走完using,就相当于Dispose了,就回滚了

(3). 测试

   这里仅仅演示,不注册全局了,仅仅加在一个action上进行测试[TypeFilter(typeof(TransactionScopeFilter))] ,两个savechange,然后throw new Exception(); 模拟是否出错

过滤器代码

    /// <summary>
    /// 表示不开启事务的特性
    /// </summary>
    [AttributeUsage(AttributeTargets.Method)]
    public class NoTransAttribute:Attribute
    {
    }
    /// <summary>
    /// 自动开启事务
    /// </summary>
    public class TransactionScopeFilter : IAsyncActionFilter
    {
        public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
        {
            bool hasNoTransAttribute = false;  //表示没有NoTrans特性, 即开始事务
            if (context.ActionDescriptor is ControllerActionDescriptor)
            {
                var actionDesc = (ControllerActionDescriptor)context.ActionDescriptor;
                hasNoTransAttribute = actionDesc.MethodInfo.IsDefined(typeof(NoTransAttribute));
            }
            if (hasNoTransAttribute)
            {
                //表示不开启事务, 执行action中的业务,并且return; 不走该过滤器后面的代码
                await next();
                return;
            }
            using var txTransScope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled);
            var result = await next();
            if (result.Exception == null)
            {
                txTransScope.Complete();
            }
        }
    }

测试代码

        /// <summary>
        /// 测试自动事务
        /// </summary>
        /// <returns></returns>
        /// <exception cref="Exception"></exception>
        [TypeFilter(typeof(TransactionScopeFilter))]
        [HttpPost]
        public async Task TestTransAuto()
        {
            dbCtx.Books.Add(new Book { Id = Guid.NewGuid(), Name = "1", Price = 1 });
            await dbCtx.SaveChangesAsync();
            dbCtx.Books.Add(new Book { Id = Guid.NewGuid(), Name = "2", Price = 2 });
            await dbCtx.SaveChangesAsync();
            throw new Exception();
        }

 

四. 中间件复习

 (参考之前文章:https://www.cnblogs.com/yaopengfei/p/10764553.html)

1. 什么是中间件?

   这里的中间件指的是Asp.net core中的一系列组件

   中间件由前逻辑、next、后逻辑3部分组成,前逻辑为第一段要执行的逻辑代码、next为指向下一个中间件的调用、后逻辑为从下一个中间件执行返回所执行的逻辑代码。

每个HTTP请求都要经历一系列中间件的处理,每个中间件对于请求进行特定的处理后,再转到下一个中间件,最终的业务逻辑代码执行完成后,响应的内容也会按照处理的相反顺序进行处理,然后形成HTTP响应报文返回给客户端。

  中间件组成一个管道,整个ASP.NET Core的执行过程就是HTTP请求和响应按照中间件组装的顺序在中间件之间流转的过程。开发人员可以对组成管道的中间件按照需要进行自由组合

 

2. Asp.net Core 内置中间件的执行顺序

   异常/错误处理→ HTTP 严格传输安全协议→HTTPS 重定向→静态文件服务器→Cookie 策略实施→身份验证→会话→MVC。

特别说明:

  (1).UseStaticFiles 要在 UseCors之前调用。

  (2).UseCors、UseAuthentication 和 UseAuthorization 必须按这个的顺序依次显示。

  (3).UseCors 当前必须在 UseResponseCaching 之前出现

 3. 常见中间件

  (1).HTTP 严格传输安全协议 (HSTS) 中间件 (UseHsts) 添加 Strict-Transport-Security 标头。

  (2).HTTPS 重定向中间件 (UseHttpsRedirection) 将 HTTP 请求重定向到 HTTPS。

  (3).静态文件中间件 (UseStaticFiles) 返回静态文件,并简化进一步请求处理。

  (4).Cookie 策略中间件 (UseCookiePolicy) 使应用符合欧盟一般数据保护条例 (GDPR) 规定。

  (5).用于路由请求的路由中间件 (UseRouting)。

  (6).身份验证中间件 (UseAuthentication) 尝试对用户进行身份验证,然后才会允许用户访问安全资源。【比如能否登录】

  (7).用于授权用户访问安全资源的授权中间件 (UseAuthorization)。【比如是否具有这个权限】

  (8).会话中间件 (UseSession) 建立和维护会话状态。 如果应用使用会话状态,请在 Cookie 策略中间件之后和 MVC 中间件之前调用会话中间件。

  (9).用于将 Razor Pages 终结点添加到请求管道的终结点路由中间件(带有 MapRazorPages 的 UseEndpoints)。

4. 三个常用方法

  (1). Use方法:可以将多个中间件连接在一起,实现在下一个中间件的前后执行操作,Use方法也可以使管道短路,即不调用next请求委托,此时和Run的作用一样.

  (2). Run方法:是一种约定,在某些中间件的可公开的管道末尾运行Run[Middleware]方法,表示终止请求.

  (3). Map和MapWhen方法:  当请求满足某种规则,执行某个中间件

5. 中间件类

   中间件类是一个普通的.NET类,它不需要继承任何父类或者实现任何接口,但是这个类需要有一个构造方法,构造方法至少要有一个RequestDelegate类型的参数,这个参数用来指向下一个中间件。

   这个类还需要定义一个名字为Invoke或InvokeAsync的方法,方法至少有一个HttpContext类型的参数,方法的返回值必须是Task类型。

   中间件类的构造方法和Invoke(或InvokeAsync)方法还可以定义其他参数,其他参数的值会通过依赖注入自动赋值。

6. 中间件和过滤器的区别

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

 (1)中间件可以处理所有的请求,而Filter只能处理对控制器的请求;中间件运行在一个更底层、更抽象的级别,因此在中间件中无法处理MVC中间件特有的概念。

 (2)中间件和Filter可以完成很多相似的功能。“未处理异常中间件”和“未处理异常Filter”;“请求限流中间件”和“请求限流Filter”均可以实现。

 (3)优先选择使用中间件;但是如果这个组件只针对MVC或者需要调用一些MVC相关的类的时候,我们就只能选择Filter。

 

五. 中间件实操

1. 操作:

   使用use方法、run方法、和将中间件封装成类调用  app.UseMiddleware<myMiddleware>();

中间件代码

 public class myMiddleware
    {
		private readonly RequestDelegate next;
		public myMiddleware(RequestDelegate next)
		{
			this.next = next;
		}
		public async Task InvokeAsync(HttpContext context)
		{
			await context.Response.WriteAsync("MyOwinMiddle Start<br/>");
			await next(context);
			await context.Response.WriteAsync("MyOwinMiddle End<br/>");
		}
	}

program代码

//下面区域测试中间件
{

    app.Use(async (context, next) =>
    {
        await context.Response.WriteAsync("First  Start<br/>");
        //调用下一个中间件
        await next.Invoke();
        await context.Response.WriteAsync("First End<br/>");
    });
    app.Use(async (context, next) =>
    {
        await context.Response.WriteAsync("Second Start<br/>");
        //调用下一个中间件
        await next.Invoke();
        await context.Response.WriteAsync("Second End<br/>");
    });
    //调用封装后的中间件
    app.UseMiddleware<myMiddleware>();

    //没有调用next,它在此处的作用是使管道短路
    app.Run(async context =>
    {
        await context.Response.WriteAsync("third  Start<br/>");
        await context.Response.WriteAsync("third  End<br/>");
    });

}
2. 运行结果
  First  Start<br/>Second Start<br/>MyOwinMiddle Start<br/>third  Start<br/>third  End<br/>MyOwinMiddle End<br/>Second End<br/>First End<br/>
 
 
 
 
 
 
 
 
 
 
 
 
 
 

!

  • 作       者 : Yaopengfei(姚鹏飞)
  • 博客地址 : http://www.cnblogs.com/yaopengfei/
  • 声     明1 : 如有错误,欢迎讨论,请勿谩骂^_^。
  • 声     明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。
 
posted @ 2022-05-25 16:06  Yaopengfei  阅读(442)  评论(1编辑  收藏  举报