前言
在日常使用ASP.NET Core開發的過程中我們多多少少會設計到使用中間件的場景,ASP.NET Core默認也為我們內置了許多的中間件,甚至有時候我們需要自定義中間件來幫我們處理一些請求管道過程中的處理。接下來,我們將圍繞着以下幾個問題來簡單探究一下,關於ASP.NET Core中間件是如何初始化的
- 首先,使用UseMiddleware注冊自定義中間件和直接Use的方式有何不同
- 其次,使用基於約定的方式定義中間件和使用實現IMiddleware接口的方式定義中間件有何不同
- 再次,使用基於約定的方式自定義中間件的究竟是如何約束我們編寫的類和方法格式的
- 最后,使用約定的方式定義中間件,通過構造注入和通過Invoke方法注入的方式有何不同
接下來我們將圍繞這幾個核心點來逐步探究關於ASP.NET Core關於中間件初始化的神秘面紗,來指導我們以后使用它的時候需要有注意點,來減少踩坑的次數。
自定義的方式
使用自定義中間件的方式有好幾種,咱們簡單來演示一下三種比較常用方式。
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的類
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);
}
}
編寫完成之后,需要手動的將類注冊到管道中才能生效,注冊方式如下所示
app.UseMiddleware<RequestCultureMiddleware>();
實現IMiddleware的方式
還有一種方式是實現IMiddleware接口的方式,這種方式比如前兩種方式常用,但是也確確實實的存在於ASP.NET Core中,既然存在也就有它存在的理由,我們也可以探究一下,它的使用方式也是需要自定義一個類去實現IMiddleware接口,如下所示
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<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
/// <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方法的約束。
當然不僅僅是我們在總結中說的的這些,還存在更多的細節,這些我們在分析源碼的時候都有涉及,相信閱讀文章比較仔細的同學肯定會注意到這些。閱讀源碼收獲正是這些,解決心中的疑問,了解更多的細節,有助於在實際使用中避免一些不必要的麻煩。本次講解就到這里,願各位能有所收獲。
