13

ASP.NET Core管道详解[4]: 中间件委托链

 3 years ago
source link: https://www.cnblogs.com/artech/p/inside-pipeline-04.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

ASP.NET Core应用默认的请求处理管道是由注册的IServer对象和HostingApplication对象组成的,后者利用一个在创建时提供的RequestDelegate对象来处理IServer对象分发给它的请求。而RequestDelegate对象实际上是由所有的中间件按照注册顺序创建的。换句话说,这个RequestDelegate对象是对中间件委托链的体现。如果将RequestDelegate替换成原始的中间件,那么ASP.NET Core应用的请求处理管道体现为下图所示的形式。[本文节选自《ASP.NET Core 3框架揭秘》第13章, 更多关于ASP.NET Core的文章请点这里]

7

目录
一、IApplicationBuilder
二、弱类型中间件
三、强类型中间件
四、注册中间件

一、IApplicationBuilder

对于一个ASP.NET Core应用来说,它对请求的处理完全体现在注册的中间件上,所以“应用”从某种意义上来讲体现在通过所有注册中间件创建的RequestDelegate对象上。正因为如此,ASP.NET Core框架才将构建这个RequestDelegate对象的接口命名为IApplicationBuilder。IApplicationBuilder是ASP.NET Core框架中的一个核心对象,我们将中间件注册在它上面,并且最终利用它来创建代表中间件委托链的RequestDelegate对象。

如下所示的代码片段是IApplicationBuilder接口的定义。该接口定义了3个属性:ApplicationServices属性代表针对当前应用程序的依赖注入容器,ServerFeatures属性则返回服务器提供的特性集合,Properties属性返回的字典则代表一个可以用来存放任意属性的容器。

public interface IApplicationBuilder
{
    IServiceProvider ApplicationServices { get; set; }
    IFeatureCollection ServerFeatures { get; }
    IDictionary<string, object> Properties { get; }

    IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware);
    RequestDelegate Build();
    IApplicationBuilder New();
}

通过《模拟管道实现》的介绍可知,ASP.NET Core应用的中间件体现为一个Func<RequestDelegate, RequestDelegate>对象,而针对中间件的注册则通过调用IApplicationBuilder接口的Use方法来完成。IApplicationBuilder对象最终的目的就是根据注册的中间件创建作为代表中间件委托链的RequestDelegate对象,这个目标是通过调用Build方法来完成的。New方法可以帮助我们创建一个新的IApplicationBuilder对象,除了已经注册的中间件,创建的IApplicationBuilder对象与当前对象具有相同的状态。

具有如下定义的ApplicationBuilder类型是对IApplicationBuilder接口的默认实现。ApplicationBuilder类型利用一个List<Func<RequestDelegate, RequestDelegate>>对象来保存注册的中间件,所以Use方法只需要将指定的中间件添加到这个列表中即可,而Build方法只需要逆序调用这些注册的中间件对应的Func<RequestDelegate, RequestDelegate>对象就能得到我们需要的RequestDelegate对象。值得注意的是,Build方法会在委托链的尾部添加一个额外的中间件,该中间件会将响应状态码设置为404,所以应用在默认情况下会回复一个404响应。

public class ApplicationBuilder : IApplicationBuilder
{
    private readonly IList<Func<RequestDelegate, RequestDelegate>> middlewares= new List<Func<RequestDelegate, RequestDelegate>>();

    public IDictionary<string, object> Properties { get; }
    public IServiceProvider ApplicationServices
    {
        get { return GetProperty<IServiceProvider>("application.Services"); }
        set { SetProperty<IServiceProvider>("application.Services", value); }
    }

    public IFeatureCollection ServerFeatures
    {
        get { return GetProperty<IFeatureCollection>("server.Features"); }
    }

    public ApplicationBuilder(IServiceProvider serviceProvider)
    {
        Properties = new Dictionary<string, object>();
        ApplicationServices = serviceProvider;
    }

    public ApplicationBuilder(IServiceProvider serviceProvider, object server) : this(serviceProvider)
        => SetProperty("server.Features", server);

    public IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware)
    {
        middlewares.Add(middleware);
        return this;
    }

    public IApplicationBuilder New() => new ApplicationBuilder(this);

    public RequestDelegate Build()
    {
        RequestDelegate app = context =>
        {
            context.Response.StatusCode = 404;
            return Task.FromResult(0);
        };
        foreach (var component in middlewares.Reverse())
        {
            app = component(app);
        }
        return app;
    }

    private ApplicationBuilder(ApplicationBuilder builder) =>Properties = new CopyOnWriteDictionary<string, object>(builder.Properties, StringComparer.Ordinal);
    private T GetProperty<T>(string key)=>Properties.TryGetValue(key, out var value) ? (T)value : default;
    private void SetProperty<T>(string key, T value)=> Properties[key] = value;
}

由上面的代码片段可以看出,不论是通过ApplicationServices属性返回的IServiceProvider对象,还是通过ServerFeatures属性返回的IFeatureCollection对象,它们实际上都保存在通过Properties属性返回的字典对象上。ApplicationBuilder具有两个公共构造函数重载,其中一个构造函数具有一个类型为Object的server参数,但这个参数并不是表示服务器,而是表示服务器提供的IFeatureCollection对象。New方法直接调用私有构造函数创建一个新的ApplicationBuilder对象,属性字典的所有元素会复制到新创建的ApplicationBuilder对象中。

ASP.NET Core框架使用的IApplicationBuilder对象是通过注册的IApplicationBuilderFactory服务创建的。如下面的代码片段所示,IApplicationBuilderFactory接口具有唯一的CreateBuilder方法,它会根据提供的特性集合创建相应的IApplicationBuilder对象。具有如下定义的ApplicationBuilderFactory类型是对该接口的默认实现,前面介绍的ApplicationBuilder对象正是由它创建的。

public interface IApplicationBuilderFactory
{
    IApplicationBuilder CreateBuilder(IFeatureCollection serverFeatures);
}

public class ApplicationBuilderFactory : IApplicationBuilderFactory
{
    private readonly IServiceProvider _serviceProvider;
    public ApplicationBuilderFactory(IServiceProvider serviceProvider) =>_serviceProvider = serviceProvider;
    public IApplicationBuilder CreateBuilder(IFeatureCollection serverFeatures) => new ApplicationBuilder(this._serviceProvider, serverFeatures);
}

二、弱类型中间件

虽然中间件最终体现为一个Func<RequestDelegate, RequestDelegate>对象,但是在大部分情况下我们总是倾向于将中间件定义成一个POCO类型。通过前面介绍可知,中间件类型的定义具有两种形式:一种是按照预定义的约定规则来定义中间件类型,即弱类型中间件;另一种则是直接实现IMiddleware接口,即强类型中间件。下面介绍基于约定的中间件类型的定义方式,这种方式定义的中间件类型需要采用如下约定。

  • 中间件类型需要有一个有效的公共实例构造函数,该构造函数必须包含一个RequestDelegate类型的参数,当前中间件通过执行这个委托对象将请求分发给后续中间件进行处理。这个构造函数不仅可以包含任意其他参数,对参数RequestDelegate出现的位置也不做任何约束。
  • 针对请求的处理实现在返回类型为Task的Invoke方法或者InvokeAsync方法中,该方法的第一个参数表示当前请求对应的HttpContext上下文,对于后续的参数,虽然约定并未对此做限制,但是由于这些参数最终是由依赖注入框架提供的,所以相应的服务注册必须存在。

如下所示的代码片段就是一个典型的按照约定定义的中间件类型。我们在构造函数中注入了一个必需的RequestDelegate对象和一个IFoo服务。在用于请求处理的InvokeAsync方法中,除了包含表示当前HttpContext上下文的参数,我们还注入了一个IBar服务,该方法在完成自身请求处理操作之后,通过构造函数中注入的RequestDelegate对象可以将请求分发给后续的中间件。

public class FoobarMiddleware
{
    private readonly RequestDelegate _next;
    private readonly IFoo _foo;

