asp.net core中間件初始化的實現(xiàn)
前言
在日常使用asp.net core開發(fā)的過程中我們多多少少會設(shè)計到使用中間件的場景,asp.net core默認(rèn)也為我們內(nèi)置了許多的中間件,甚至有時候我們需要自定義中間件來幫我們處理一些請求管道過程中的處理。接下來,我們將圍繞著以下幾個問題來簡單探究一下,關(guān)于asp.net core中間件是如何初始化的
- 首先,使用usemiddleware注冊自定義中間件和直接use的方式有何不同
- 其次,使用基于約定的方式定義中間件和使用實現(xiàn)imiddleware接口的方式定義中間件有何不同
- 再次,使用基于約定的方式自定義中間件的究竟是如何約束我們編寫的類和方法格式的
- 最后,使用約定的方式定義中間件,通過構(gòu)造注入和通過invoke方法注入的方式有何不同
接下來我們將圍繞這幾個核心點來逐步探究關(guān)于asp.net core關(guān)于中間件初始化的神秘面紗,來指導(dǎo)我們以后使用它的時候需要有注意點,來減少踩坑的次數(shù)。
自定義的方式
使用自定義中間件的方式有好幾種,咱們簡單來演示一下三種比較常用方式。
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也是我們比較常用的一種方式,這種方式使用起來相對于第一種來說,雖然使用起來可能會稍微繁瑣一點,畢竟需要定義一個類,但是更好的符合符合面向?qū)ο蟮姆庋b思想,它的使用方式大致如下,首先定義一個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>();
實現(xiàn)imiddleware的方式
還有一種方式是實現(xiàn)imiddleware接口的方式,這種方式比如前兩種方式常用,但是也確確實實的存在于asp.net core中,既然存在也就有它存在的理由,我們也可以探究一下,它的使用方式也是需要自定義一個類去實現(xiàn)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<imiddleware,requestcultureothermiddleware>();
完成上步操作之后,同樣也需要將其注冊到管道中去
app.usemiddleware<requestcultureothermiddleware>();
這種方式相對于第二種方式的主要區(qū)別在于靈活性方面的差異,它實現(xiàn)了imiddleware接口,那就要受到imiddleware接口的約束,也就是我們常說的里氏代換原則,首先我們可以先來看下imiddleware接口的定義[點擊查看源碼👈]
public interface imiddleware { /// <summary> /// 請求處理方法 /// </summary> /// <param name="context">當(dāng)前請求上下文</param> /// <param name="next">請求管道中下一個中間件的委托</param> task invokeasync (httpcontext context, requestdelegate next); }
通過這個接口也就看出來invokeasync只能接受httpcontext和requestdelegate參數(shù),無法定義其他形式的參數(shù),也沒辦法通過注入的方式編寫invokeasync方法參數(shù),說白了就是沒有第二種方式靈活,受限較大。
關(guān)于常用的自定義中間件的方式,我們就先說到這里,我們也知道了如何定義使用中間件。接下來我們就來探討一下,這么多種方式之間到底存在怎樣的聯(lián)系。
源碼探究
上面我們已經(jīng)演示了關(guān)于使用中間件的幾種方式,那么這么幾種使用方式之間有啥聯(lián)系或區(qū)別,我們只看到了表面的,接下來我們來看一下關(guān)于中間件初始化的源碼來一探究竟。
首先,無論那種形式都是基于iapplicationbuilder這個接口擴展而來的,所以我們先從這里下手,找到源碼iapplicationbuilder位置[點擊查看源碼👈]可以看到以下代碼
/// <summary> /// 將中間件委托添加到應(yīng)用程序的請求管道。 /// </summary> /// <param name="middleware">中間件委托</param> /// <returns>the <see cref="iapplicationbuilder"/>.</returns> iapplicationbuilder use(func<requestdelegate, requestdelegate> middleware);
iapplicationbuilder接口里只有use的方式可以添加中間件,由此我們可以大致猜到兩點信息
- 其它添加中間件的方式,都是在擴展自iapplicationbuilder,并不是iapplicationbuilder本身的方法。
- 其它添加中間件的形式,最終都會轉(zhuǎn)換為use的方式。
use擴展方法
上面我們看到了iapplicationbuilder只包含了一個use方法,但是我們?nèi)粘>幊讨凶畛J褂玫降膮s并不是這一個,而是來自useextensions擴展類的use擴展方法,實現(xiàn)如下所示[點擊查看源碼👈]
public static iapplicationbuilder use(this iapplicationbuilder app, func<httpcontext, func<task>, task> middleware) { //將middleware轉(zhuǎn)換為use(func<requestdelegate, requestdelegate> middleware)的形式 return app.use(next => { return context => { func<task> simplenext = () => next(context); return middleware(context, simplenext); }; }); }
如預(yù)料的那樣,use的擴展方法最終都會轉(zhuǎn)換為use(func<requestdelegate, requestdelegate> middleware)的形式去執(zhí)行。use擴展方法的形式還是比較清晰的,畢竟也是基于委托的形式,而且參數(shù)是固定的。
usemiddleware
上面我們看到了use的擴展方法,它最終還是轉(zhuǎn)換為use(func<requestdelegate, requestdelegate> middleware)的形式去執(zhí)行。接下來我們來看下通過編寫類的形式定義中間件會是怎樣的轉(zhuǎn)換操作。找到usemiddleware擴展方法所在的地方,也就是usemiddlewareextensions擴展類里[點擊查看源碼👈],我們最常用的是usemiddleware這個方法,而且這個方法是usemiddlewareextensions擴展類的入口方法[點擊查看源碼👈],說白了就是它是完全調(diào)用別的方法沒有自己的實現(xiàn)邏輯
/// <summary> /// 將中間件類型添加到應(yīng)用程序的請求管道. /// </summary> /// <typeparam name="tmiddleware">中間件類型</typeparam> /// <param name="args">傳遞給中間件類型實例的構(gòu)造函數(shù)的參數(shù).</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); }
繼續(xù)向下看找到它調(diào)用的擴展方法,在展示該方法之前我們先羅列一下該類的常量屬性,因為類中的方法有用到,如下所示
internal const string invokemethodname = "invoke"; internal const string invokeasyncmethodname = "invokeasync";
從這里我們可以得到一個信息,基于約定的形式自定義的中間件觸發(fā)方法名可以是invoke或invokeasync
繼續(xù)看執(zhí)行方法的實現(xiàn)代碼
public static iapplicationbuilder usemiddleware(this iapplicationbuilder app, [dynamicallyaccessedmembers(middlewareaccessibility)] type middleware, params object[] args) { //判斷自定義的中間件是否是實現(xiàn)了imiddleware接口 if (typeof(imiddleware).gettypeinfo().isassignablefrom(middleware.gettypeinfo())) { //middleware不支持直接傳遞參數(shù) //因為它是注冊到容器中的,所以不能通過構(gòu)造函數(shù)傳遞自定義的參數(shù),否則拋出異常 if (args.length > 0) { throw new notsupportedexception(resources.formatexception_usemiddlewareexplicitargumentsnotsupported(typeof(imiddleware))); } //實現(xiàn)imiddleware接口的中間件走的是這個邏輯,咱們待會看 return usemiddlewareinterface(app, middleware); } var applicationservices = app.applicationservices; return app.use(next => { //獲取自定義中間件類的非靜態(tài)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方法的參數(shù) var parameters = methodinfo.getparameters(); //如果該方法不存在參數(shù)或方法的第一個參數(shù)不是httpcontext類型的實例,會拋出異常 if (parameters.length == 0 || parameters[0].parametertype != typeof(httpcontext)) { throw new invalidoperationexception(resources.formatexception_usemiddlewarenoparameters(invokemethodname, invokeasyncmethodname, nameof(httpcontext))); } //定義新的數(shù)組比傳遞的參數(shù)長度多一個,為啥呢?往下看。 var ctorargs = new object[args.length + 1]; //因為方法數(shù)組的首元素是requestdelegate類型的next //也就是基于約定定義的中間件構(gòu)造函數(shù)的第一個參數(shù)是requestdelegate類型的實例 ctorargs[0] = next; array.copy(args, 0, ctorargs, 1, args.length); //創(chuàng)建基于約定的中間件實例 //又看到activatorutilities這個類了,關(guān)于這個類有興趣的可以研究一下,可以根據(jù)容器創(chuàng)建類型實例,非常好用 var instance = activatorutilities.createinstance(app.applicationservices, middleware, ctorargs); //如果invoke或invokeasync方法只有一個參數(shù),則直接創(chuàng)建requestdelegate委托返回 if (parameters.length == 1) { //requestdelegate其實就是public delegate task requestdelegate(httpcontext context); return (requestdelegate)methodinfo.createdelegate(typeof(requestdelegate), instance); } //編譯invoke或invokeasync方法,關(guān)于compile的實現(xiàn)等會咱們再看 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))); } //返回委托執(zhí)行結(jié)果 return factory(instance, context, serviceprovider); }; }); }
這個方法其實是工作的核心方法,通過這里可以看出來,自定義中間件的大致執(zhí)行過程。代碼中的注釋我寫的比較詳細(xì),有興趣的可以仔細(xì)了解一下,如果懶得看我們就大致總結(jié)一下大致的核心點
- 首先usemiddleware的本質(zhì)確實還是執(zhí)行的use方法
- 實現(xiàn)imiddleware接口的中間件走的是獨立的處理邏輯,而且構(gòu)造函數(shù)傳遞自定義的參數(shù),因為它的數(shù)據(jù)來自于容器的注入。
- 基于約定定義中間件的情況,即不實現(xiàn)imiddleware的情況下。
- ①基于約定定義的中間件,構(gòu)造函數(shù)的第一個參數(shù)需要是requestdelegate類型
- ②查找方法名可以為invoke或invokeasync,且存在而且只能存在一個
- ③invoke或invokeasync方法返回值需為task,且方法的第一個參數(shù)必須為httpcontext類型
- ④invoke或invokeasync方法如果只包含httpcontext類型參數(shù),則該方法直接轉(zhuǎn)換為requestdelegate
- ⑤我們之所以可以通過構(gòu)造注入在中間件中獲取服務(wù)是因為基于約定的方式是通過activatorutilities類創(chuàng)建的實例
通過上面的源碼我們了解到了實現(xiàn)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))); } //創(chuàng)建middleware實例 var middleware = middlewarefactory.create(middlewaretype); if (middleware == null) { throw new invalidoperationexception(resources.formatexception_usemiddlewareunabletocreatemiddleware(middlewarefactory.gettype(), middlewaretype)); } try { //執(zhí)行middleware的invokeasync方法 await middleware.invokeasync(context, next); } finally { //釋放middleware middlewarefactory.release(middleware); } }; }); }
通過上面的代碼我們可以看到,imiddleware實例是通過imiddlewarefactory實例創(chuàng)建而來,asp.net core中imiddlewarefactory默認(rèn)注冊的實現(xiàn)類是middlewarefactory,接下來我們看下這個類的實現(xiàn)[點擊查看源碼👈]
public class middlewarefactory : imiddlewarefactory { private readonly iserviceprovider _serviceprovider; public middlewarefactory(iserviceprovider serviceprovider) { _serviceprovider = serviceprovider; } public imiddleware? create(type middlewaretype) { //根據(jù)類型從容器中獲取imiddleware實例 return _serviceprovider.getrequiredservice(middlewaretype) as imiddleware; } public void release(imiddleware middleware) { //因為容器控制了對象的生命周期,所以這里啥也沒有 } }
好吧,其實就是在容器中獲取的imiddleware實例,通過這個我們就可以總結(jié)出來實現(xiàn)imiddleware接口的形式創(chuàng)建中間件的操作
- 需要實現(xiàn)imiddleware接口,來約束中間件的行為,方法名只能為invokeasync
- 需要手動注冊imiddleware和實現(xiàn)類到容器中,生命周期可自行約束,如果生命周期為scope或瞬時,那么每次請求都會創(chuàng)建新的中間件實例
- 沒辦法通過invokeasync方法注入服務(wù),因為受到了imiddleware接口的約束
上面我們看到了實現(xiàn)imiddleware接口的方式中間件是如何被初始化的,接下來我們繼續(xù)來看,基于約定的方式定義的中間件是如何被初始化的。通過上面我們展示的源碼可知,實現(xiàn)邏輯在compile方法中,該方法整體實現(xiàn)方式就是基于expression,主要原因個人猜測有兩點,一個是形式比較靈活能應(yīng)對的場景較多,二是性能稍微比反射好一點。在此之前,我們先展示一下compile方法依賴的操作,首先反射是獲取usemiddlewareextensions類的getservice方法操作
private static readonly methodinfo getserviceinfo = typeof(usemiddlewareextensions).getmethod(nameof(getservice), bindingflags.nonpublic | bindingflags.static)!;
其中g(shù)etservice方法的實現(xiàn)如下所示,其實就是在容器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外部依賴已經(jīng)展示出來了,接下來我們就可以繼續(xù)探究compile方法了[點擊查看源碼👈]
private static func<t, httpcontext, iserviceprovider, task> compile<t>(methodinfo methodinfo, parameterinfo[] parameters) { var middleware = typeof(t); //構(gòu)建三個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數(shù)組,且數(shù)組第一個參數(shù)為httpcontextarg var methodarguments = new expression[parameters.length]; methodarguments[0] = httpcontextarg; //因為invoke或invokeasync方法第一個參數(shù)為httpcontext,且methodarguments第一個參數(shù)占位,所以跳過第一個參數(shù) for (int i = 1; i < parameters.length; i++) { //獲取方法參數(shù) var parametertype = parameters[i].parametertype; //不支持ref類型操作 if (parametertype.isbyref) { throw new notsupportedexception(resources.formatexception_invokedoesnotsupportreforoutparams(invokemethodname)); } //構(gòu)建參數(shù)類型表達(dá)式,即用戶構(gòu)建方法參數(shù)的操作 var parametertypeexpression = new expression[] { providerarg, expression.constant(parametertype, typeof(type)), expression.constant(methodinfo.declaringtype, typeof(type)) }; //聲明調(diào)用getserviceinfo的表達(dá)式 var getservicecall = expression.call(getserviceinfo, parametertypeexpression); //將getservicecall操作轉(zhuǎn)換為parametertype methodarguments[i] = expression.convert(getservicecall, parametertype); } //獲取中間件類型表達(dá)式 expression middlewareinstancearg = instancearg; if (methodinfo.declaringtype != null && methodinfo.declaringtype != typeof(t)) { //轉(zhuǎn)換中間件類型表達(dá)式類型與聲明類型一致 middlewareinstancearg = expression.convert(middlewareinstancearg, methodinfo.declaringtype); } //調(diào)用middlewareinstancearg(即當(dāng)前中間件)的methodinfo(即獲取invoke或invokeasync)方法參數(shù)(methodarguments) var body = expression.call(middlewareinstancearg, methodinfo, methodarguments); //轉(zhuǎn)換為lambda var lambda = expression.lambda<func<t, httpcontext, iserviceprovider, task>>(body, instancearg, httpcontextarg, providerarg); return lambda.compile(); }
上面的代碼比較抽象,其實主要是因為它是基于表達(dá)式樹進(jìn)行各種操作的,如果對表達(dá)式樹比較熟悉的話,可能對上面的代碼理解起來還好一點,如果不熟悉表達(dá)式樹的話,可能理解起來比較困難,不過還是建議簡單學(xué)習(xí)一下expression相關(guān)的操作,慢慢的發(fā)現(xiàn)還是挺有意思的,它的性能整體來說比傳統(tǒng)的反射性能也會更好一點。其實compile主要實現(xiàn)的操作轉(zhuǎn)化為我們比較容易理解的代碼的話就是下面所示的操作,如果我們編寫了一個如下的中間件代碼
public class middleware { public task invoke(httpcontext context, iloggerfactory loggerfactory) { } }
那么通過compile方法將轉(zhuǎn)換為類似以下形式的操作,這樣說的話可能會好理解一點
task invoke(middleware instance, httpcontext httpcontext, iserviceprovider provider) { return instance.invoke(httpcontext, (iloggerfactory)usemiddlewareextensions.getservice(provider, typeof(iloggerfactory)); }
通過上面的源碼分析我們了解到,基于約定的方式定義的中間件實例是通過activatorutilities類創(chuàng)建的,而且創(chuàng)建實例是在返回requestdelegate委托之前,iapplicationbuilder的use方法只會在首次運行的時候執(zhí)行,后續(xù)管道串聯(lián)執(zhí)行的其實正是它返回的結(jié)果requestdelegate這個委托。但是執(zhí)行轉(zhuǎn)換invoke或invokeasync方法為執(zhí)行委托的操作卻是在返回的requestdelegate委托當(dāng)中,也就是我們每次請求管道會處理的邏輯中。這個邏輯可以在iapplicationbuilder默認(rèn)的實現(xiàn)類applicationbuilder類的build方法中可以得知[點擊查看源碼👈],它的實現(xiàn)邏輯如下所示
public requestdelegate build() { //最后的管道處理,即請求未能匹配到任何終結(jié)點的情況 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); } //執(zhí)行管道的重點是404,只有未命中任何終結(jié)點的情況下才會走到這里 context.response.statuscode = statuscodes.status404notfound; return task.completedtask; }; //_components即我們通過use添加的中間件 foreach (var component in _components.reverse()) { //得到執(zhí)行結(jié)果即requestdelegate app = component(app); } //返回第一個管道中間件 return app; }
通過上面的代碼我們可以清楚的看到,管道最終執(zhí)行的就是執(zhí)行func<requestdelegate, requestdelegate>這個委托的返回結(jié)果requestdelegate。
由此得到結(jié)論,基于約定的中間件形式,通構(gòu)造函數(shù)注入的服務(wù)實例,是和應(yīng)用程序的生命周期一致的。通過invoke或invokeasync方法注入的服務(wù)實例每次請求都會被執(zhí)行到,即生命周期是scope的。
總結(jié)
通過本次對源碼的研究,我們認(rèn)識到了自定義的asp.net core中間件是如何被初始化的。雖然自定義的中間件的形式有許多種方式,但是最終還都是轉(zhuǎn)換為iapplicationbuilder use(func<requestdelegate, requestdelegate> middleware)這種方式。將中間件抽離為獨立的類有兩種方式,即基于約定的方式和實現(xiàn)imiddleware接口的形式,通過分析源碼我們也更深刻的了解兩種方式的不同之處?;诩s定的方式更靈活,它的聲明周期是單例的,但是通過它的invoke或invokeasync方法注入的服務(wù)實例生命周期是scope的。實現(xiàn)imiddleware接口的方式生命周期取決于自己注冊服務(wù)實例時候聲明的周期,而且這種方式?jīng)]辦法通過方法注入服務(wù),因為有imiddleware接口invokeasync方法的約束。
當(dāng)然不僅僅是我們在總結(jié)中說的的這些,還存在更多的細(xì)節(jié),這些我們在分析源碼的時候都有涉及,相信閱讀文章比較仔細(xì)的同學(xué)肯定會注意到這些。閱讀源碼收獲正是這些,解決心中的疑問,了解更多的細(xì)節(jié),有助于在實際使用中避免一些不必要的麻煩。本次講解就到這里,愿各位能有所收獲。
關(guān)于asp.net core中間件初始化的實現(xiàn)的文章就介紹至此,更多相關(guān)asp.net core中間件初始化內(nèi)容請搜索碩編程以前的文章,希望大家多多支持碩編程!