ASP.NET Core中間件初始化探究

前言

    在平常使用ASP.NET Core開發的過程當中咱們多多少少會設計到使用中間件的場景,ASP.NET Core默認也爲咱們內置了許多的中間件,甚至有時候咱們須要自定義中間件來幫咱們處理一些請求管道過程當中的處理。接下來,咱們將圍繞着如下幾個問題來簡單探究一下,關於ASP.NET Core中間件是如何初始化的git

  • 首先,使用UseMiddleware註冊自定義中間件和直接Use的方式有何不一樣
  • 其次,使用基於約定的方式定義中間件和使用實現IMiddleware接口的方式定義中間件有何不一樣
  • 再次,使用基於約定的方式自定義中間件的到底是如何約束咱們編寫的類和方法格式的
  • 最後,使用約定的方式定義中間件,經過構造注入和經過Invoke方法注入的方式有何不一樣

接下來咱們將圍繞這幾個核心點來逐步探究關於ASP.NET Core關於中間件初始化的神祕面紗,來指導咱們之後使用它的時候須要有注意點,來減小踩坑的次數。github

自定義的方式

使用自定義中間件的方式有好幾種,我們簡單來演示一下三種比較經常使用方式。編程

Use方式

首先,也是最直接最簡單的使用Use的方式,好比數組

app.Use(async (context, next) =>
{
    var endpoint = context.Features.Get<IEndpointFeature>()?.Endpoint;
    if (endpoint != null)
    {
        ResponseCacheAttribute responseCache = endpoint.Metadata.GetMetadata<ResponseCacheAttribute>();
        if (responseCache != null)
        {
            //作一些事情
        }
    }
    await next();
});
基於約定的方式

而後使用UseMiddleware也是咱們比較經常使用的一種方式,這種方式使用起來相對於第一種來講,雖然使用起來可能會稍微繁瑣一點,畢竟須要定義一個類,可是更好的符合符合面向對象的封裝思想,它的使用方式大體以下,首先定義一個Middleware的類app

public class RequestCultureMiddleware
{
    private readonly RequestDelegate _next;
    public RequestCultureMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        var cultureQuery = context.Request.Query["culture"];
        if (!string.IsNullOrWhiteSpace(cultureQuery))
        {
            var culture = new CultureInfo(cultureQuery);
            CultureInfo.CurrentCulture = culture;
            CultureInfo.CurrentUICulture = culture;
        }
        await _next(context);
    }
}

編寫完成以後,須要手動的將類註冊到管道中才能生效,註冊方式以下所示async

app.UseMiddleware<RequestCultureMiddleware>();
實現IMiddleware的方式

還有一種方式是實現IMiddleware接口的方式,這種方式好比前兩種方式經常使用,可是也確確實實的存在於ASP.NET Core中,既然存在也就有它存在的理由,咱們也能夠探究一下,它的使用方式也是須要自定義一個類去實現IMiddleware接口,以下所示ide

public class RequestCultureOtherMiddleware:IMiddleware
{
    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        var cultureQuery = context.Request.Query["culture"];
        if (!string.IsNullOrWhiteSpace(cultureQuery))
        {
            var culture = new CultureInfo(cultureQuery);
            CultureInfo.CurrentCulture = culture;
            CultureInfo.CurrentUICulture = culture;
        }
        await next(context);
    }
}

這種方式和第二種方式略有不一樣,須要手動將中間件註冊到容器中,至於聲明週期也沒作特殊要求,能夠直接註冊爲單例模式函數

services.AddSingleton<IMiddleware,RequestCultureOtherMiddleware>();

完成上步操做以後,一樣也須要將其註冊到管道中去源碼分析

app.UseMiddleware<RequestCultureOtherMiddleware>();

這種方式相對於第二種方式的主要區別在於靈活性方面的差別,它實現了IMiddleware接口,那就要受到IMiddleware接口的約束,也就是咱們常說的里氏代換原則,首先咱們能夠先來看下IMiddleware接口的定義[點擊查看源碼👈]性能

public interface IMiddleware
{
	/// <summary>
	/// 請求處理方法
	/// </summary>
	/// <param name="context">當前請求上下文</param>
	/// <param name="next">請求管道中下一個中間件的委託</param>
	Task InvokeAsync (HttpContext context, RequestDelegate next);
}

經過這個接口也就看出來InvokeAsync只能接受HttpContext和RequestDelegate參數,沒法定義其餘形式的參數,也沒辦法經過注入的方式編寫InvokeAsync方法參數,說白了就是沒有第二種方式靈活,受限較大。
關於經常使用的自定義中間件的方式,咱們就先說到這裏,咱們也知道了如何定義使用中間件。接下來咱們就來探討一下,這麼多種方式之間到底存在怎樣的聯繫。

