1. 程式人生 > >ASP.NET Core管道詳解[4]: 中介軟體委託鏈

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的文章請點這裡]

目錄
一、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]: 應用承載