中间件

管道与中间件

管道由中间件组成,可以想象成管道就是一个产品线的处理流程模板,中间件就是这个流程上需要对一件产品做的处理,而这个产品就是我们的请求,当我们的请求进入管道的时候,会按照中间件的顺序对于请求做处理,然后选择是否传至下一个中间件进行处理,直到到达管道末尾,然后返回结果。

中间件

中间件作用:

微软官网原话:

中间件是一种装配到应用管道以处理请求和响应的软件。 每个组件

  • 选择是否将请求传递到管道中的下一个组件。
  • 可在管道中的下一个组件前后执行工作。

请求委托用于生成请求管道。 请求委托处理每个 HTTP 请求。

使用RunMap或者Use拓展方法来配置请求委托,也可将一个单独的请求委托并行指定为匿名方法(称为并行中间件),或在可重用的类中对其进行定义。 这些可重用的类和并行匿名方法即为中间件,也叫中间件组件。 请求管道中的每个中间件组件负责调用管道中的下一个组件,或使管道短路。 当中间件短路时,它被称为"终端中间件",因为它阻止中间件进一步处理请求。

简单来说,我们的请求处理管道其实是由一个个中间件组成的,每一个中间件都是一个处理请求和响应的组件(或者说委托,也就是RequestDelegate),每一个中间件处理完成后,会把HttpContext上下文(next)传递到下一个组件,直到到达终端中间件,终端中间件执行完成后,会原路返回。

中间件短路:

中间件短路是指在这个中间件处理中,不会把HttpContext上下文传递给下一个组件,比如app.Run()就是一个终端中间件,他不会收到next,无论你自己定义几个app.Run(),永远在到达第一个终端中间件后,立即短路,不会调用后续的中间件。

常见中间件及执行顺序

中间件一般摆放的顺序即完整请求处理管道:

  1. ExceptionHandler:异常处理
  2. UseHSTS:https
  3. UseHttpsRedirection:重定向
  4. UseStaticFiles:静态文件
  5. UseRouting:路由,因为6.0,应用继承了
  6. UseCORS:跨域
  7. UseAuthentication:身份认证
  8. UseAuthorization:鉴权
  9. 终端中间件

并非所有中间件都完全按照此顺序出现,但许多中间件都会遵循此顺序。 例如

  • UseCorsUseAuthenticationUseAuthorization 必须按显示的顺序出现。
  • UseCors 当前必须在 UseResponseCaching 之前出现。
  • UseRequestLocalization 必须在可能检查请求区域性的任何中间件(例如 app.UseMvcWithDefaultRoute())之前出现。
  • 如果显示调用了UseRouting(),则后面必须紧跟UseEndpoint()中间件
  • 应尽早在管道中调用异常处理委托,这样它们就能捕获在管道的后期阶段发生的异常。

想要了解更多内置中间件,官网(ASP.NET Core 中间件 | Microsoft Learn)

中间件的顺序对于安全性、性能和功能至关重要。

在我们的Program.cs文件中,中间件执行的顺序就是以app.UseXXX()拓展方法从上至下执行,即Request请求,在到达终端中间件后,按原路返回,即从下至上执行则是Respones响应。所以每个中间件摆放的位置和,会影响http请求执行的顺序。

举个例子,一般我们使用 app.UseStaticFiles()静态文件中间件会放在app.UseAuthentication()身份认证中间件前面,这样代表是公开访问。所以当请求需要在访问一些系统的一些静态文件的时候不会被拦截,比如系统的favicon图标、js文件等。

中间件运行原理

Fun<RequestDelegate,RequestDelegate>委托

单纯看类型就知道这是一个委托,传入下一个RequestDelegate委托、返回当前RequestDelegate委托处理的结果。

RequestDelegate委托

微软的官方解释是

一个可以处理 HTTP 请求的函数。

在UseMiddleware()拓展方法中,我们自定义的中间件会被包装成为这个委托添加到管道中。

RequestDelegate源码