源碼探究

上面咱們已經演示了關於使用中間件的幾種方式,那麼這麼幾種使用方式之間有啥聯繫或區別,咱們只看到了表面的,接下來咱們來看一下關於中間件初始化的源碼來一探究竟。
首先,不管那種形式都是基於IApplicationBuilder這個接口擴展而來的,因此咱們先從這裏下手,找到源碼IApplicationBuilder位置[點擊查看源碼👈]能夠看到如下代碼

/// <summary> 
/// 將中間件委託添加到應用程序的請求管道。
/// </summary> 
/// <param name="middleware">中間件委託</param> 
/// <returns>The <see cref="IApplicationBuilder"/>.</returns> 
IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware);

IApplicationBuilder接口裏只有Use的方式能夠添加中間件,由此咱們能夠大體猜到兩點信息

  • 其它添加中間件的方式,都是在擴展自IApplicationBuilder,並非IApplicationBuilder自己的方法。
  • 其它添加中間件的形式,最終都會轉換爲Use的方式。
Use擴展方法

上面咱們看到了IApplicationBuilder只包含了一個Use方法,可是咱們平常編程中最常使用到的卻並非這一個,而是來自UseExtensions擴展類的Use擴展方法,實現以下所示[點擊查看源碼👈]

public static IApplicationBuilder Use(this IApplicationBuilder app, Func<HttpContext, Func<Task>, Task> middleware)
{
   //將middleware轉換爲Use(Func<RequestDelegate, RequestDelegate> middleware)的形式
    return app.Use(next =>
    {
        return context =>
        {
            Func<Task> simpleNext = () => next(context);
            return middleware(context, simpleNext);
        };
    });
}

如預料的那樣,Use的擴展方法最終都會轉換爲Use(Func<RequestDelegate, RequestDelegate> middleware)的形式去執行。Use擴展方法的形式仍是比較清晰的,畢竟也是基於委託的形式,並且參數是固定的。

UseMiddleware

上面咱們看到了Use的擴展方法,它最終仍是轉換爲Use(Func<RequestDelegate, RequestDelegate> middleware)的形式去執行。接下來咱們來看下經過編寫類的形式定義中間件會是怎樣的轉換操做。找到UseMiddleware擴展方法所在的地方,也就是UseMiddlewareExtensions擴展類裏[點擊查看源碼👈],咱們最經常使用的是UseMiddleware 這個方法,並且這個方法是UseMiddlewareExtensions擴展類的入口方法[ 點擊查看源碼👈],說白了就是它是徹底調用別的方法沒有本身的實現邏輯

/// <summary> 
/// 將中間件類型添加到應用程序的請求管道.
/// </summary> 
/// <typeparam name="TMiddleware">中間件類型</typeparam> 
/// <param name="args">傳遞給中間件類型實例的構造函數的參數.</param> 
/// <returns>The <see cref="IApplicationBuilder"/> instance.</returns> 
public static IApplicationBuilder UseMiddleware<[DynamicallyAccessedMembers(MiddlewareAccessibility)]TMiddleware>(this IApplicationBuilder app, params object[] args) 
{ 
    return app.UseMiddleware(typeof(TMiddleware), args); 
}

繼續向下看找到它調用的擴展方法,在展現該方法以前咱們先羅列一下該類的常量屬性,由於類中的方法有用到,以下所示

internal const string InvokeMethodName = "Invoke"; 
internal const string InvokeAsyncMethodName = "InvokeAsync";

從這裏咱們能夠獲得一個信息,基於約定的形式自定義的中間件觸發方法名能夠是Invoke或InvokeAsync

繼續看執行方法的實現代碼