    public FoobarMiddleware(RequestDelegate next, IFoo foo)
    {
        _next = next;
        _foo = foo;
    }

    public async Task InvokeAsync(HttpContext context, IBar bar)
    {
        ...
        await _next(context);
    }
}

采用上述方式定义的中间件最终是通过调用IApplicationBuilder接口如下所示的两个扩展方法进行注册的。当我们调用这两个方法时,除了指定具体的中间件类型,还可以传入一些必要的参数,它们将作为调用构造函数的输入参数。对于定义在中间件类型构造函数中的参数,如果有对应的服务注册,ASP.NET Core框架在创建中间件实例时可以利用依赖注入框架来提供对应的参数,所以在注册中间件时是不需要提供构造函数的所有参数的。

public static class UseMiddlewareExtensions
{
    public static IApplicationBuilder UseMiddleware<TMiddleware>( this IApplicationBuilder app, params object[] args);
    public static IApplicationBuilder UseMiddleware(this IApplicationBuilder app, Type middleware, params object[] args);
}

由于ASP.NET Core应用的请求处理管道总是采用Func<RequestDelegate, RequestDelegate>对象来表示中间件,所以无论采用什么样的中间件定义方式,注册的中间件总是会转换成一个委托对象。那么上述两个扩展方法是如何实现这样的转换的?为了解决这个问题,我们采用极简的形式自行定义了第二个非泛型的UseMiddleware方法。

public static class UseMiddlewareExtensions
{
    private static readonly MethodInfo GetServiceMethod = typeof(IServiceProvider) .GetMethod("GetService", BindingFlags.Public | BindingFlags.Instance);
    public static IApplicationBuilder UseMiddleware(this IApplicationBuilder app, Type middlewareType, params object[] args)
    {
        ...
        var invokeMethod = middlewareType
            .GetMethods(BindingFlags.Instance | BindingFlags.Public)
            .Where(it => it.Name == "InvokeAsync" || it.Name == "Invoke")
            .Single();
        Func<RequestDelegate, RequestDelegate> middleware = next =>
        {
            var arguments = (object[])Array.CreateInstance(typeof(object), args.Length + 1);
            arguments[0] = next;
            if (args.Length > 0)
            {
                Array.Copy(args, 0, arguments, 1, args.Length);
            }
            var instance = ActivatorUtilities.CreateInstance(app.ApplicationServices,middlewareType, arguments);
            var factory = CreateFactory(invokeMethod);
            return context => factory(instance, context, app.ApplicationServices);
        };

        return app.Use(middleware);
    }

    private static Func<object, HttpContext, IServiceProvider, Task>CreateFactory(MethodInfo invokeMethod)
    {
        var middleware = Expression.Parameter(typeof(object), "middleware");
        var httpContext = Expression.Parameter(typeof(HttpContext), "httpContext");
        var serviceProvider = Expression.Parameter(typeof(IServiceProvider),"serviceProvider");

        var parameters = invokeMethod.GetParameters();
        var arguments = new Expression[parameters.Length];
        arguments[0] = httpContext;
        for (int index = 1; index < parameters.Length; index++)
        {
            var parameterType = parameters[index].ParameterType;
            var type = Expression.Constant(parameterType, typeof(Type));
            var getService = Expression.Call(serviceProvider, GetServiceMethod, type);
            arguments[index] = Expression.Convert(getService, parameterType);
        }
        var converted = Expression.Convert(middleware, invokeMethod.DeclaringType);
        var body = Expression.Call(converted, invokeMethod, arguments);
        var lambda = Expression.Lambda<Func<object, HttpContext, IServiceProvider, Task>>(body, middleware, httpContext, serviceProvider);

        return lambda.Compile();
    }
}

由于请求处理的具体实现定义在中间件类型的Invoke方法或者InvokeAsync方法上,所以注册这样一个中间件需要解决两个核心问题:其一,创建对应的中间件实例;其二,将针对中间件实例的Invoke方法或者InvokeAsync方法调用转换成Func<RequestDelegate, RequestDelegate>对象。由于存在依赖注入框架,所以第一个问题很好解决,从上面给出的代码片段可以看出,我们最终调用静态类型ActivatorUtilities的CreateInstance方法创建出中间件实例。

