AspNetCore7.0原始碼解讀之UseMiddleware

gui.h發表於2022-05-07

Use​Middleware​Extensions

前言

本文編寫時原始碼參考github倉庫主分支。

aspnetcore提供了Use方法供開發者自定義中介軟體,該方法接收一個委託物件,該委託接收一個RequestDelegate物件,並返回一個RequestDelegate物件,方法定義如下:

IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware);

委託RequestDelegate的定義

/// <summary>
/// A function that can process an HTTP request.
/// </summary>
/// <param name="context">The <see cref="HttpContext"/> for the request.</param>
/// <returns>A task that represents the completion of request processing.</returns>
public delegate Task RequestDelegate(HttpContext context);

如果我們直接使用IApplicationBuilder.Use來寫中介軟體邏輯,可以使用lamda表示式來簡化程式碼,如下:

app.Use((RequestDelegate next) =>
{
    return (HttpContext ctx) =>
    {
        // do your logic
        return next(ctx);
    };
});

如果寫一些簡單的邏輯,這種方式最為方便,問題是如果需要寫的中介軟體程式碼比較多,依然這樣去寫,會導致我們Program.cs檔案程式碼非常多,如果有多箇中介軟體,那麼最後我們的的Program.cs檔案包含多箇中介軟體程式碼,看上去十分混亂。

將中介軟體邏輯獨立出來

為了解決我們上面的程式碼不優雅,我們希望能將每個中介軟體業務獨立成一個檔案,多箇中介軟體程式碼不混亂的搞到一起。我們需要這樣做。

單獨的中介軟體檔案

// Middleware1.cs
public class Middleware1
{
    public static RequestDelegate Logic(RequestDelegate requestDelegate)
    {
        return (HttpContext ctx) =>
        {
            // do your logic
            return requestDelegate(ctx);
        };
    }
}

呼叫中介軟體

app.Use(Middleware1.Logic);
// 以下是其他中介軟體示例
app.Use(Middleware2.Logic);
app.Use(Middleware3.Logic);
app.Use(Middleware4.Logic);

這種方式可以很好的將各個中介軟體邏輯獨立出來,Program.cs此時變得十分簡潔,然而我們還不滿足這樣,因為我們的Logic方法中直接返回一個lamada表示式(RequestDelegate物件),程式碼層級深了一層,每個中介軟體都多寫這一層殼似乎不太優雅,能不能去掉這層lamada表示式呢?

UseMiddlewareExtensions

為了解決上面提到的痛點,UseMiddlewareExtensions擴充套件類應運而生,它在Aspnetcore底層大量使用,它主要提供一個泛型UseMiddleware<T>方法用來方便我們註冊中介軟體,下面是該方法的定義

public static IApplicationBuilder UseMiddleware<TMiddleware>(this IApplicationBuilder app, params object?[] args)

如果只看這個方法的宣告,估計沒人知道如何使用,因為該方法接收的泛型引數TMiddleware沒有新增任何限制,而另一個args引數也是object型別,而且是可以不傳的,也就是它只需要傳任意一個型別都不會在編譯時報錯。
比如這樣,完全不會報錯:

image.png
當然,如果你這樣就執行程式,一定會收到下面的異常

System.InvalidOperationException:“No public 'Invoke' or 'InvokeAsync' method found for middleware of type 'System.String'.”

提示我們傳的型別沒有InvokeInvokeAsync公共方法,這裡大概能猜到,底層應該是通過反射進行動態呼叫InvokeInvokeAsync公共方法的。

原始碼分析

想要知道其本質,唯有檢視原始碼,以下原始碼來自UseMiddlewareExtensions

如下,該擴充套件類一共提供兩個並且是過載的公共方法UseMiddleware,一般都只會使用第一個UseMiddleware,第一個UseMiddleware方法內部再去呼叫第二個UseMiddleware方法,原始碼中對型別前面新增的[DynamicallyAccessedMembers(MiddlewareAccessibility)]屬性可以忽略,它的作用是為了告訴編譯器我們通過反射訪問的範圍,以防止對程式集對我們可能呼叫的方法或屬性等進行裁剪。


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

/// <summary>
/// Adds a middleware type to the application's request pipeline.
/// </summary>
/// <typeparam name="TMiddleware">The middleware type.</typeparam>
/// <param name="app">The <see cref="IApplicationBuilder"/> instance.</param>
/// <param name="args">The arguments to pass to the middleware type instance's constructor.</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);
}
 
