前言
Asp.NetCore中的請求管道是通過一系列的中介軟體組成的,使得請求會根據需求進行對應的過濾和加工處理。在平時開發中會時常引用別人定義好的中介軟體,只需簡單進行app.Usexxx就能完成中介軟體的註冊,但是對於一些定製化需求還得自己進行處理和封裝,以下說說中介軟體的註冊應用和自定義中介軟體;
正文
在上一小節中有簡單提到,當註冊第三方封裝的中介軟體時,其實本質還是呼叫了IApplicationBuilder的Use方法;而在開發過程中,會使用以下三種方式進行中介軟體的註冊:
- Use:通過Use的方式註冊中介軟體,可以控制是否將請求傳遞到下一個中介軟體;
- Run:通過Run的方式註冊中介軟體,一般用於斷路或請求管道末尾,即不會將請求傳遞下去;
- Map/MapWhen:請求管道中增加分支,條件滿足之後就由分支管道進行處理,而不會切換回主管道;Map用於請求路徑匹配,而MapWhen可以有更多的條件進行過濾;
- UseMiddleWare : 一般用於註冊自定義封裝的中介軟體,內部其實是使用Use的方式進行中介軟體註冊;
相信都知道我的套路了,光說不練假把式,來一個Asp.NetCore API專案進行以上幾種中介軟體註冊方式演示:
圖中程式碼部分將原先預設註冊的中介軟體刪除了,用Use和Run的方式分別註冊了兩個中介軟體(這裡只是簡單的顯示文字,裡面可以根據需求新增相關邏輯),其中用Use註冊的方式在上一節中已經提及到,直接將中介軟體新增連結串列中,這裡就不再贅述了;
對於使用Run方式註冊中間,小夥伴們肯定不甘心止於此吧,所以這裡直接看Run是如何實現:
namespace Microsoft.AspNetCore.Builder
{
public static class RunExtensions
{
// 也是一個擴充套件方法,但引數就是一個委託
public static void Run(this IApplicationBuilder app, RequestDelegate handler)
{
// 引數校驗,如果null就丟擲異常
if (app == null)
{
throw new ArgumentNullException(nameof(app));
}
// 傳入的委託校驗,如果null也是丟擲異常
if (handler == null)
{
throw new ArgumentNullException(nameof(handler));
}
// 這裡其實只有一個 RequestDelegate執行邏輯,並沒有傳遞功能
// 本質也是使用方法Use
app.Use(_ => handler);
}
}
}
通過程式碼可知,用Run方式只是將處理邏輯RequestDelegate傳入,並沒有傳遞的邏輯,所以Run註冊的中介軟體就會形成斷路,導致後面的中介軟體不能再執行了;
使用Map和MapWhen註冊的方式,其實是給管道開一個分支,就像高速公路一樣,有匝道,到了對應出口就進匝道了,就不能倒車回來了(倒回來扣你12分);同樣,請求管道也是,當條件滿足時,請求就走Map對應的分支管道,就不能重新返回主管道了;
程式碼走一波,在註冊中介軟體的地方增加Map的使用:
Configure全部程式碼如下:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// 使用Use註冊中間
app.Use(async (context, next) => {
await context.Response.WriteAsync("Hello Use1\r\n");
// 將請求傳遞到下一個中介軟體
await next();
await context.Response.WriteAsync("Hello Use1 Response\r\n");
});
// 使用Use註冊中間 引數型別不一樣
app.Use(requestDelegate =>
{
return async (context) =>
{
await context.Response.WriteAsync("Hello Use2\r\n");
// 將請求傳遞到下一個中介軟體
await requestDelegate(context);
await context.Response.WriteAsync("Hello Use2 Response\r\n");
};
});
// 分支管道,只有匹配到路徑才走分支管道
app.Map("/Hello", builder =>
{
builder.Use(async (context, next) =>
{
await context.Response.WriteAsync("Hello MapUse\r\n");
// 將請求傳遞到分支管道的下一個中介軟體
await next();
await context.Response.WriteAsync("Hello MapUse Response\r\n");
});
// 註冊分支管道中介軟體
builder.Run(async context => {
await context.Response.WriteAsync("Hello MapRun1~~~\r\n");
});
// 註冊分支管道中介軟體
builder.Run(async context => {
await context.Response.WriteAsync("Hello MapRun2~~~\r\n");
});
});
// 使用Run
app.Run(async context => {
await context.Response.WriteAsync("Hello Run~~~\r\n");
});
//使用Run註冊
app.Run(async context => {
await context.Response.WriteAsync("Hello Code綜藝圈~~~\r\n");
});
}
執行看效果:
Map方式註冊的分支管道只有路徑匹配了才走,否則都會走主管道;
仔細的小夥伴肯定會說,那是在分支管道上用了Run註冊中介軟體了,形成了斷路,所以導致不能執行主管道剩下的中介軟體,好,那我們稍微改改程式碼:
這樣執行訪問分支管道時會報錯,因為分支管道中沒有下一個中介軟體了,還呼叫下一個中介軟體,那肯定有問題;
改了改,如下執行:
進入匝道還想倒回來,12分不要了嗎,哈哈哈;
MapWhen註冊的分支管道邏輯和Map差不多類似,只是匹配的條件更加靈活而已,可以根據自己需求進行調節匹配,如下:
看到這,小夥伴應該都知道,接下來肯定不會放過Map/MapWhen的實現:
-
Map
public static IApplicationBuilder Map(this IApplicationBuilder app, PathString pathMatch, Action<IApplicationBuilder> configuration) { // 進行引數校驗 IApplicationBuilder物件不能為空 if (app == null) { throw new ArgumentNullException(nameof(app)); } // 進行引數校驗 傳進的委託物件不能為空 if (configuration == null) { throw new ArgumentNullException(nameof(configuration)); } // 匹配的路徑末尾不能有"/",否則就拋異常 if (pathMatch.HasValue && pathMatch.Value.EndsWith("/", StringComparison.Ordinal)) { throw new ArgumentException("The path must not end with a '/'", nameof(pathMatch)); } // 克隆一個IApplicationBuilder,共用之前的屬性,這裡其實建立了分支管道 var branchBuilder = app.New(); // 將建立出來的branchBuilder進行相關配置 configuration(branchBuilder); // 構造出分支管道 var branch = branchBuilder.Build(); // 將構造出來的管道和匹配路徑進行封裝 var options = new MapOptions { Branch = branch, PathMatch = pathMatch, }; // 註冊中介軟體 return app.Use(next => new MapMiddleware(next, options).Invoke); } // MapMiddleware 的Invoke方法,及如何進入分支管道處理的 public async Task Invoke(HttpContext context) { // 引數判斷 if (context == null) { throw new ArgumentNullException(nameof(context)); } PathString matchedPath; PathString remainingPath; // 判斷是否匹配路徑,如果匹配上就進入分支 if (context.Request.Path.StartsWithSegments(_options.PathMatch, out matchedPath, out remainingPath)) { // 更新請求地址 var path = context.Request.Path; var pathBase = context.Request.PathBase; context.Request.PathBase = pathBase.Add(matchedPath); context.Request.Path = remainingPath; try { // 進入分支管道 await _options.Branch(context); } finally { // 恢復原先請求地址,回到主管道之後,並沒有進行主管道也下一個中介軟體的傳遞,所以主管道後續不在執行 context.Request.PathBase = pathBase; context.Request.Path = path; } } else { // 匹配不到路徑就繼續主管道執行 await _next(context); } }
-
MapWhen:其實和Map差不多,只是傳入的匹配規則不一樣,比較靈活:
public static IApplicationBuilder MapWhen(this IApplicationBuilder app, Predicate predicate, Action<IApplicationBuilder> configuration) { if (app == null) { throw new ArgumentNullException(nameof(app)); } if (predicate == null) { throw new ArgumentNullException(nameof(predicate)); } if (configuration == null) { throw new ArgumentNullException(nameof(configuration)); } // 構建分支管道,和Map一致 var branchBuilder = app.New(); configuration(branchBuilder); var branch = branchBuilder.Build(); // 封裝匹配規則 var options = new MapWhenOptions { Predicate = predicate, Branch = branch, }; // 註冊中介軟體 return app.Use(next => new MapWhenMiddleware(next, options).Invoke); } // MapWhenMiddleware 的Invoke方法 public async Task Invoke(HttpContext context) { // 引數校驗 if (context == null) { throw new ArgumentNullException(nameof(context)); } // 判斷是否匹配規則,如果匹配就進入分支管道 if (_options.Predicate(context)) { await _options.Branch(context); } else { // 沒有匹配就繼續執行主管道 await _next(context); } }
現在是不是清晰明瞭多了,不懵了吧;還沒完呢,繼續往下;
上面註冊中介軟體的方式是不是有點不那麼好看,當中介軟體多了時候,可讀性很是頭疼,維護性也得花點功夫,所以微軟肯定想到這了,提供了類的方式進行中介軟體的封裝(但是要按照約定來),從而可以像使用第三方中介軟體那樣簡單,如下:
使用及執行:
是不是自定義也沒想象中那麼難,其中註冊封裝的中介軟體時,在擴充套件方法中使用了app.UseMiddleware
// 看著呼叫的方法
public static IApplicationBuilder UseMiddleware<TMiddleware>(this IApplicationBuilder app, params object[] args)
{
// 內部呼叫了以下方法
return app.UseMiddleware(typeof(TMiddleware), args);
}
// 其實這裡是對自定義中介軟體的註冊,這裡可以不用太深入瞭解
public static IApplicationBuilder UseMiddleware(this IApplicationBuilder app, Type middleware, params object[] args)
{
if (typeof(IMiddleware).GetTypeInfo().IsAssignableFrom(middleware.GetTypeInfo()))
{
// 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;
// 反編譯進行包裝成註冊中介軟體的樣子(Func<ReuqestDelegate,RequestDelegate>),但可以看到本質使用IApplicationBuilder中Use方法
return app.Use(next =>
{
// 獲取指定型別中的方法列表
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();
// 如果有多個方法 ,就丟擲異常,這裡保證方法的唯一
if (invokeMethods.Length > 1)
{
throw new InvalidOperationException(Resources.FormatException_UseMiddleMutlipleInvokes(InvokeMethodName, InvokeAsyncMethodName));
}
// 如果沒有找到,也就丟擲異常
if (invokeMethods.Length == 0)
{
throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNoInvokeMethod(InvokeMethodName, InvokeAsyncMethodName, middleware));
}
// 取得唯一的方法Invoke或是InvokeAsync方法
var methodInfo = invokeMethods[0];
// 判斷型別是否返回Task,如果不是就丟擲異常,要求返回Task的目的是為了後續包裝RequestDelegate
if (!typeof(Task).IsAssignableFrom(methodInfo.ReturnType))
{
throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNonTaskReturnType(InvokeMethodName, InvokeAsyncMethodName, nameof(Task)));
}
// 判斷方法的引數,引數的第一個引數必須是HttpContext型別
var parameters = methodInfo.GetParameters();
if (parameters.Length == 0 || parameters[0].ParameterType != typeof(HttpContext))
{
throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNoParameters(InvokeMethodName, InvokeAsyncMethodName, nameof(HttpContext)));
}
// 開始構造RequestDelegate物件
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);
// 如果引數只有一個HttpContext 就包裝成一個RequestDelegate返回
if (parameters.Length == 1)
{
return (RequestDelegate)methodInfo.CreateDelegate(typeof(RequestDelegate), instance);
}
// 如果引數有多個的情況就單獨處理,這裡不詳細進去了
var factory = Compile<object>(methodInfo, parameters);
return context =>
{
var serviceProvider = context.RequestServices ?? applicationServices;
if (serviceProvider == null)
{
throw new InvalidOperationException(Resources.FormatException_UseMiddlewareIServiceProviderNotAvailable(nameof(IServiceProvider)));
}
return factory(instance, context, serviceProvider);
};
});
}
可以看出,框架將我們封裝的中介軟體類進行了反射獲取對應的方法和屬性,然後封裝成中介軟體(Func<RequestDelegate,RequestDelegate>)的樣子,從而是得編碼更加方便,中介軟體更容易分類管理了;通過以上程式碼註釋也能看出在封裝中介軟體的時候對應的約定,哈哈哈,是不是得重新看一遍程式碼(如果這樣,目標達到了);對了,框架提供了IMiddleware了介面,實現中介軟體的時候可以實現,但是約定還是一個不能少;
總結
我去,不能熬了,再熬明天起不來跑步了;這篇內容有點多,之所以沒分開,感覺關聯性比較強,一口氣看下來比較合適;下一節說說檔案相關的點;
---------------------------------------------------
CSDN:Code綜藝圈
知乎:Code綜藝圈
掘金:Code綜藝圈
部落格園:Code綜藝圈
bilibili:Code綜藝圈
---------------------------------------------------
一個被程式搞醜的帥小夥,關注"Code綜藝圈",識別關注跟我一起學~~~
擼文不易,莫要白瞟,三連走起~~~~