關於aspnetcore中介軟體的一些思考

果小天發表於2024-05-27

關於aspnetcore中介軟體的一些思考

最近很久沒有寫部落格了,還是自己懶惰了,前面一段時間重溫了中介軟體的原始碼(24年一月份左右),主要是專案採用了和中介軟體類似的設計,實際上就是一個簡化版的管道中介軟體模型,但是在使用過程中出現了一些問題,想著如何去解決,以及為什麼出現這樣的問題,於是有了這一篇記錄。

什麼是中介軟體

按照官方定義 :

中介軟體是一種裝配到應用管道以處理請求和響應的軟體。 每個元件:

  • 選擇是否將請求傳遞到管道中的下一個元件
  • 可在管道中的下一個元件前後執行工作

請求委託用於生成請求管道。 請求委託處理每個 HTTP 請求。

說的已經很明確了,我們的請求實際上就是中介軟體去處理的,每個中介軟體處理自己職責部門的內容,最後將處理的結果返回給使用者。

如何使用

約定俗稱

中介軟體有約定俗稱的使用方式 ,官方說明:

必須包括中介軟體類:

  • 具有型別為 RequestDelegate 的引數的公共建構函式。
  • 名為 Invoke/InvokeAsync 的公共方法。 此方法必須:
    • 返回 Task
    • 接受型別 HttpContext 的第一個引數。

建構函式和 Invoke/InvokeAsync 的其他引數由依賴關係注入 (DI) 填充。

using System.Globalization;

namespace Middleware.Example;

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;
        }

        // Call the next delegate/middleware in the pipeline.
        await _next(context);
    }
}
生命週期:

中介軟體在應用啟動時構造,因此具有應用程式生存期。 在每個請求過程中,中介軟體建構函式使用的範圍內生存期服務不與其他依賴關係注入型別共享。 若要在中介軟體和其他型別之間共享範圍內服務,請將這些服務新增到 InvokeAsync 方法的簽名。 InvokeAsync 方法可接受由 DI 填充的其他引數:

解釋一下:即中介軟體只是在啟動時構造一個例項,不是每個請求過來就構造一個例項,所以建構函式中依賴的服務,只能是單例或者瞬時的,無法接受範圍內的服務。只能在 InvokeAsync 函式 裡面新增我們需要使用的服務。 這也是我們在編寫中介軟體時遇到的常見的問題。

如果還是不太明白,下面有原始碼解析。

實現介面

官方替我們定義了IMiddleware介面,我們實現介面即可。主要是實現InvokeAsync(HttpContext context, RequestDelegate next)函式,看官方示例

public class SimpleInjectorActivatedMiddleware : IMiddleware
{
    private readonly AppDbContext _db;

    public SimpleInjectorActivatedMiddleware(AppDbContext db)
    {
        _db = db;
    }
    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        var keyValue = context.Request.Query["key"];

        if (!string.IsNullOrWhiteSpace(keyValue))
        {
            _db.Add(new Request()
                {
                    DT = DateTime.UtcNow, 
                    MiddlewareActivation = "SimpleInjectorActivatedMiddleware", 
                    Value = keyValue
                });

            await _db.SaveChangesAsync();
        }

        await next(context);
    }
}

實現IMiddleware介面,進而實現繼承的方法即可。但是和約定俗稱的可以對比一下,實現介面,我們的構造引數根據官方的說法,可以依賴範圍服務,但是我們的InvokeAsync只能有這兩個引數。但是我們需要注意的一點是我們需要註冊這個服務,生命週期為範圍或者瞬時,如下,這更加說明了此種中介軟體不是如同約定的方式是單例生命週期的

builder.Services.AddTransient<FactoryActivatedMiddleware>();

以下是官方文件

IMiddlewareFactory/IMiddleware中介軟體啟用的擴充套件點,具有以下優勢:

  • 按客戶端請求(作用域服務的注入)啟用
  • 讓中介軟體強型別化

UseMiddleware 擴充套件方法檢查中介軟體的已註冊型別是否實現 IMiddleware。 如果是,則使用在容器中註冊的 IMiddlewareFactory 例項來解析 IMiddleware 實現,而不使用基於約定的中介軟體啟用邏輯。 中介軟體在應用的服務容器中註冊為作用域或瞬態服務。

IMiddleware 按客戶端請求(連線)啟用,因此作用域服務可以注入到中介軟體的建構函式中。

實現原理

上面知道了如何使用,同時也稍微瞭解了兩者的差異,我們嘗試從原始碼解析兩者這樣原因