public static IApplicationBuilder UseMiddleware(this IApplicationBuilder app, [DynamicallyAccessedMembers(MiddlewareAccessibility)] Type middleware, params object[] args)
{
    //判斷自定義的中間件是不是實現了IMiddleware接口
    if (typeof(IMiddleware).GetTypeInfo().IsAssignableFrom(middleware.GetTypeInfo()))
    {
        //Middleware不支持直接傳遞參數
        //由於它是註冊到容器中的,因此不能經過構造函數傳遞自定義的參數,不然拋出異常
        if (args.Length > 0)
        {
            throw new NotSupportedException(Resources.FormatException_UseMiddlewareExplicitArgumentsNotSupported(typeof(IMiddleware)));
        }
        //實現IMiddleware接口的中間件走的是這個邏輯,我們待會看
        return UseMiddlewareInterface(app, middleware);
    }

    var applicationServices = app.ApplicationServices;
    return app.Use(next =>
    {
        //獲取自定義中間件類的非靜態public方法
        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的方法只能有有一個,存在多個話會拋出異常
        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的方法的返回值類型必須是Task類型,不然會拋出異常
        var methodInfo = invokeMethods[0];
        if (!typeof(Task).IsAssignableFrom(methodInfo.ReturnType))
        {
            throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNonTaskReturnType(InvokeMethodName, InvokeAsyncMethodName, nameof(Task)));
        }
        //獲取Invoke或InvokeAsync方法的參數
        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];
        //由於方法數組的首元素是RequestDelegate類型的next
        //也就是基於約定定義的中間件構造函數的第一個參數是RequestDelegate類型的實例
        ctorArgs[0] = next;
        Array.Copy(args, 0, ctorArgs, 1, args.Length);
        //建立基於約定的中間件實例
        //又看到ActivatorUtilities這個類了,關於這個類有興趣的能夠研究一下,能夠根據容器建立類型實例,很是好用
        var instance = ActivatorUtilities.CreateInstance(app.ApplicationServices, middleware, ctorArgs);
        //若是Invoke或InvokeAsync方法只有一個參數,則直接建立RequestDelegate委託返回
        if (parameters.Length == 1)
        {
            //RequestDelegate其實就是public delegate Task RequestDelegate(HttpContext context);
            return (RequestDelegate)methodInfo.CreateDelegate(typeof(RequestDelegate), instance);
        }
        //編譯Invoke或InvokeAsync方法,關於Compile的實現等會我們再看
        var factory = Compile<object>(methodInfo, parameters);
        //返回這個委託
        //看着這個委託的格式有點眼熟,其實就是RequestDelegate即public delegate Task RequestDelegate(HttpContext context);
        return context =>
        {
            var serviceProvider = context.RequestServices ?? applicationServices;
            //serviceProvider不能爲空,不然無法玩了
            if (serviceProvider == null)
            {
                throw new InvalidOperationException(Resources.FormatException_UseMiddlewareIServiceProviderNotAvailable(nameof(IServiceProvider)));
            }
            //返回委託執行結果
            return factory(instance, context, serviceProvider);
        };
    });
}

這個方法實際上是工做的核心方法,經過這裏能夠看出來,自定義中間件的大體執行過程。代碼中的註釋我寫的比較詳細,有興趣的能夠仔細瞭解一下,若是懶得看咱們就大體總結一下大體的核心點

  • 首先UseMiddleware的本質確實仍是執行的Use方法
  • 實現IMiddleware接口的中間件走的是獨立的處理邏輯,並且構造函數傳遞自定義的參數,由於它的數據來自於容器的注入。
  • 基於約定定義中間件的狀況,即不實現IMiddleware的狀況下。
    ①基於約定定義的中間件,構造函數的第一個參數須要是RequestDelegate類型
    ②查找方法名能夠爲Invoke或InvokeAsync,且存在並且只能存在一個
    ③Invoke或InvokeAsync方法返回值需爲Task,且方法的第一個參數必須爲HttpContext類型
    ④Invoke或InvokeAsync方法若是隻包含HttpContext類型參數,則該方法直接轉換爲RequestDelegate
    ⑤咱們之因此能夠經過構造注入在中間件中獲取服務是由於基於約定的方式是經過ActivatorUtilities類建立的實例

經過上面的源碼咱們瞭解到了實現IMiddleware接口的方式自定義中間件的方式是單獨處理的即在UseMiddlewareInterface方法中[點擊查看源碼👈],接下來咱們查看一下該方法的代碼

private static IApplicationBuilder UseMiddlewareInterface(IApplicationBuilder app, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type middlewareType)
{
    return app.Use(next =>
    {
        return async context =>
        {
            var middlewareFactory = (IMiddlewareFactory?)context.RequestServices.GetService(typeof(IMiddlewareFactory));
            if (middlewareFactory == null)
            {
                // 沒有middlewarefactory直接拋出異常
                throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNoMiddlewareFactory(typeof(IMiddlewareFactory)));
            }
            //建立middleware實例
            var middleware = middlewareFactory.Create(middlewareType);
            if (middleware == null)
            {
                throw new InvalidOperationException(Resources.FormatException_UseMiddlewareUnableToCreateMiddleware(middlewareFactory.GetType(), middlewareType));
            }

            try
            {
                //執行middleware的InvokeAsync方法
                await middleware.InvokeAsync(context, next);
            }
            finally
            {
                //釋放middleware
                middlewareFactory.Release(middleware);
            }
        };
    });
}

