ASP.NET Core管道詳解[4]: 中間件委托鏈


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]: 應用承載[下篇]


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM