前言
本文編寫時原始碼參考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
型別,而且是可以不傳的,也就是它只需要傳任意一個型別都不會在編譯時報錯。
比如這樣,完全不會報錯:
當然,如果你這樣就執行程式,一定會收到下面的異常
System.InvalidOperationException:“No public 'Invoke' or 'InvokeAsync' method found for middleware of type 'System.String'.”
提示我們傳的型別沒有Invoke
或InvokeAsync
公共方法,這裡大概能猜到,底層應該是通過反射進行動態呼叫Invoke
或InvokeAsync
公共方法的。
原始碼分析
想要知道其本質,唯有檢視原始碼,以下原始碼來自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
,繼續往下看
通過反射查詢泛型類中Invoke
或InvokeAsync
方法
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)));
}
上面一堆邏輯主要就是檢查我們的Invoke
和InvokeAsync
方法是否符合要求,即:必須是接收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
,而且要有一個公共函式Invoke
或InvokeAsync
,並且接收的第一個引數是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);
}
所以在需要傳參的場景,則必須使用反射的方式,所以兩種方式都有其存在的必要。
如果本文對您有幫助,還請點贊轉發關注一波支援作者。