經過上面的代碼咱們能夠看到,IMiddleware實例是經過IMiddlewareFactory實例建立而來,ASP.NET Core中IMiddlewareFactory默認註冊的實現類是MiddlewareFactory,接下來咱們看下這個類的實現[點擊查看源碼👈]

public class MiddlewareFactory : IMiddlewareFactory
{
    private readonly IServiceProvider _serviceProvider;

    public MiddlewareFactory(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    public IMiddleware? Create(Type middlewareType)
    {
        //根據類型從容器中獲取IMiddleware實例
        return _serviceProvider.GetRequiredService(middlewareType) as IMiddleware;
    }

    public void Release(IMiddleware middleware)
    {
        //由於容器控制了對象的生命週期,因此這裏啥也沒有
    }
}

好吧,其實就是在容器中獲取的IMiddleware實例,經過這個咱們就能夠總結出來實現IMiddleware接口的形式建立中間件的操做

  • 須要實現IMiddleware接口,來約束中間件的行爲,方法名只能爲InvokeAsync
  • 須要手動註冊IMiddleware和實現類到容器中,生命週期可自行約束,若是生命週期爲Scope或瞬時,那麼每次請求都會建立新的中間件實例
  • 沒辦法經過InvokeAsync方法注入服務,由於受到了IMiddleware接口的約束

上面咱們看到了實現IMiddleware接口的方式中間件是如何被初始化的,接下來咱們繼續來看,基於約定的方式定義的中間件是如何被初始化的。經過上面咱們展現的源碼可知,實現邏輯在Compile方法中,該方法總體實現方式就是基於Expression,主要緣由我的猜想有兩點,一個是形式比較靈活能應對的場景較多,二是性能稍微比反射好一點。在此以前,咱們先展現一下Compile方法依賴的操做,首先反射是獲取UseMiddlewareExtensions類的GetService方法操做

private static readonly MethodInfo GetServiceInfo = typeof(UseMiddlewareExtensions).GetMethod(nameof(GetService), BindingFlags.NonPublic | BindingFlags.Static)!;

其中GetService方法的實現以下所示,其實就是在容器ServiceProvider中獲取指定類型實例

private static object GetService(IServiceProvider sp, Type type, Type middleware)
{
    var service = sp.GetService(type);
    if (service == null)
    {
        throw new InvalidOperationException(Resources.FormatException_InvokeMiddlewareNoService(type, middleware));
    }
    return service;
}

好了上面已將Compile外部依賴已經展現出來了,接下來咱們就能夠繼續探究Compile方法了[點擊查看源碼👈]

private static Func<T, HttpContext, IServiceProvider, Task> Compile<T>(MethodInfo methodInfo, ParameterInfo[] parameters)
{
    var middleware = typeof(T);
    //構建三個Parameter名爲httpContext、serviceProvider、middleware
    var httpContextArg = Expression.Parameter(typeof(HttpContext), "httpContext");
    var providerArg = Expression.Parameter(typeof(IServiceProvider), "serviceProvider");
    var instanceArg = Expression.Parameter(middleware, "middleware");

    //穿件Expression數組,且數組第一個參數爲httpContextArg
    var methodArguments = new Expression[parameters.Length];
    methodArguments[0] = httpContextArg;
    //由於Invoke或InvokeAsync方法第一個參數爲HttpContext,且methodArguments第一個參數佔位,因此跳過第一個參數
    for (int i = 1; i < parameters.Length; i++)
    {
        //獲取方法參數
        var parameterType = parameters[i].ParameterType;
        //不支持ref類型操做
        if (parameterType.IsByRef)
        {
            throw new NotSupportedException(Resources.FormatException_InvokeDoesNotSupportRefOrOutParams(InvokeMethodName));
        }
       
       //構建參數類型表達式,即用戶構建方法參數的操做
        var parameterTypeExpression = new Expression[]
        {
            providerArg,
            Expression.Constant(parameterType, typeof(Type)),
            Expression.Constant(methodInfo.DeclaringType, typeof(Type))
        };
        //聲明調用GetServiceInfo的表達式
        var getServiceCall = Expression.Call(GetServiceInfo, parameterTypeExpression);
        //將getServiceCall操做轉換爲parameterType
        methodArguments[i] = Expression.Convert(getServiceCall, parameterType);
    }
    //獲取中間件類型表達式
    Expression middlewareInstanceArg = instanceArg;
    if (methodInfo.DeclaringType != null && methodInfo.DeclaringType != typeof(T))
    {
        //轉換中間件類型表達式類型與聲明類型一致
        middlewareInstanceArg = Expression.Convert(middlewareInstanceArg, methodInfo.DeclaringType);
    }
    //調用middlewareInstanceArg(即當前中間件)的methodInfo(即獲取Invoke或InvokeAsync)方法參數(methodArguments)
    var body = Expression.Call(middlewareInstanceArg, methodInfo, methodArguments);
    //轉換爲lambda
    var lambda = Expression.Lambda<Func<T, HttpContext, IServiceProvider, Task>>(body, instanceArg, httpContextArg, providerArg);
    return lambda.Compile();
}

上面的代碼比較抽象,其實主要是由於它是基於表達式樹進行各類操做的,若是對錶達式樹比較熟悉的話,可能對上面的代碼理解起來還好一點,若是不熟悉表達式樹的話,可能理解起來比較困難,不過仍是建議簡單學習一下Expression相關的操做,慢慢的發現仍是挺有意思的,它的性能總體來講比傳統的反射性能也會更好一點。其實Compile主要實現的操做轉化爲咱們比較容易理解的代碼的話就是下面所示的操做,若是咱們編寫了一個以下的中間件代碼

public class Middleware
{
    public Task Invoke(HttpContext context, ILoggerFactory loggerFactory)
    {
    }
}

那麼經過Compile方法將轉換爲相似如下形式的操做,這樣說的話可能會好理解一點

Task Invoke(Middleware instance, HttpContext httpContext, IServiceProvider provider)
{
    return instance.Invoke(httpContext, (ILoggerFactory)UseMiddlewareExtensions.GetService(provider, typeof(ILoggerFactory));
}

經過上面的源碼分析咱們瞭解到,基於約定的方式定義的中間件實例是經過ActivatorUtilities類建立的,並且建立實例是在返回RequestDelegate委託以前,IApplicationBuilder的Use方法只會在首次運行的時候執行,後續管道串聯執行的其實正是它返回的結果RequestDelegate這個委託。可是執行轉換Invoke或InvokeAsync方法爲執行委託的操做倒是在返回的RequestDelegate委託當中,也就是咱們每次請求管道會處理的邏輯中。這個邏輯能夠在IApplicationBuilder默認的實現類ApplicationBuilder類的Build方法中能夠得知[點擊查看源碼👈],它的實現邏輯以下所示

public RequestDelegate Build()
{
    //最後的管道處理,即請求未能匹配到任何終結點的狀況
    RequestDelegate app = context =>
    {
        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;
    };
    //_components即咱們經過Use添加的中間件
    foreach (var component in _components.Reverse())
    {
       //獲得執行結果即RequestDelegate
        app = component(app);
    }
    //返回第一個管道中間件
    return app;
}

經過上面的代碼咱們能夠清楚的看到,管道最終執行的就是執行Func<RequestDelegate, RequestDelegate>這個委託的返回結果RequestDelegate。

由此獲得結論,基於約定的中間件形式,通構造函數注入的服務實例,是和應用程序的生命週期一致的。經過Invoke或InvokeAsync方法注入的服務實例每次請求都會被執行到,即生命週期是Scope的。

總結

    經過本次對源碼的研究,咱們認識到了自定義的ASP.NET Core中間件是如何被初始化的。雖然自定義的中間件的形式有許多種方式,可是最終還都是轉換爲IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware)這種方式。將中間件抽離爲獨立的類有兩種方式,即基於約定的方式和實現IMiddleware接口的形式,經過分析源碼咱們也更深入的瞭解兩種方式的不一樣之處。基於約定的方式更靈活,它的聲明週期是單例的,可是經過它的Invoke或InvokeAsync方法注入的服務實例生命週期是Scope的。實現IMiddleware接口的方式生命週期取決於本身註冊服務實例時候聲明的週期,並且這種方式沒辦法經過方法注入服務,由於有IMiddleware接口InvokeAsync方法的約束。
    固然不只僅是咱們在總結中說的的這些,還存在更多的細節,這些咱們在分析源碼的時候都有涉及,相信閱讀文章比較仔細的同窗確定會注意到這些。閱讀源碼收穫正是這些,解決心中的疑問,瞭解更多的細節,有助於在實際使用中避免一些沒必要要的麻煩。本次講解就到這裏,願各位能有所收穫。

👇歡迎掃碼關注個人公衆號👇
相關文章
相關標籤/搜索