首先我們知道要如何去使用,我們使用UseMiddleware<>來使用我們的中介軟體,在我們啟動的時候,就會呼叫我們註冊中介軟體,所以從這方面來找。下面是UseMiddleware原始碼

 public static IApplicationBuilder UseMiddleware(
        this IApplicationBuilder app,
        [DynamicallyAccessedMembers(MiddlewareAccessibility)] Type middleware,
        params object?[] args)
    {
    	//如果middleware實現了 IMiddleware介面,
        if (typeof(IMiddleware).IsAssignableFrom(middleware))
        {   // IMiddleware不支援傳遞引數
            // IMiddleware doesn't support passing args directly since it's
            // activated from the container
            //省略一些程式碼
            var interfaceBinder = new InterfaceMiddlewareBinder(middleware);
            //直接返回
            return app.Use(interfaceBinder.CreateMiddleware);
        }
		 //如果middleware   不 實現 IMiddleware介面
        var methods = middleware.GetMethods(BindingFlags.Instance | BindingFlags.Public);
        MethodInfo? invokeMethod = null;
        foreach (var method in methods)
        {
            if (string.Equals(method.Name, InvokeMethodName, StringComparison.Ordinal) || string.Equals(method.Name, InvokeAsyncMethodName, StringComparison.Ordinal))
            {
                //省略判斷,目標是尋找InvokeAsync方法
                invokeMethod = method;
            }
        } 
       //省略一些程式碼 
        var parameters = invokeMethod.GetParameters();
     //如果InvokeAsync方法的引數為空,或者第一個引數不是HttpContext型別,報錯
        if (parameters.Length == 0 || parameters[0].ParameterType != typeof(HttpContext))
        {
            throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNoParameters(InvokeMethodName, InvokeAsyncMethodName, nameof(HttpContext)));
        }
        var reflectionBinder = new ReflectionMiddlewareBinder(app, middleware, args, invokeMethod, parameters);
        return app.Use(reflectionBinder.CreateMiddleware);
    }

如果是實現Imiddleware介面:

程式碼中可以看到,我們的中介軟體如果是實現了IMiddleware,會把中介軟體轉為為一個委託注入進去,具體關鍵程式碼是 interfaceBinder.CreateMiddleware

public RequestDelegate CreateMiddleware(RequestDelegate next)
        {
            return async context =>
            {    //di中獲取middlewareFactory
                var middlewareFactory = (IMiddlewareFactory?)context.RequestServices.GetService(typeof(IMiddlewareFactory));
                //省略一些判斷,利用middlewareFactory構造我們的中介軟體,實際上就是從di中獲取,
                //只不過包裹了一層
                var middleware = middlewareFactory.Create(_middlewareType);  
 			    //省略一些判斷
                try
                {  
                    await middleware.InvokeAsync(context, next);
                }
                finally
                {
                    middlewareFactory.Release(middleware);
                }
            };
        }

可以看到返回是 RequestDelegate ,內部邏輯是,對於每個context,從context.RequestServices獲取我們註冊的IMiddlewareFactory,然後再去建立我們的中介軟體,這就說明了,實際上我們這裡註冊的中介軟體是範圍的,是針對於每個請求建立的。既然是範圍的,那我們就可以從建構函式中注入生命週期為範圍的服務。

如果是實現基於約定的:

如果沒有實現IMiddleware,那麼就檢查,中介軟體是否實現了invokeasync函式,且第一個引數是不是context,返回的是不是task什麼的,然後關鍵函式是:reflectionBinder.CreateMiddleware,實際上也是把中介軟體轉化為RequestDelegate,但是邏輯不同,不是針對於每個context就構造一個,而是隻是在應用啟動的時候構造一次:

 public RequestDelegate CreateMiddleware(RequestDelegate next)
        {
            var ctorArgs = new object[_args.Length + 1];
            ctorArgs[0] = next;
            Array.Copy(_args, 0, ctorArgs, 1, _args.Length);
            // 直接利用反射構造,實際上也是從ApplicationServices中獲取,但是這個是根服務,只能提供單例以及瞬時的服務,不能提供範圍的服務
            var instance = ActivatorUtilities.CreateInstance(_app.ApplicationServices, _middleware, ctorArgs);
            if (_parameters.Length == 1)
            {
                return (RequestDelegate)_invokeMethod.CreateDelegate(typeof(RequestDelegate), instance);
            }
 
            // Performance optimization: Use compiled expressions to invoke middleware with services injected in Invoke.
            // If IsDynamicCodeCompiled is false then use standard reflection to avoid overhead of interpreting expressions.
            var factory = RuntimeFeature.IsDynamicCodeCompiled
                ? CompileExpression<object>(_invokeMethod, _parameters)
                : ReflectionFallback<object>(_invokeMethod, _parameters);
 			//到這裡才返回委託
            return context =>
            {
                var serviceProvider = context.RequestServices ?? _app.ApplicationServices;
                if (serviceProvider == null)
                {
                    throw new InvalidOperationException(Resources.FormatException_UseMiddlewareIServiceProviderNotAvailable(nameof(IServiceProvider)));
                }
 			 //呼叫例項的_invokeMethod方法,實際上也是serviceProvider提供所需要的額外的引數,但是隻是invokeasync方法而不是整個例項
                return factory(instance, context, serviceProvider);
            };
        }

從原始碼中可以看到,就是我們的中介軟體會在應用啟動的時候就被建立出來了,但只是建立這一次,可以認為它是單例的,由我們的根容器建立的,所以建構函式引數只能是單例或者瞬時,但是他的invokeasync函式則是針對於context每次都會呼叫一次,提供引數的容器則是context.RequestServices 它不是根容器,所以可以提供範圍服務。然後呼叫invokeasync函式。

總結

瞭解了中介軟體實現的兩種方式,一種約定俗稱,另一種實現介面,以及兩者構造的差異以及依賴服務的差異。瞭解這些可以更加方便我們去編寫合適的中介軟體去完成我們的業務。

參考文章

微軟官方文章:https://learn.microsoft.com/zh-cn/aspnet/core/fundamentals/middleware/?view=aspnetcore-8.0

寫的非常棒的博主文章:https://www.cnblogs.com/xiaoxiaotank/p/15203811.html

相關文章