C# 复制代码
using System.Threading.Tasks;

namespace Microsoft.AspNetCore.Http
{
    /// <summary>
    /// A一个可以处理 HTTP 请求的函数。
    /// </summary>
    /// <param name="context">The <see cref="HttpContext"/> 请求</param>
    /// <returns>表示请求处理完成的任务</returns>
    public delegate Task RequestDelegate(HttpContext context);
}

IApplicationBuilder接口

通过IApplicationBuilder接口,注册中间件,然后会在app.Run()的时候调用WebApplication.BuildRequestDelegation()方法(这个方法本质上就是调用的ApplicationBuilder.Builder()方法)把注册的中间件串联一起来作为请求管道。

这个接口有一个实现类ApplicationBuilder。

我们在应用的章节就知道了WebApplication类继承了这个接口,所以我们可以直接在应用上注册中间件。

关键的属性和方法

  • 有一个存储请求中间件的列表
  • 有一个Use()方法用于把中间件添加到列表中
  • 有一个New()方法用来创造ApplicationBuilder,也就是管道分支
  • 有一个Builder()方法用于执行添加的中间件,并返回处理好的结果

ApplicationBuilder实现类源码

c# 复制代码
public class ApplicationBuilder : IApplicationBuilder
{
    // 用于存储委托的列表
    private readonly List<Func<RequestDelegate, RequestDelegate>> _components = new();
    // 从ApplicationBuilder获取一组属性
    public IDictionary<string, object?> Properties { get; }
    // 添加委托到管道中
    public IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware)
    {
        _components.Add(middleware);
        return this;
    }
     // 创造当前的管道,也是管道分支。
     public IApplicationBuilder New()
     {
         return new ApplicationBuilder(this);
     }    
     // 用于执行添加的中间件,并返回处理好的结果,WebApplication.BuildRequestDelegation()本质上就是调用的这个方法来创建中间件委托
     public RequestDelegate Build()
        {
            // 注册一个中间件,默认请求上下文的结果是404
            RequestDelegate app = context =>
            {
               // 如果我们到达管道的尽头,但我们有一个端点,那么就会发生一些意想不到的事情。
               // 如果用户代码设置了终结点,但他们忘记添加 UseEndpoint 中间件,则可能会发生这种情况。
                var endpoint = context.GetEndpoint();
                var endpointRequestDelegate = endpoint?.RequestDelegate;
                // 判断有无设置终结点,如果没有设置终结点,则报错
                if (endpointRequestDelegate != null)
                {
                    var message =
                        $"The request reached the end of the pipeline without executing the endpoint: '{endpoint!.DisplayName}'. " +
                        $"Please register the EndpointMiddleware using '{nameof(IApplicationBuilder)}.UseEndpoints(...)' if using " +
                        $"routing.";
                    throw new InvalidOperationException(message);
                }
                // 默认先设置请求的上下文的状态是404
                context.Response.StatusCode = StatusCodes.Status404NotFound;
                return Task.CompletedTask;
            };
            // 这里循环需要注意的是。执行从列表索引的最后一个开始循环的,也就是默认把委托列表倒序执行,所以这里最先开始执行的是我们注册的最后一个中间件
            // 第一次的时候,先把最上面的定义的那个404中间件传入进去,获取最后一个中间件的结果。
            // 第二次的时候,把最后一个中间件的结果,放到倒数第二个中间件中,并获得执行的结果。
            // 第三次的时候,把倒数二个中间件的结果,放到倒数第三个中间件中,并获得结果。
            // 直到执行到第一个中间件。最终返回注册的第一个中间件的返回值。
            for (var c = _components.Count - 1; c >= 0; c--)
            {
                app = _components[c](app);
            }

            return app;
        }
}

自定义中间件