由于ASP.NET Core框架对中间件类型的Invoke方法和InvokeAsync方法的声明并没有严格限制,该方法返回类型为Task,它的第一个参数为HttpContext上下文,所以针对该方法的调用比较烦琐。要调用某个方法,需要先传入匹配的参数列表,有了IServiceProvider对象的帮助,针对输入参数的初始化就显得非常容易。我们只需要从表示方法的MethodInfo对象中解析出方法的参数类型,就能够根据类型从IServiceProvider对象中得到对应的参数实例。

如果有表示目标方法的MethodInfo对象和与之匹配的输入参数列表,就可以采用反射的方式来调用对应的方法,但是反射并不是一种高效的手段,所以ASP.NET Core框架采用表达式树的方式来实现针对InvokeAsync方法或者Invoke方法的调用。基于表达式树针对中间件实例的InvokeAsync方法或者Invoke方法的调用实现在前面提供的CreateFactory方法中,由于实现逻辑并不复杂,所以不需要再对提供的代码做详细说明。

三、强类型中间件

通过调用IApplicationBuilder接口的UseMiddleware扩展方法注册的是一个按照约定规则定义的中间件类型,由于中间件实例是在应用初始化时创建的,这样的中间件实际上是一个与当前应用程序具有相同生命周期的Singleton对象。但有时我们希望中间件对象采用Scoped模式的生命周期,即要求中间件对象在开始处理请求时被创建,在完成请求处理后被回收释放。

如果需要后面这种类型的中间件,就需要让定义的中间件类型实现IMiddleware接口。如下面的代码片段所示,IMiddleware接口定义了唯一的InvokeAsync方法,用来实现对请求的处理。对于实现该方法的中间件类型来说,它可以利用输入参数得到针对当前请求的HttpContext上下文,还可以得到用来向后续中间件分发请求的RequestDelegate对象。

public interface IMiddleware
{
    Task InvokeAsync(HttpContext context, RequestDelegate next);
}

实现了IMiddleware接口的中间件是通过依赖注入的形式提供的,所以在调用IAppplicationBuilder接口的UseMiddleware扩展方法注册中间件类型之前需要做相应的服务注册。在一般情况下,我们只会在需要使用Scoped生命周期时才会采用这种方式来定义中间件,所以在进行服务注册时一般将生命周期模式设置为Scoped,设置成Singleton模式也未尝不可,这就与按照约定规则定义的中间件没有本质区别。读者可能会有疑问,注册中间件服务时是否可以将生命周期模式设置为Transient?实际上这与Scoped是没有区别的,因为中间件在同一个请求上下文中只会被创建一次。

对实现了IMiddleware接口的中间件的创建与释放是通过注册的IMiddlewareFactory服务来完成的。如下面的代码片段所示,IMiddlewareFactory接口提供了如下两个方法:Create方法会根据指定的中间件类型创建出对应的实例,Release方法则负责释放指定的中间件对象。

public interface IMiddlewareFactory
{
    IMiddleware Create(Type middlewareType);
    void Release(IMiddleware middleware);
}

ASP.NET Core提供如下所示的MiddlewareFactory类型作为IMiddlewareFactory接口的默认实现,上面提及的中间件针对依赖注入的创建方式就体现在该类型中。如下面的代码片段所示,MiddlewareFactory直接利用指定的IServiceProvider对象根据指定的中间件类型来提供对应的实例。由于依赖注入框架自身具有针对提供服务实例的生命周期管理策略,所以MiddlewareFactory的Release方法不需要对提供的中间件实例做具体的释放操作。

public class MiddlewareFactory : IMiddlewareFactory
{
    private readonly IServiceProvider _serviceProvider;  

