ASP.NET Core 中间件
1.前言
中間件(middleware)是一種裝配到應(yīng)用管道以處理請(qǐng)求和響應(yīng)的組件。每個(gè)組件:
●可選擇是否將請(qǐng)求傳遞到管道中的下一個(gè)組件。
●可在管道中的下一個(gè)組件前后執(zhí)行工作。
請(qǐng)求委托(request delegates)用于建立請(qǐng)求管道(request pipeline),請(qǐng)求委托處理每個(gè)HTTP請(qǐng)求。
請(qǐng)求委托通過(guò)使用IApplicationBuilder類型的Run、Map和Use擴(kuò)展方法來(lái)配置,并在Strartup類中傳給Configure方法。每個(gè)單獨(dú)的請(qǐng)求委托都可以被指定為一個(gè)內(nèi)嵌匿名方法(稱為并行中間件,in-line middleware),或者其定義在一個(gè)可重用的類中。這些可重用的類被稱作“中間件”或“中間件組件”。請(qǐng)求管道中的每個(gè)中間件組件負(fù)責(zé)調(diào)用管道中的下一個(gè)組件,或使管道短路。當(dāng)中間件短路時(shí),它被稱為“終端中間件”(terminal middleware),因?yàn)樗柚怪虚g件進(jìn)一步處理請(qǐng)求。
2.使用 IApplicationBuilder 創(chuàng)建中間件管道
ASP.NET Core請(qǐng)求管道包含一系列請(qǐng)求委托,依次調(diào)用。下圖演示了這一概念。沿黑色箭頭執(zhí)行。
每個(gè)委托(中間件)均可在下一個(gè)委托前后執(zhí)行操作。任何委托都能選擇停止傳遞到下一個(gè)委托,轉(zhuǎn)而自己處理該請(qǐng)求,這就是請(qǐng)求管道的短路(下面會(huì)舉例說(shuō)明)。而且是一種有意義的設(shè)計(jì),因?yàn)樗梢员苊獠槐匾墓ぷ鳌1热?#xff0c;一個(gè)授權(quán)(authorization)中間件只有通過(guò)身份驗(yàn)證之后才能調(diào)用下一個(gè)委托,否則它就會(huì)被短路,并返回“Not Authorized”的響應(yīng)。所以應(yīng)盡早在管道中調(diào)用異常處理委托,這樣它們就能捕獲在管道的后期階段發(fā)生的異常。
現(xiàn)在我們來(lái)演示下用一個(gè)簡(jiǎn)單的ASP.NET Core應(yīng)用程序建立單個(gè)請(qǐng)求委托處理每個(gè)HTTP請(qǐng)求(這種情況不包括實(shí)際請(qǐng)求管道):
{
public void Configure(IApplicationBuilder app)
{
app.Run(async context =>
{
await context.Response.WriteAsync("Hello, World!");
});
}
}
響應(yīng)結(jié)果:
由上面我們可以看到,運(yùn)行時(shí)輸出的是Run委托消息,然后我們?cè)俣x多一個(gè)請(qǐng)求委托看看效果,請(qǐng)看如下代碼:
{
//第一個(gè)委托Run
app.Run(async context =>
{
await context.Response.WriteAsync("Hello, World!");
});
//第二個(gè)委托Run
app.Run(async context =>
{
await context.Response.WriteAsync("Hey, World!");
});
}
響應(yīng)結(jié)果:
由上述代碼可以看到,我們定義兩個(gè)Run委托,但是運(yùn)行第一個(gè)Run委托的時(shí)候就已經(jīng)終止了管道,這是為什么呢?
因?yàn)镽un方法又稱為短路管道(它不會(huì)調(diào)用next請(qǐng)求委托)。因此,Run方法一般在管道尾部被調(diào)用。Run是一種約定,有些中間件組件可能會(huì)暴露他們自己的Run方法,而這些方法只能在管道末尾處運(yùn)行。
讓我們?cè)賮?lái)看看如下代碼:
public void Configure(IApplicationBuilder app){
app.Use(async (context, next) =>
{
context.Response.ContentType = "text/plain; charset=utf-8";
await context.Response.WriteAsync("進(jìn)入第一個(gè)委托 執(zhí)行下一個(gè)委托之前\r\n");
//調(diào)用管道中的下一個(gè)委托
await next.Invoke();
await context.Response.WriteAsync("結(jié)束第一個(gè)委托 執(zhí)行下一個(gè)委托之后\r\n");
});
app.Run(async context =>
{
await context.Response.WriteAsync("進(jìn)入第二個(gè)委托\(zhòng)r\n");
await context.Response.WriteAsync("Hello from 2nd delegate.\r\n");
await context.Response.WriteAsync("結(jié)束第二個(gè)委托\(zhòng)r\n");
});
}
響應(yīng)結(jié)果:
通過(guò)響應(yīng)結(jié)果,我們可以看到Use方法將多個(gè)請(qǐng)求委托鏈接在一起。而next參數(shù)表示管道中的下一個(gè)委托。可通過(guò)不調(diào)用next 參數(shù)使管道短路,通常可在下一個(gè)委托前后執(zhí)行操作。
3.順序
向Startup.Configure方法添加中間件組件的順序定義了在請(qǐng)求上調(diào)用它們的順序,以及響應(yīng)的相反順序。此排序?qū)τ诎踩浴⑿阅芎凸δ苤陵P(guān)重要。
以下Startup.Configure方法將為常見應(yīng)用方案添加中間件組件:
●異常/錯(cuò)誤處理(Exception/error handling)
●HTTP嚴(yán)格傳輸安全協(xié)議(HTTP Strict Transport Security Protocol)
●HTTPS重定向(HTTPS redirection)
●靜態(tài)文件服務(wù)器(Static file server)
●Cookie策略實(shí)施(Cookie policy enforcement)
●身份驗(yàn)證(Authentication)
●會(huì)話(Session)
●MVC
請(qǐng)看如下代碼:
從上述示例代碼中,每個(gè)中間件擴(kuò)展方法都通過(guò)Microsoft.AspNetCore.Builder命名空間在 IApplicationBuilder上公開。但是為什么我們要按照這個(gè)順序去添加中間件組件呢?下面我們挑幾個(gè)中間件來(lái)了解下。
●UseExceptionHandler(異常/錯(cuò)誤處理)是添加到管道的第一個(gè)中間件組件。因此我們可以捕獲在應(yīng)用程序調(diào)用中發(fā)生的任何異常。那為什么要將異常/錯(cuò)誤處理放在第一位呢?那是因?yàn)檫@樣我們就不用擔(dān)心因前面中間件短路而導(dǎo)致捕獲不到整個(gè)應(yīng)用程序所有異常信息。
●UseStaticFiles(靜態(tài)文件)中間件在管道中提前調(diào)用,方便它可以處理請(qǐng)求和短路,而無(wú)需通過(guò)剩余中間組件。也就是說(shuō)靜態(tài)文件中間件不用經(jīng)過(guò)UseAuthentication(身份驗(yàn)證)檢查就可以直接訪問(wèn),即可公開訪問(wèn)由靜態(tài)文件中間件服務(wù)的任何文件,包括wwwroot下的文件。
●UseAuthentication(身份驗(yàn)證)僅在MVC選擇特定的Razor頁(yè)面或Controller和Action之后才會(huì)發(fā)生。
經(jīng)過(guò)上面描述,大家都了解中間件順序的重要性了吧。以下示例演示中間件的排序,其中靜態(tài)文件的請(qǐng)求在響應(yīng)壓縮中間件之前由靜態(tài)文件中間件進(jìn)行處理。靜態(tài)文件不會(huì)按照中間件的順序進(jìn)行壓縮。可以壓縮來(lái)自 UseMvcWithDefaultRoute的 MVC 響應(yīng)。示例:
{
// Static files not compressed by Static File Middleware.
app.UseStaticFiles();
app.UseResponseCompression();
app.UseMvcWithDefaultRoute();
}
4.Use、Run和Map方法
你可以使用Use、Run和Map配置HTTP管道。
●Use:Use方法可使管道短路(即不調(diào)用 next 請(qǐng)求委托)。第二節(jié)點(diǎn)有示例代碼演示。
●Run:Run是一種約定,并且某些中間件組件可公開在管道末尾運(yùn)行的Run[Middleware]方法。第二節(jié)點(diǎn)有示例代碼演示。
●Map:Map擴(kuò)展用作創(chuàng)建管道分支。Map*給請(qǐng)求路徑的匹配項(xiàng)來(lái)創(chuàng)建請(qǐng)求管道分支。如果請(qǐng)求路徑以給自定義路徑開頭,則執(zhí)行分支。
下面我們來(lái)看看這段代碼:
{
private static void HandleMapTest1(IApplicationBuilder app)
{
app.Run(async context =>
{
await context.Response.WriteAsync("Map Test 1");
});
}
private static void HandleMapTest2(IApplicationBuilder app)
{
app.Run(async context =>
{
await context.Response.WriteAsync("Map Test 2");
});
}
public void Configure(IApplicationBuilder app)
{
app.Map("/map1", HandleMapTest1);
app.Map("/map2", HandleMapTest2);
app.Run(async context =>
{
await context.Response.WriteAsync("Hello from non-Map delegate. <p>");
});
}
}
下面表格使用前面的代碼顯示來(lái)自http://localhost:5001的請(qǐng)求和響應(yīng)。
請(qǐng)求 | 響應(yīng) |
localhost:5001 | Hello from non-Map delegate. |
localhost:5001/map1 | Map Test 1 |
localhost:5001/map2 | Map Test 2 |
localhost:5001/map3 | Hello from non-Map delegate. |
由上面可以了解到當(dāng)使用Map方法時(shí),將從HttpRequest.Path中刪除匹配的路徑段,并針對(duì)每個(gè)請(qǐng)求將該路徑追加到HttpRequest.PathBase。
MapWhen基于給定謂詞的結(jié)果創(chuàng)建請(qǐng)求管道分支。Func<HttpContext, bool>類型的任何謂詞均可用于將請(qǐng)求映射到管道的新分支(HandleBranch)。在以下示例中,謂詞用于檢測(cè)查詢字符串變量branch是否存在:
{
private static void HandleBranch(IApplicationBuilder app)
{
app.Run(async context =>
{
var branchVer = context.Request.Query["branch"];
await context.Response.WriteAsync($"Branch used = {branchVer}");
});
}
public void Configure(IApplicationBuilder app)
{
app.MapWhen(context => context.Request.Query.ContainsKey("branch"),
HandleBranch);
app.Run(async context =>
{
await context.Response.WriteAsync("Hello from non-Map delegate. <p>");
});
}
}
下面表格使用前面的代碼顯示來(lái)自http://localhost:5001的請(qǐng)求和響應(yīng)。
請(qǐng)求 | 響應(yīng) |
http://localhost:5001 | Hello from non-Map delegate. <p> |
https://localhost:5001/?branch=master | Branch used = master |
Map支持嵌套,例如:
public void Configure(IApplicationBuilder app){
app.Map("/level1", level1App => {
level1App.Map("/level2a", level2AApp => {
// "/level1/level2a" processing
});
level1App.Map("/level2b", level2BApp => {
// "/level1/level2b" processing
});
});
}
此外Map 還可同時(shí)匹配多個(gè)段:
public class Startup{
private static void HandleMultiSeg(IApplicationBuilder app)
{
app.Run(async context =>
{
await context.Response.WriteAsync("Map multiple segments.");
});
}
public void Configure(IApplicationBuilder app)
{
app.Map("/map1/seg1", HandleMultiSeg);
app.Run(async context =>
{
await context.Response.WriteAsync("Hello from non-Map delegate.");
});
}
}
5.編寫中間件(重點(diǎn))
雖然ASP.NET Core為我們提供了一組豐富的內(nèi)置中間件組件,但在某些情況下,你可能需要寫入自定義中間件。
5.1中間件類
通常,中間件應(yīng)該封裝在自定義類中,并且通過(guò)擴(kuò)展方法公開。
下面我們自定義一個(gè)查詢當(dāng)前區(qū)域性的中間件:
{
public void Configure(IApplicationBuilder app)
{
app.Use((context, next) =>
{
var cultureQuery = context.Request.Query["culture"];
if (!string.IsNullOrWhiteSpace(cultureQuery))
{
var culture = new CultureInfo(cultureQuery);
CultureInfo.CurrentCulture = culture;
CultureInfo.CurrentUICulture = culture;
}
// Call the next delegate/middleware in the pipeline
return next();
});
app.Run(async (context) =>
{
await context.Response.WriteAsync(
$"Hello {CultureInfo.CurrentCulture.DisplayName}");
});
}
}
可通過(guò)傳入?yún)^(qū)域性參數(shù)測(cè)試該中間件。例如 http://localhost:7997/?culture=zh、http://localhost:7997/?culture=en。
但是為了更好管理代碼,我們應(yīng)該把委托函數(shù)移到自定義類去:
public class RequestCultureMiddleware
{
private readonly RequestDelegate _next;
public RequestCultureMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
context.Response.ContentType = "text/plain; charset=utf-8";
var cultureQuery = context.Request.Query["culture"];
if (!string.IsNullOrWhiteSpace(cultureQuery))
{
var culture = new CultureInfo(cultureQuery);
CultureInfo.CurrentCulture = culture;
CultureInfo.CurrentUICulture = culture;
}
// Call the next delegate/middleware in the pipeline
await _next(context);
}
}
5.2中間件擴(kuò)展方法
中間件擴(kuò)展方法可以通過(guò)IApplicationBuilder公開中間件。示例創(chuàng)建一個(gè)RequestCultureMiddlewareExtensions擴(kuò)展類并通過(guò)IApplicationBuilder公開:
public static class RequestCultureMiddlewareExtensions{
public static IApplicationBuilder UseRequestCulture(this IApplicationBuilder builder)
{
return builder.UseMiddleware<RequestCultureMiddleware>();
}
}
再通過(guò)Startup.Configure方法調(diào)用中間件:
public class Startup{
public void Configure(IApplicationBuilder app)
{
app.UseRequestCulture();
app.Run(async (context) =>
{
await context.Response.WriteAsync(
$"Hello {CultureInfo.CurrentCulture.DisplayName}");
});
}
}
響應(yīng)結(jié)果:
由此整個(gè)自定義ASP.NET Core中間件完成。
6.按請(qǐng)求依賴項(xiàng)
因?yàn)橹虚g件是在應(yīng)用程序啟動(dòng)時(shí)構(gòu)建的,而不是每個(gè)請(qǐng)求時(shí)構(gòu)建,所以在每個(gè)請(qǐng)求期間,中間件構(gòu)造函數(shù)使用的范圍內(nèi)生命周期服務(wù)不與其他依賴關(guān)系注入類型共享。如果您必須在中間件和其他類型之間共享作用域服務(wù),請(qǐng)將這些服務(wù)添加到Invoke方法的簽名中。Invoke方法可以接受由依賴注入(DI)填充的其他參數(shù)。示例:
public class CustomMiddleware{
private readonly RequestDelegate _next;
public CustomMiddleware(RequestDelegate next)
{
_next = next;
}
// IMyScopedService is injected into Invoke
public async Task Invoke(HttpContext httpContext, IMyScopedService svc)
{
svc.MyProperty(1000);
await _next(httpContext);
}
}
public static class CustomMiddlewareExtensions
{
public static IApplicationBuilder UseCustomMiddleware(this IApplicationBuilder builder)
{
return builder.UseMiddleware<CustomMiddleware>();
}
}
public interface IMyScopedService
{
void MyProperty(decimal input);
}
public class MyScopedService : IMyScopedService
{
public void MyProperty(decimal input)
{
Console.WriteLine("MyProperty is " + input);
}
}
public void ConfigureServices(IServiceCollection services)
{
//注入DI服務(wù)
services.AddScoped<IMyScopedService, MyScopedService>();
}
響應(yīng)結(jié)果:
參考文獻(xiàn):
ASP.NET Core中間件
寫入自定義ASP.NET Core中間件
原文地址:https://www.cnblogs.com/wzk153/p/10904988.html
.NET社區(qū)新聞,深度好文,歡迎訪問(wèn)公眾號(hào)文章匯總?http://www.csharpkit.com?
總結(jié)
以上是生活随笔為你收集整理的ASP.NET Core 中间件的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 读《代码整洁之道》
- 下一篇: .Net Core实现的文档数据库Rav