有2种方式:

  1. 继承IMiddleware接口需要满足的条件:

    注册实现的中间件类,必须通过依赖注入到容器中,否则会报错,原因是,在UseMiddleware()的时候,这种方式实现的中间件类,会调用内部的UseMiddlewareInterface()从依赖注入容器中获取服务实例。

  2. 自定义中间件需要满足的条件:

    必须是public修饰,含有一个参数为RequestDelegate类型的构造函数,

    必须包含一个public修饰的,名称为Invoke或者InvokeAsync的实例异步方法,且返回类型为Task,包含一个类型为HttpContext的参数。

    无需注入,会通过反射创造实例.

自定义容错中间件demo

C# 复制代码
// 这个自定义中间件的作用是,当我们的程序在请求过程中抛出了一些内部异常,提示500,则可以捕捉并进行友好捕捉返回。
public class ErrorMiddleware
{
  public readonly RequestDelegate _next;
  // 构造函数会尝试从依赖注入容器中获取服务
  public ErrorMiddleware (RequestDelegate next)
  {
     _next = next;
  }
    
  public async Task InvokeAsync(HttpContext context)
  {
      try
      {
          // 把请求传递给下一个中间件
          awite _next.Invoke(context);         
      }
      catch(Exception ex)
      {
          // 这里可以执行记录日志等操作
          // 日志记录方法。。。。。
          
          // 友好包装
          // 调用ConfigureAwait,可以在异步执行的时候不捕获上下文,意思是如果你的异步方法后不涉及上下文操作,调用这个可以减少性能开销
             await WriteExceptionAsync(context, ex).ConfigureAwait(false);
          
      }      
  }
      /// <summary>
        /// 友好返回处理
        /// </summary>
        /// <param name="context"></param>
        /// <param name="exception"></param>
        /// <returns></returns>
    private async Task WriteExceptionAsync((HttpContext context, Exception exception)
    {
        if (exception.IsNull()) return;
        // 自定义友好返回类
        ServiceResult result = new ServiceResult();
         // 返回友好的提示
            HttpResponse response = context.Response;
            response.ContentType = context.Request.Headers["Accept"];
            response.ContentType = "application/json";
            result.IsFailure(exception.Message);   
        // 写入返回信息中
            await response.WriteAsync(result.ToJson<ServiceResult>()).ConfigureAwait(false);
    }
}

注册自定义中间件

因为定义的中间件有2种方式,一个是通过继承IMiddlewar接口实现,一个是按照约定格式实现的。他们都是通过使用的ApplicationBuilder的拓展方法UseMiddleware()方法来把中间件注册到管道中。

不过在UseMiddleware()方法内部实现其实是2种:

  • 通过接口实现的,会调用私有的UseMiddlewareInterface()方法。
  • 按照约定格式实现的,调用UseMiddleware()方法。

他们的区别就在

  • UseMiddlewareInterface()方法会从依赖注入容器中获取服务实例(毕竟是通过接口实现的)然后调用InvokeAsync()方法,
  • UseMiddleware()则是通过反射创造中间件委托表达式,然后调用执行返回。不过无论是接口实现还是约定实现,都可以调用UseMiddleware()方法来把中间件注册到管道中。

注意!!!!!

  • 如果是手写中间件,则Invoke()方法的参数不要带requestdelegation。否则会产生依赖注入容器报错,
  • 如果继承IMiddleware,内部会调用UseMiddlewareInterface()从依赖注入容器中获取实例,如果使用了AutoFac替换掉了依赖注入容器,那么就需要手动注册,

UseMiddleware

调用中间件

C# 复制代码
 app.UseMiddleware<ErrorMiddleware>();

UseMiddleware源码

c# 复制代码
public static class UseMiddlewareExtensions
{
    // 这里就是为什么即使没有继承IMiddleware接口,方法可以定义为Invoke的原因。
    internal const string InvokeMethodName = "Invoke";
    internal const string InvokeAsyncMethodName = "InvokeAsync";

