在ASP.Net Core 添加了諸多的“中間件”處理用戶的請求和響應,這種方式非常編譯功能擴展,如果我需要修改某個功能時 直接添加對應的中間件就好了,不需要 大規模的調整架構了。
當然再好的架構都有其弊端。
矛盾存在於一切事物,並貫穿於事物發展的始終。
好了,言歸正傳
消息中間件的原理
先看下面的代碼
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseStaticFiles();
app.UseStatusCodePages();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/", async context =>
{
await context.Response.WriteAsync("Hello World!");
});
});
}
這段代碼很常見,依次注入了使用靜態文件,是錯狀態碼頁,使用路由,使用節點,那麼這些都是如何實現的呢?
我們先看一下原理,搞懂原理再擼代碼。
如下是個簡化的管道
看完上面的圖片,再看代碼我想 你也就能明白編寫各個Use時順序爲啥是異常處理在最前面了吧。
越是靠前,請求越是先運行,越是靠前,應答越是靠後處理。
因果關係是客觀事物本身的一種規律。
如何編寫自己的中間件
在MVC中如果我們要在管道中添加一箇中間件,可以使用Use方法,該方法是MVC有限的共有方法中的一個,你應該可以發現這個Run方法是多麼重要。
在事物發展的任何階段上,必有而且只有一種矛盾居於支配的地位,起着規定或影響其他矛盾的作用。這種矛盾就是主要矛盾。其他矛盾則是非主要矛盾,即次要矛盾。
我們先看一下Use方法。
//
// 摘要:
// Adds a middleware delegate to the application's request pipeline.
//
// 參數:
// middleware:
// The middleware delegate.
//
// 返回結果:
// The Microsoft.AspNetCore.Builder.IApplicationBuilder.
IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware);
這個方法只有一個參數,這個參數 是個帶返回值的委託,第一個委託參數 你可以理解爲HttpContext上下文(也就是被處理就是對象),第二個委託參數就是管道中的下一個消息中間件,我們調用時不能直接return掉。爲啥?如果你直接return掉 後面的消息中間件就不會執行了,任務直接涼涼。
我們應該這樣寫。
app.Use(async (current, next) =>
{
if (current.Request.Headers.ContainsKey("CurrentDateTime"))
{
current.Response.Headers.Add("DateTime", DateTime.Now.ToString());
}
await next.Invoke();
});
當我們檢測到Http上下文請求中包含頭CurrentDateTime,就在返回的頭中添加 當前時間,最後我們直接await 下一個 中間件。這裏不是 retun切記。當然這種寫法 在.net 3.0中 已經不推薦了(微軟內部還是這樣實現的,只是不推薦你這樣寫)。
小夥子 你不推薦我這樣寫,那我怎麼寫?
你應該這樣寫。
public class DateTimeMiddleware
{
private readonly RequestDelegate next;
public DateTimeMiddleware(RequestDelegate next)
{
this.next = next;
}
public async Task Inboke(HttpContext httpContext)
{
if (httpContext.Request.Headers.ContainsKey("CurrentDateTime"))
{
httpContext.Response.Headers.Add("DateTime", DateTime.Now.ToString());
}
await next.Invoke(httpContext);
}
}
那麼這個類如何調用呢?當然這裏不能直接使用Use了,很明顯沒法傳入,怎麼搞?使用微軟編寫的擴展方法
app.UseMiddleware<DateTimeMiddleware>();
這個擴展方法也是擴展的Use,你也可以自己寫一個。爲啥要這樣寫???因爲這樣方便做單元測試,而你直接寫Use那其實是一個匿名方法,不太好做單元測試。
要想詳細瞭解中間件,我們還不得不瞭解一下分支管道,所謂分支管道就是指滿足一定條件纔會觸發的管道,我們前面說的管道其實無論是否滿足條件是都會執行一遍的,好,接下來,我們說下分支管道。
在Asp.Net Core中,分支管道是通過Map來實現的。
原理很簡單,框架是通過識別PathString來識別,然後條件注入IApplicationBuilder。我們具體看一下這個方法。
IApplicationBuilder Map(this IApplicationBuilder app, PathString pathMatch, Action<IApplicationBuilder> configuration);
依據上面說的,我們就可以把前面寫的中間件改成如下格式
app.Map("/dateTime", appbuilder =>
{
appbuilder.UseMiddleware<DateTimeMiddleware>();
});
當HttpContext請求上下文Uri中包含dateTime,該消息中間件便會被觸發。
發展的實質是新事物產生和舊事物滅亡,發展的本質是事物不斷實現自身的“揚棄”向着更高的層次不斷前進
上面說的管道實際上 進入是完全判斷dateTime的,但是如果我們還想附加一些條件怎麼辦?那就使用MapWhen()
基於上面的介紹再看如下的代碼,我想你肯定能搞懂如何使用了。
IApplicationBuilder MapWhen(this IApplicationBuilder app, Func<HttpContext, bool> predicate, Action<IApplicationBuilder> configuration);
其實他不是調用PathString了,而是傳入一個HTTPContext,你給他返回一個bool結果,然後框架根據返回值自動決定是否運行後面的代碼。如此擴展性強多了。
在Asp.Net Core3.0中(我不是很確定之前是否存在)微軟又擴展出了一個新的中間件,
UseEndpoints(this IApplicationBuilder builder, Action<IEndpointRouteBuilder> configure);
這個中間件使用前必須調用
public static IApplicationBuilder UseRouting(this IApplicationBuilder builder);
你可以把他倆理解爲一對鴛鴦。
所以我們編寫的代碼應該類似於這樣
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/", async context =>
{
await context.Response.WriteAsync("Hello World!");
});
});
嗯?怎麼這裏又出現一個Map????啥????這個和app直接map有啥區別??
其實本質上沒啥區別,這裏的map 匹配的是路由字符串。Look at this!
//
// 摘要:
// Adds a Microsoft.AspNetCore.Routing.RouteEndpoint to the Microsoft.AspNetCore.Routing.IEndpointRouteBuilder
// that matches HTTP requests for the specified pattern.
//
// 參數:
// endpoints:
// The Microsoft.AspNetCore.Routing.IEndpointRouteBuilder to add the route to.
//
// pattern:
// The route pattern.
//
// requestDelegate:
// The delegate executed when the endpoint is matched.
//
// 返回結果:
// A Microsoft.AspNetCore.Builder.IEndpointConventionBuilder that can be used to
// further customize the endpoint.
public static IEndpointConventionBuilder Map(this IEndpointRouteBuilder endpoints, RoutePattern pattern, RequestDelegate requestDelegate);
//
// 摘要:
// Adds a Microsoft.AspNetCore.Routing.RouteEndpoint to the Microsoft.AspNetCore.Routing.IEndpointRouteBuilder
// that matches HTTP requests for the specified pattern.
//
// 參數:
// endpoints:
// The Microsoft.AspNetCore.Routing.IEndpointRouteBuilder to add the route to.
//
// pattern:
// The route pattern.
//
// requestDelegate:
// The delegate executed when the endpoint is matched.
//
// 返回結果:
// A Microsoft.AspNetCore.Builder.IEndpointConventionBuilder that can be used to
// further customize the endpoint.
public static IEndpointConventionBuilder Map(this IEndpointRouteBuilder endpoints, string pattern, RequestDelegate requestDelegate);
第一個參數換成了RoutePattern,哦吼!這不就是之前老版使用的路由嘛!
微軟還在此基礎上擴展出瞭如下方法。
public static IEndpointConventionBuilder Map(this IEndpointRouteBuilder endpoints, string pattern, RequestDelegate requestDelegate);
public static IEndpointConventionBuilder MapDelete(this IEndpointRouteBuilder endpoints, string pattern, RequestDelegate requestDelegate);
public static IEndpointConventionBuilder MapGet(this IEndpointRouteBuilder endpoints, string pattern, RequestDelegate requestDelegate);
public static IEndpointConventionBuilder MapMethods(this IEndpointRouteBuilder endpoints, string pattern, IEnumerable<string> httpMethods, RequestDelegate requestDelegate);
public static IEndpointConventionBuilder MapPost(this IEndpointRouteBuilder endpoints, string pattern, RequestDelegate requestDelegate);
public static IEndpointConventionBuilder MapPut(this IEndpointRouteBuilder endpoints, string pattern, RequestDelegate requestDelegate);
類似,只不過區分了Http請求方法。看完這些你再看其他的擴展方法,就會容易理解多啦