    public MiddlewareFactory(IServiceProvider serviceProvider) => _serviceProvider = serviceProvider;   
    public IMiddleware Create(Type middlewareType) => _serviceProvider.GetRequiredService(this._serviceProvider, middlewareType) as IMiddleware;       
    public void Release(IMiddleware middleware) {}
}

了解了作为中间件工厂的IMiddlewareFactory接口之后,下面介绍IApplicationBuilder用于注册中间件的UseMiddleware扩展方法是如何利用它来创建并释放中间件的,为此我们编写了如下这段简写的代码来模拟相关的实现。如下面的代码片段所示,如果注册的中间件类型实现了IMiddleware接口,UseMiddleware方法会直接创建一个Func<RequestDelegate, RequestDelegate>对象作为注册的中间件。

public static class UseMiddlewareExtensions
{ 
    public static IApplicationBuilder UseMiddleware(this IApplicationBuilder app, Type middlewareType, params object[] args)
    {
        if (typeof(IMiddleware).IsAssignableFrom(middlewareType))
        {
            if (args.Length > 0)
            {
                throw new NotSupportedException("Types that implement IMiddleware do not support explicit arguments.");
            }
            app.Use(next =>
            {
                return async context =>
                {
                    var middlewareFactory = context.RequestServices.GetRequiredService<IMiddlewareFactory>();
                    var middleware = middlewareFactory.Create(middlewareType);
                    try
                    {
                        await middleware.InvokeAsync(context, next);
                    }
                    finally
                    {
                        middlewareFactory.Release(middleware);
                    }
                };
            }); 
        }
    }
    ...
}

当作为中间件的委托对象被执行时,它会从当前HttpContext上下文的RequestServices属性中获取针对当前请求的IServiceProvider对象,并由它来提供IMiddlewareFactory对象。在利用IMiddlewareFactory对象根据注册的中间件类型创建出对应的中间件对象之后,中间件的InvokeAsync方法被调用。在当前及后续中间件针对当前请求的处理完成之后,IMiddlewareFactory对象的Release方法被调用来释放由它创建的中间件。

UseMiddleware方法之所以从当前HttpContext上下文的RequestServices属性获取IServiceProvider,而不是直接使用IApplicationBuilder的ApplicationServices属性返回的IServiceProvider来创建IMiddlewareFactory对象,是出于生命周期方面的考虑。由于后者采用针对当前应用程序的生命周期模式,所以不论注册中间件类型采用的生命周期模式是Singleton还是Scoped,提供的中间件实例都是一个Singleton对象,所以无法满足我们针对请求创建和释放中间件对象的初衷。

上面的代码片段还反映了一个细节:如果注册了一个实现了IMiddleware接口的中间件类型,我们是不允许指定任何参数的,一旦调用UseMiddleware方法时指定了参数,就会抛出一个NotSupportedException类型的异常。

四、注册中间件

在ASP.NET Core应用请求处理管道构建过程中,IApplicationBuilder对象的作用就是收集我们注册的中间件,并最终根据注册的先后顺序创建一个代表中间件委托链的RequestDelegate对象。在一个具体的ASP.NET Core应用中,利用IApplicationBuilder对象进行中间件的注册主要体现为如下3种方式。

  • 调用IWebHostBuilder的Configure方法。
  • 调用注册Startup类型的Configure方法。
  • 利用注册的IStartupFilter对象。

如下所示的IStartupFilter接口定义了唯一的Configure方法,它返回的Action<IApplicationBuilder>对象将用来注册所需的中间件。作为该方法唯一输入参数的Action<IApplicationBuilder>对象,则用来完成后续的中间件注册工作。IStartupFilter接口的Configure方法比IStartup的Configure方法先执行,所以可以利用前者注册一些前置或者后置的中间件。

public interface IStartupFilter
{
    Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next);
}

请求处理管道[1]: 模拟管道实现请求处理管道[2]: HttpContext本质论请求处理管道[3]: Pipeline = IServer +  IHttpApplication<TContext
请求处理管道[4]: 中间件委托链
请求处理管道[5]: 应用承载[上篇请求处理管道[6]: 应用承载[下篇]


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK