• 跟我一起学.NetCore之中间件(Middleware)应用和自定义


    前言

    Asp.NetCore中的请求管道是通过一系列的中间件组成的,使得请求会根据需求进行对应的过滤和加工处理。在平时开发中会时常引用别人定义好的中间件,只需简单进行app.Usexxx就能完成中间件的注册,但是对于一些定制化需求还得自己进行处理和封装,以下说说中间件的注册应用和自定义中间件;

    正文

    在上一小节中有简单提到,当注册第三方封装的中间件时,其实本质还是调用了IApplicationBuilder的Use方法;而在开发过程中,会使用以下三种方式进行中间件的注册:

    • Use:通过Use的方式注册中间件,可以控制是否将请求传递到下一个中间件;
    • Run:通过Run的方式注册中间件,一般用于断路或请求管道末尾,即不会将请求传递下去;
    • Map/MapWhen:请求管道中增加分支,条件满足之后就由分支管道进行处理,而不会切换回主管道;Map用于请求路径匹配,而MapWhen可以有更多的条件进行过滤;
    • UseMiddleWare : 一般用于注册自定义封装的中间件,内部其实是使用Use的方式进行中间件注册;

    相信都知道我的套路了,光说不练假把式,来一个Asp.NetCore API项目进行以上几种中间件注册方式演示:

    img

    图中代码部分将原先默认注册的中间件删除了,用Use和Run的方式分别注册了两个中间件(这里只是简单的显示文字,里面可以根据需求添加相关逻辑),其中用Use注册的方式在上一节中已经提及到,直接将中间件添加链表中,这里就不再赘述了;

    对于使用Run方式注册中间,小伙伴们肯定不甘心止于此吧,所以这里直接看Run是如何实现:

    namespace Microsoft.AspNetCore.Builder
    {
        public static class RunExtensions
        {
            // 也是一个扩展方法,但参数就是一个委托
            public static void Run(this IApplicationBuilder app, RequestDelegate handler)
            {
                // 参数校验,如果null就抛出异常
                if (app == null)
                {
                    throw new ArgumentNullException(nameof(app));
                }
                // 传入的委托校验,如果null也是抛出异常
                if (handler == null)
                {
                    throw new ArgumentNullException(nameof(handler));
                }
                // 这里其实只有一个 RequestDelegate执行逻辑,并没有传递功能
                // 本质也是使用方法Use
                app.Use(_ => handler);
            }
        }
    }
    

    通过代码可知,用Run方式只是将处理逻辑RequestDelegate传入,并没有传递的逻辑,所以Run注册的中间件就会形成断路,导致后面的中间件不能再执行了;

    使用Map和MapWhen注册的方式,其实是给管道开一个分支,就像高速公路一样,有匝道,到了对应出口就进匝道了,就不能倒车回来了(倒回来扣你12分);同样,请求管道也是,当条件满足时,请求就走Map对应的分支管道,就不能重新返回主管道了;

    img

    代码走一波,在注册中间件的地方增加Map的使用:

    img

    Configure全部代码如下:

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        // 使用Use注册中间 
        app.Use(async (context, next) => {
            await context.Response.WriteAsync("Hello Use1
    ");
            // 将请求传递到下一个中间件
            await next();
    ​
            await context.Response.WriteAsync("Hello Use1 Response
    ");
        });
    ​
        // 使用Use注册中间   参数类型不一样
        app.Use(requestDelegate =>
        {
            return async (context) =>
            {
                await context.Response.WriteAsync("Hello Use2
    ");
                // 将请求传递到下一个中间件
                await requestDelegate(context);
    ​
                await context.Response.WriteAsync("Hello Use2 Response
    ");
            };
        });
        // 分支管道,只有匹配到路径才走分支管道
        app.Map("/Hello", builder =>
        {
            builder.Use(async (context, next) =>
            {
                await context.Response.WriteAsync("Hello MapUse
    ");
                // 将请求传递到分支管道的下一个中间件
                await next();
    ​
                await context.Response.WriteAsync("Hello MapUse Response
    ");
            });
            // 注册分支管道中间件
            builder.Run(async context => {
                await context.Response.WriteAsync("Hello MapRun1~~~
    ");
            });
            // 注册分支管道中间件
            builder.Run(async context => {
                await context.Response.WriteAsync("Hello MapRun2~~~
    ");
            });
        });
    ​
         // 使用Run 
         app.Run(async context => {
             await context.Response.WriteAsync("Hello Run~~~
    ");
         });
    ​
         //使用Run注册
         app.Run(async context => {
             await context.Response.WriteAsync("Hello Code综艺圈~~~
    ");
         });
    }
    

    执行看效果:

    img

    Map方式注册的分支管道只有路径匹配了才走,否则都会走主管道;

    仔细的小伙伴肯定会说,那是在分支管道上用了Run注册中间件了,形成了断路,所以导致不能执行主管道剩下的中间件,好,那我们稍微改改代码:

    img

    这样运行访问分支管道时会报错,因为分支管道中没有下一个中间件了,还调用下一个中间件,那肯定有问题;

    img

    改了改,如下运行:

    img

    进入匝道还想倒回来,12分不要了吗,哈哈哈;

    MapWhen注册的分支管道逻辑和Map差不多类似,只是匹配的条件更加灵活而已,可以根据自己需求进行调节匹配,如下:

    img

    看到这,小伙伴应该都知道,接下来肯定不会放过Map/MapWhen的实现:

    • Map

      public static IApplicationBuilder Map(this IApplicationBuilder app, PathString pathMatch, Action<IApplicationBuilder> configuration)
      {
          // 进行参数校验 IApplicationBuilder对象不能为空
          if (app == null)
          {
              throw new ArgumentNullException(nameof(app));
          }
          // 进行参数校验 传进的委托对象不能为空
          if (configuration == null)
          {
              throw new ArgumentNullException(nameof(configuration));
          }
          // 匹配的路径末尾不能有"/",否则就抛异常
          if (pathMatch.HasValue && pathMatch.Value.EndsWith("/", StringComparison.Ordinal))
          {
              throw new ArgumentException("The path must not end with a '/'", nameof(pathMatch));
          }
      ​
          // 克隆一个IApplicationBuilder,共用之前的属性,这里其实创建了分支管道
          var branchBuilder = app.New();
          // 将创建出来的branchBuilder进行相关配置
          configuration(branchBuilder);
          // 构造出分支管道
          var branch = branchBuilder.Build();
          // 将构造出来的管道和匹配路径进行封装
          var options = new MapOptions
          {
              Branch = branch,
              PathMatch = pathMatch,
          };
          // 注册中间件
          return app.Use(next => new MapMiddleware(next, options).Invoke);
      }
      ​
      // MapMiddleware 的Invoke方法,及如何进入分支管道处理的
      public async Task Invoke(HttpContext context)
      {
          // 参数判断
          if (context == null)
          {
              throw new ArgumentNullException(nameof(context));
          }
      ​
          PathString matchedPath;
          PathString remainingPath;
      ​
          // 判断是否匹配路径,如果匹配上就进入分支
          if (context.Request.Path.StartsWithSegments(_options.PathMatch, out matchedPath, out remainingPath))
          {
              // 更新请求地址
              var path = context.Request.Path;
              var pathBase = context.Request.PathBase;
              context.Request.PathBase = pathBase.Add(matchedPath);
              context.Request.Path = remainingPath;
      ​
              try
              {
                  // 进入分支管道
                  await _options.Branch(context);
              }
              finally
              {
                  // 恢复原先请求地址,回到主管道之后,并没有进行主管道也下一个中间件的传递,所以主管道后续不在执行
                  context.Request.PathBase = pathBase;
                  context.Request.Path = path;
              }
          }
          else
          {
              // 匹配不到路径就继续主管道执行
              await _next(context);
          }
      }
      
    • MapWhen:其实和Map差不多,只是传入的匹配规则不一样,比较灵活:

      public static IApplicationBuilder MapWhen(this IApplicationBuilder app, Predicate predicate, Action<IApplicationBuilder> configuration)
      {
          if (app == null)
          {
              throw new ArgumentNullException(nameof(app));
          }
      ​
          if (predicate == null)
          {
              throw new ArgumentNullException(nameof(predicate));
          }
      ​
          if (configuration == null)
          {
              throw new ArgumentNullException(nameof(configuration));
          }
      ​
          // 构建分支管道,和Map一致
          var branchBuilder = app.New();
          configuration(branchBuilder);
          var branch = branchBuilder.Build();
      ​
          // 封装匹配规则
          var options = new MapWhenOptions
          {
              Predicate = predicate,
              Branch = branch,
          };
          // 注册中间件
          return app.Use(next => new MapWhenMiddleware(next, options).Invoke);
      }
      // MapWhenMiddleware 的Invoke方法
      public async Task Invoke(HttpContext context)
      {
          // 参数校验
          if (context == null)
          {
              throw new ArgumentNullException(nameof(context));
          }
          // 判断是否匹配规则,如果匹配就进入分支管道
          if (_options.Predicate(context))
          {
              await _options.Branch(context);
          }
          else
          {
              // 没有匹配就继续执行主管道
              await _next(context);
          }
      }
      

    现在是不是清晰明了多了,不懵了吧;还没完呢,继续往下;

    上面注册中间件的方式是不是有点不那么好看,当中间件多了时候,可读性很是头疼,维护性也得花点功夫,所以微软肯定想到这了,提供了类的方式进行中间件的封装(但是要按照约定来),从而可以像使用第三方中间件那样简单,如下:

    img

    使用及运行:

    img

    是不是自定义也没想象中那么难,其中注册封装的中间件时,在扩展方法中使用了app.UseMiddleware()进行注册,这个上一节中提到过,就是那段在上一节中有点嫌早的代码,这里就拷过来了(偷个懒):

    // 看着调用的方法
    public static IApplicationBuilder UseMiddleware<TMiddleware>(this IApplicationBuilder app, params object[] args)
    {
        // 内部调用了以下方法
        return app.UseMiddleware(typeof(TMiddleware), args);
    }
    // 其实这里是对自定义中间件的注册,这里可以不用太深入了解
    public static IApplicationBuilder UseMiddleware(this IApplicationBuilder app, Type middleware, params object[] args)
    {
        if (typeof(IMiddleware).GetTypeInfo().IsAssignableFrom(middleware.GetTypeInfo()))
        {
            // IMiddleware doesn't support passing args directly since it's
            // activated from the container
            if (args.Length > 0)
            {
                throw new NotSupportedException(Resources.FormatException_UseMiddlewareExplicitArgumentsNotSupported(typeof(IMiddleware)));
            }
    ​
            return UseMiddlewareInterface(app, middleware);
        }
        // 取得容器
        var applicationServices = app.ApplicationServices;
        // 反编译进行包装成注册中间件的样子(Func<ReuqestDelegate,RequestDelegate>),但可以看到本质使用IApplicationBuilder中Use方法
        return app.Use(next =>
        {
            // 获取指定类型中的方法列表
            var methods = middleware.GetMethods(BindingFlags.Instance | BindingFlags.Public);
            // 找出名字是Invoke或是InvokeAsync的方法
            var invokeMethods = methods.Where(m =>
                string.Equals(m.Name, InvokeMethodName, StringComparison.Ordinal)
                || string.Equals(m.Name, InvokeAsyncMethodName, StringComparison.Ordinal)
                ).ToArray();
            // 如果有多个方法 ,就抛出异常,这里保证方法的唯一
            if (invokeMethods.Length > 1)
            {
                throw new InvalidOperationException(Resources.FormatException_UseMiddleMutlipleInvokes(InvokeMethodName, InvokeAsyncMethodName));
            }
            // 如果没有找到,也就抛出异常
            if (invokeMethods.Length == 0)
            {
                throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNoInvokeMethod(InvokeMethodName, InvokeAsyncMethodName, middleware));
            }
            // 取得唯一的方法Invoke或是InvokeAsync方法
            var methodInfo = invokeMethods[0];
            // 判断类型是否返回Task,如果不是就抛出异常,要求返回Task的目的是为了后续包装RequestDelegate
            if (!typeof(Task).IsAssignableFrom(methodInfo.ReturnType))
            {
                throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNonTaskReturnType(InvokeMethodName, InvokeAsyncMethodName, nameof(Task)));
            }
            // 判断方法的参数,参数的第一个参数必须是HttpContext类型
            var parameters = methodInfo.GetParameters();
            if (parameters.Length == 0 || parameters[0].ParameterType != typeof(HttpContext))
            {
                throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNoParameters(InvokeMethodName, InvokeAsyncMethodName, nameof(HttpContext)));
            }
    ​
            // 开始构造RequestDelegate对象
            var ctorArgs = new object[args.Length + 1];
            ctorArgs[0] = next;
            Array.Copy(args, 0, ctorArgs, 1, args.Length);
            // 这里找到参数匹配最多的构造函数进行实例创建
            var instance = ActivatorUtilities.CreateInstance(app.ApplicationServices, middleware, ctorArgs);
            // 如果参数只有一个HttpContext 就包装成一个RequestDelegate返回
            if (parameters.Length == 1)
            {
                return (RequestDelegate)methodInfo.CreateDelegate(typeof(RequestDelegate), instance);
            }
            // 如果参数有多个的情况就单独处理,这里不详细进去了
            var factory = Compile<object>(methodInfo, parameters);
    ​
            return context =>
            {
                var serviceProvider = context.RequestServices ?? applicationServices;
                if (serviceProvider == null)
                {
                    throw new InvalidOperationException(Resources.FormatException_UseMiddlewareIServiceProviderNotAvailable(nameof(IServiceProvider)));
                }
    ​
                return factory(instance, context, serviceProvider);
            };
        });
    }
    

    可以看出,框架将我们封装的中间件类进行了反射获取对应的方法和属性,然后封装成中间件(Func<RequestDelegate,RequestDelegate>)的样子,从而是得编码更加方便,中间件更容易分类管理了;通过以上代码注释也能看出在封装中间件的时候对应的约定,哈哈哈,是不是得重新看一遍代码(如果这样,目标达到了);对了,框架提供了IMiddleware了接口,实现中间件的时候可以实现,但是约定还是一个不能少;

    总结

    我去,不能熬了,再熬明天起不来跑步了;这篇内容有点多,之所以没分开,感觉关联性比较强,一口气看下来比较合适;下一节说说文件相关的点;

    ---------------------------------------------------

    CSDN:Code综艺圈

    知乎:Code综艺圈

    掘金:Code综艺圈

    博客园:Code综艺圈

    bilibili:Code综艺圈

    ---------------------------------------------------

    一个被程序搞丑的帅小伙,关注"Code综艺圈",识别关注跟我一起学~~~

    img

    撸文不易,莫要白瞟,三连走起~~~~

  • 相关阅读:
    Flask入门到精通(二)
    MySQL安装配置,命令,异常纪要
    JQuery 选择器
    redhat Enterprise Linux Server release 7.2(Maipo) 安装redis-stat
    pssh 不能执行指定用户命令
    VMware 命令行下安装以及导入Ubuntu系统
    Linux CPU相关信息查看
    Ubuntu 16.04 Mxnet CPU 版本安装
    Ubuntu 16.04 TensorFlow CPU 版本安装
    <转>揭秘DNS后台文件:DNS系列之五
  • 原文地址:https://www.cnblogs.com/zoe-zyq/p/13631614.html
Copyright © 2020-2023  润新知