    private static readonly MethodInfo GetServiceInfo = typeof(UseMiddlewareExtensions).GetMethod(nameof(GetService), BindingFlags.NonPublic | BindingFlags.Static)!;                                                                
    // 我们将把所有公共构造函数和公共方法保留在中间件上
    private const DynamicallyAccessedMemberTypes MiddlewareAccessibility = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicMethods;

       /// <summary>
       /// 添加中间件到管道里面
       /// </summary>
       /// <param name="app">The <see cref="IApplicationBuilder"/> instance.</param>
       /// <param name="middleware">中间件</param>
       /// <param name="args">要传递给中间件类型实例的构造函数的参数。</param>
    public static IApplicationBuilder UseMiddleware<[DynamicallyAccessedMembers(MiddlewareAccessibility)]TMiddleware>(this IApplicationBuilder app, params object?[] args)
    {
        return app.UseMiddleware(typeof(TMiddleware), args);
    }
       /// <summary>
       /// 自定义中间件类,添加中间件到管道里面
       /// </summary>
       /// <param name="app">The <see cref="IApplicationBuilder"/> instance.</param>
       /// <param name="middleware">中间件</param>
       /// <param name="args">要传递给中间件类型实例的构造函数的参数。</param>    
    public static IApplicationBuilder UseMiddleware(this IApplicationBuilder app, [DynamicallyAccessedMembers(MiddlewareAccessibility)] Type middleware, params object?[] args)
    {
        // 如果是继承IMiddleware实现的。
        if (typeof(IMiddleware).IsAssignableFrom(middleware))
        {
            // 如果继承的IMiddleware,不允许从构造函数中传递参数
            if (args.Length > 0)
            {
                throw new NotSupportedException(Resources.FormatException_UseMiddlewareExplicitArgumentsNotSupported(typeof(IMiddleware)));
            }
            // 从下方的UseMiddlewareInterface工厂来生成委托
            return UseMiddlewareInterface(app, middleware);
        }
        // 否则进入自定义中间件的解析流程
        // 获取依赖注入容器
        var applicationServices = app.ApplicationServices;
        // 添加到委托链表中
        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();
                           // 如果定义Invoke或者InvokeAsync超过1个则抛出异常
                           if (invokeMethods.Length > 1)
                           {
                               throw new InvalidOperationException(Resources.FormatException_UseMiddleMutlipleInvokes(InvokeMethodName, InvokeAsyncMethodName));
                           }
                            // 如果没有定义Invoke或者InvokeAsync方法则抛出异常
                           if (invokeMethods.Length == 0)
                           {
                               throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNoInvokeMethod(InvokeMethodName, InvokeAsyncMethodName, middleware));
                           }
                           // 获取定义好的第一个Invoke或者InvokeAsync方法
                           var methodInfo = invokeMethods[0];
                           // 如果这个方法的返回值不是Task则抛出异常
                           if (!typeof(Task).IsAssignableFrom(methodInfo.ReturnType))
                           {
                               throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNonTaskReturnType(InvokeMethodName, InvokeAsyncMethodName, nameof(Task)));
                           }
                           // 获取方法的参数列表
                           var parameters = methodInfo.GetParameters();
                           // 如果没有定义参数,或者第一个参数不是HttpContext则抛出异常
                           if (parameters.Length == 0 || parameters[0].ParameterType != typeof(HttpContext))
                           {
                               throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNoParameters(InvokeMethodName, InvokeAsyncMethodName, nameof(HttpContext)));
                           }
                           // 获取构造函数中传递的参数
                           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);
                           // 如果参数只有一个,(invoker默认的参数必须有一个HttpContext)
                           if (parameters.Length == 1)
                           {  
                               // 则根据该中间件实例创造一个委托
                               return (RequestDelegate)methodInfo.CreateDelegate(typeof(RequestDelegate), instance);
                           }
                           // 如果方法参数超过1个,则代表有自己依赖注入的方法。
                           // 把这个Invoke或者InvokeAsync方法编译成一个可以执行的Lambda表达式委托
                           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);
                           };
                       });
    }
       /// <summary>
       /// 如果继承的是IMiddleware的中间件,则调用的此方法添加中间件到管道里面
       /// </summary>
       /// <param name="app">The <see cref="IApplicationBuilder"/> instance.</param>
       /// <param name="middlewareType">中间件类型</param>
    private static IApplicationBuilder UseMiddlewareInterface(IApplicationBuilder app, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type middlewareType)
    {
        // 往中间件列表中添加一个委托
        return app.Use(next =>
                       {
                           // 解析
                           return async context =>
                           {
                               // 从依赖注入容器中获取IMiddlewareFactory工厂
                               // context.RequestServices.GetService,我们之前在依赖注入章节讲过,调用这个方法其实就是向WebApplication.ServiceProvider获取服务。
                               var middlewareFactory = (IMiddlewareFactory?)context.RequestServices.GetService(typeof(IMiddlewareFactory));
                               // 如果没有实现这个工厂,则报错
                               if (middlewareFactory == null)
                               {
                                   // No middleware factory
                                   throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNoMiddlewareFactory(typeof(IMiddlewareFactory)));
                               }
                               // 根据这个工厂创造中间件的实例
                               var middleware = middlewareFactory.Create(middlewareType);
                               // 创造失败
                               if (middleware == null)
                               {
                                   // 抛出异常
                                   // The factory returned null, it's a broken implementation
                                   throw new InvalidOperationException(Resources.FormatException_UseMiddlewareUnableToCreateMiddleware(middlewareFactory.GetType(), middlewareType));
                               }
                               try
                               {
                                   // 调用实现的InvokeAsync方法
                                   await middleware.InvokeAsync(context, next);
                               }
                               finally
                               {
                                   // 释放中间件
                                   middlewareFactory.Release(middleware);
                               }
                           };
                       });
    }
}