/// <summary>
/// Adds a middleware type to the application's request pipeline.
/// </summary>
/// <param name="app">The <see cref="IApplicationBuilder"/> instance.</param>
/// <param name="middleware">The middleware type.</param>
/// <param name="args">The arguments to pass to the middleware type instance's constructor.</param>
/// <returns>The <see cref="IApplicationBuilder"/> instance.</returns>
public static IApplicationBuilder UseMiddleware(
    this IApplicationBuilder app,
    [DynamicallyAccessedMembers(MiddlewareAccessibility)] Type middleware,
    params object?[] args)
{
    if (typeof(IMiddleware).IsAssignableFrom(middleware))
    {
        // IMiddleware doesn't support passing args directly since it's
        // activated from the container
        if (args.Length > 0)
        {
            throw new NotSupportedException(Resources.FormatException_UseMiddlewareExplicitArgumentsNotSupported(typeof(IMiddleware)));
        }
 
        return UseMiddlewareInterface(app, middleware);
    }
 
    var applicationServices = app.ApplicationServices;
    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))
        {
            if (invokeMethod is not null)
            {
                throw new InvalidOperationException(Resources.FormatException_UseMiddleMutlipleInvokes(InvokeMethodName, InvokeAsyncMethodName));
            }
 
            invokeMethod = method;
        }
    }
 
    if (invokeMethod is null)
    {
        throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNoInvokeMethod(InvokeMethodName, InvokeAsyncMethodName, middleware));
    }
 
    if (!typeof(Task).IsAssignableFrom(invokeMethod.ReturnType))
    {
        throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNonTaskReturnType(InvokeMethodName, InvokeAsyncMethodName, nameof(Task)));
    }
 
    var parameters = invokeMethod.GetParameters();
    if (parameters.Length == 0 || parameters[0].ParameterType != typeof(HttpContext))
    {
        throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNoParameters(InvokeMethodName, InvokeAsyncMethodName, nameof(HttpContext)));
    }
 
    var state = new InvokeMiddlewareState(middleware);
 
    return app.Use(next =>
    {
        var middleware = state.Middleware;
 
        var ctorArgs = new object[args.Length + 1];
        ctorArgs[0] = next;
        Array.Copy(args, 0, ctorArgs, 1, args.Length);
        var instance = ActivatorUtilities.CreateInstance(app.ApplicationServices, middleware, ctorArgs);
        if (parameters.Length == 1)
        {
            return (RequestDelegate)invokeMethod.CreateDelegate(typeof(RequestDelegate), instance);
        }
 
        var factory = Compile<object>(invokeMethod, parameters);
 
        return context =>
        {
            var serviceProvider = context.RequestServices ?? applicationServices;
            if (serviceProvider == null)
            {
                throw new InvalidOperationException(Resources.FormatException_UseMiddlewareIServiceProviderNotAvailable(nameof(IServiceProvider)));
            }
 
            return factory(instance, context, serviceProvider);
        };
    });
}

第一個UseMiddleware可以直接跳過,看第二個UseMiddleware方法,該方法一上來就先判斷我們傳的泛型型別是不是IMiddleware介面的派生類,如果是,直接交給UseMiddlewareInterface方法。

if (typeof(IMiddleware).IsAssignableFrom(middleware))
 {
     // IMiddleware doesn't support passing args directly since it's
     // activated from the container
     if (args.Length > 0)
     {
         throw new NotSupportedException(Resources.FormatException_UseMiddlewareExplicitArgumentsNotSupported(typeof(IMiddleware)));
     }
 
     return UseMiddlewareInterface(app, middleware);
 }

這裡總算看到應該有的東西了,如果宣告UseMiddleware<T>方法時,對泛型T新增IMiddleware限制,我們不看原始碼就知道如何編寫我們的中介軟體邏輯了,只需要寫一個類,繼承IMiddleware並實現InvokeAsync方法即可, UseMiddlewareInterface方法的實現比較簡單,因為我們繼承了介面,邏輯相對會簡單點。

private static IApplicationBuilder UseMiddlewareInterface(
    IApplicationBuilder app,
    Type middlewareType)
{
    return app.Use(next =>
    {
        return async context =>
        {
            var middlewareFactory = (IMiddlewareFactory?)context.RequestServices.GetService(typeof(IMiddlewareFactory));
            if (middlewareFactory == null)
            {
                // No middleware factory
                throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNoMiddlewareFactory(typeof(IMiddlewareFactory)));
            }
 
            var middleware = middlewareFactory.Create(middlewareType);
            if (middleware == null)
            {
                // The factory returned null, it's a broken implementation
                throw new InvalidOperationException(Resources.FormatException_UseMiddlewareUnableToCreateMiddleware(middlewareFactory.GetType(), middlewareType));
            }
 
            try
            {
                await middleware.InvokeAsync(context, next);
            }
            finally
            {
                middlewareFactory.Release(middleware);
            }
        };
    });
}
public interface IMiddleware
{
    /// <summary>
    /// Request handling method.
    /// </summary>
    /// <param name="context">The <see cref="HttpContext"/> for the current request.</param>
    /// <param name="next">The delegate representing the remaining middleware in the request pipeline.</param>
    /// <returns>A <see cref="Task"/> that represents the execution of this middleware.</returns>
    Task InvokeAsync(HttpContext context, RequestDelegate next);
}

如果我們的類不滿足IMiddleware,繼續往下看

通過反射查詢泛型類中InvokeInvokeAsync方法

var applicationServices = app.ApplicationServices;
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))
    {
        // 如果Invoke和InvokeAsync同時存在,則丟擲異常,也就是,我們只能二選一
        if (invokeMethod is not null)
        {
            throw new InvalidOperationException(Resources.FormatException_UseMiddleMutlipleInvokes(InvokeMethodName, InvokeAsyncMethodName));
        }
 
        invokeMethod = method;
    }
}