管道分支

什么是管道分支,按照正常的请求,我们每个请求都是走的同一个管道,同一个模板的处理方式,那么如果我们需要对一个请求,比如有一个关于处理用户信息请求,在到了主管道的特定中间件,我需要对于这个请求做一些特殊处理,就可以用到管道分支这个操作。

Map

特点:使用拓展方法app.Map("匹配的路由",处理方法):根据请求路径HttpRequest.Path中的值匹配项来创建请求管道分支。 如果请求路径以给定路径开头,则执行分支。分支内还可以嵌套分支。当请求进入到管道分支后,执行完成不会再回到主管道!!!!,所以必须在处理方法中,调用App.Run()来终结请求。

MapWhen

特点:使用拓展方法app.MapWhen(context => 表达式,处理方法):根据表达式的值来判断是否需要创建管道分支,当请求进入到管道分支后,执行完成不会再回到主管道!!!!,所以必须在处理方法中,调用App.Run()来终结请求。

UseWhen

特点:会回到主管道

请求是如何进入管道、中间件什么时候被执行

相关推荐
RH23121111 小时前
2026.6.8Linux
java·数据库·中间件
理人综艺好会1 天前
双Token机制在实际项目中的应用与实践
中间件·token
番茄去哪了2 天前
神领物流面试题(一)
java·大数据·中间件
念何架构之路2 天前
消息中间件
中间件
都说名字长不会被发现2 天前
Spring Boot Starter 中间件账号密码加密方案设计与实现
java·spring boot·后端·中间件
瀚高PG实验室2 天前
java中间件无法连接数据库
java·数据库·中间件·瀚高数据库
之歆3 天前
Day11_Express 深入解析:从中间件到项目实战
中间件·express
码农飞哥3 天前
RocketMQ消费接口设计实战:为什么HTTP回调接口必须吞掉所有异常,始终返回成功?
网络协议·http·中间件·消息队列·rocketmq
硅谷秋水3 天前
物理人工智能的驾驭工程:机器人中间件是驾驭层
人工智能·机器学习·语言模型·中间件·机器人
初中就开始混世的大魔王3 天前
6 Fast DDS-传输层
开发语言·c++·中间件·信息与通信