// 如果找不到Invoke和InvokeAsync則丟擲異常,上文提到的那個異常。
if (invokeMethod is null)
{
    throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNoInvokeMethod(InvokeMethodName, InvokeAsyncMethodName, middleware));
}

// 如果Invoke和InvokeAsync方法的返回值不是Task或Task的派生類,則丟擲異常
if (!typeof(Task).IsAssignableFrom(invokeMethod.ReturnType))
{
    throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNonTaskReturnType(InvokeMethodName, InvokeAsyncMethodName, nameof(Task)));
}
Snippet

// 如果Invoke和InvokeAsync方法沒有引數,或第一個引數不是HttpContext,拋異常
var parameters = invokeMethod.GetParameters();
if (parameters.Length == 0 || parameters[0].ParameterType != typeof(HttpContext))
{
    throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNoParameters(InvokeMethodName, InvokeAsyncMethodName, nameof(HttpContext)));
}

上面一堆邏輯主要就是檢查我們的InvokeInvokeAsync方法是否符合要求,即:必須是接收HttpContext引數,返回Task物件,這恰好就是委託RequestDelegate的定義。

構造RequestDelegate
這部分原始碼的解讀都註釋到相應的位置了,如下

var state = new InvokeMiddlewareState(middleware);
// 呼叫Use函式,向管道中註冊中介軟體
return app.Use(next =>
{
    var middleware = state.Middleware;

    var ctorArgs = new object[args.Length + 1];
    // next是RequestDelegate物件,作為建構函式的第一個引數傳入
    ctorArgs[0] = next;
    Array.Copy(args, 0, ctorArgs, 1, args.Length);
    // 反射例項化我們傳入的泛型類,並把next和args作為建構函式的引數傳入
    var instance = ActivatorUtilities.CreateInstance(app.ApplicationServices, middleware, ctorArgs);
    // 如果我們的Invoke方法只有一個引數,則直接建立該方法的委託
    if (parameters.Length == 1)
    {
        return (RequestDelegate)invokeMethod.CreateDelegate(typeof(RequestDelegate), instance);
    }
    
    // 當Invoke方法不止一個引數HttpContext,通過Compile函式建立動態表示式目錄樹,
    // 表示式目錄樹的構造此處略過,其目的是實現將除第一個引數的其他引數通過IOC注入
    var factory = Compile<object>(invokeMethod, parameters);

    return context =>
    {
        // 獲取serviceProvider用於在上面構造的表示式目錄樹中實現依賴注入
        var serviceProvider = context.RequestServices ?? applicationServices;
        if (serviceProvider == null)
        {
            throw new InvalidOperationException(Resources.FormatException_UseMiddlewareIServiceProviderNotAvailable(nameof(IServiceProvider)));
        }
        // 將所需的引數傳入構造的表示式目錄樹工廠
        return factory(instance, context, serviceProvider);
    };
});

至此,整個擴充套件類的原始碼就解讀完了。

通過UseMiddleware注入自定義中介軟體

通過上面的原始碼解讀,我們知道了其實我們傳入的泛型型別是有嚴格的要求的,主要有兩種

通過繼承IMiddleware

繼承IMiddleware並實現該介面的InvokeAsync函式

public class Middleware1 : IMiddleware
{
    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        // do your logic
        await next(context);
    }
}

通過反射

我們知道,在不繼承IMiddleware的情況下,底層會通過反射例項化泛型型別,並通過建構函式傳入RequestDelegate,而且要有一個公共函式InvokeInvokeAsync,並且接收的第一個引數是HttpContext,返回Task,根據要求我們將Middleware1.cs改造如下

public class Middleware1
{
    RequestDelegate next;
 
    public Middleware1(RequestDelegate next)
    {
        this.next = next;
    }
 
    public async Task Invoke(HttpContext httpContext)
    {
        // do your logic
        await this.next(httpContext);
    }
}

總結

通過原始碼的學習,我們弄清楚底層註冊中介軟體的來龍去脈,兩種方式根據自己習慣進行使用,筆者認為通過介面的方式更加簡潔直觀簡單,並且省去了反射帶來的效能損失,推薦使用。既然通過繼承介面那麼爽,為啥還費那麼大勁實現反射的方式呢?由原始碼可知,如果繼承介面的話,就不能進行動態傳參了。

if (typeof(IMiddleware).IsAssignableFrom(middleware))
        {
            // IMiddleware doesn't support passing args directly since it's
            // activated from the container
            if (args.Length > 0)
            {
                throw new NotSupportedException(Resources.FormatException_UseMiddlewareExplicitArgumentsNotSupported(typeof(IMiddleware)));
            }
 
            return UseMiddlewareInterface(app, middleware);
        }

所以在需要傳參的場景,則必須使用反射的方式,所以兩種方式都有其存在的必要。

如果本文對您有幫助,還請點贊轉發關注一波支援作者。

相關文章