一、前言
到這篇文章爲止,關於.NET "溫故知新"系列的基礎知識就完結了,從這一系列的系統回顧和再學習,對於.NET core、ASP.NET CORE又有了一個新的認識。
不光是從使用,還包括這些知識點的原理,雖然深入原理談不上,但對於日常使用也夠了,我想的是知其然,知其所以然。
在實際開發過程中可能是知道怎麼使用就行,但系統學習了這些基本的框架、組件、或者說原理後,對於我們軟件設計、開發、擴展和解決問題還是有幫助的。
剛好到2023新年前趕着寫完,也算對自己這個系列的一個交代,實際上我平時基本不使用ASP.NET CORE,目前我主要開發桌面程序,還是用的winform。
寫這個系列的初衷是想緊跟.NET的發展進程,同時儲備基礎知識,平時還搞一些微服務(Java)、NLP、OCR、知識圖譜、前端(Vue3),只要需要反正啥都搞,沒必要固執,技術只是手段,不是目的。
那麼接下來就繼續簡單的梳理一下中間件,歡迎對這個系列拍磚!
二、中間件
中間件是一種裝配到應用管道以處理請求和響應的軟件。 每個組件:
- 選擇是否將請求傳遞到管道中的下一個組件。
- 可在管道中的下一個組件前後執行工作。
這個是關於中間件概念的概括,官方的概括是相當精準,那麼我們就圍繞管道、傳遞、組件來看看中間件。
請求委託用於生成請求管道。 請求委託處理每個 HTTP 請求。使用 Run、Map 和 Use 擴展方法來配置請求委託。
我們照例新建一個ASP.NET CORE Web API 項目:WebAPI_Middleware
namespace WebAPI_Middleware
{
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
}
}
}
在Program.cs 中我們看到前面部分builder
是配置依賴注入的東西,這部分可以參看.net 溫故知新【13】:Asp.Net Core WebAPI 使用依賴注入DI 。
app
使用Use
擴展用於中間件添加到管道中
Map
基於給定請求路徑的匹配項來創建請求管道分支
Run
委託始終爲終端,用於終止管道。
中間件的執行順序過程如下:
三、Map
我們將上面自動創建的東西全都刪除,用Map
來匹配路由,然後通過不同的代理處理請求。
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
//匹配map1 請求
app.Map("/map1", new Action<IApplicationBuilder>((app) =>
{
app.Run(async context =>
{
await context.Response.WriteAsync("map1 run");
});
}));
//匹配map2 請求
app.Map("/map2", new Action<IApplicationBuilder>((app) =>
{
app.Run(async context =>
{
await context.Response.WriteAsync("map2 run");
});
}));
app.Run();
}
}
- 請求map1 我們輸出:map1 run
- 請求map2 我們輸出:map2 run
Asp.Net Core MapControllers
的擴展方法也是類似道理,用來匹配路由調用處理程序。
四、Run
在上面的 Map 後面我們使用的處理方法中 Run 用於終止管道。也就是說在該管道中如果調用了 Run 那麼就直接返回了,即使你後面還添加了 Use 也不會執行。
app.Run(async context =>
{
await context.Response.WriteAsync("map1 run");
});
Map 相當於是迎客進門,Map 上了就用指定的管道進行處理,如果沒有 Map 上就調用主管道,也就是主管道上的其他中間件也會執行處理。比如我們再加一個 Run 用於沒匹配上路由也輸出點信息。
加了context.Response.ContentType = "text/plain; charset=utf-8";
不然中文會亂碼。
因爲 Run 是終結點,那這個管道中我還想加其他處理怎麼辦呢,這個時候就該輪到 Use 出場了。
五、Use
用 Use 將多個請求委託鏈接在一起。 next 參數表示管道中的下一個委託。 可通過不調用 next 參數使管道短路。
首先我們在外面添加兩個 Use,不放到 Map 中,這樣的話就只有未匹配到的路由會調用
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.Map("/map1", new Action<IApplicationBuilder>((app) =>
{
app.Run(async context =>
{
await context.Response.WriteAsync("map1 run");
});
}));
app.Map("/map2", new Action<IApplicationBuilder>((app) =>
{
app.Run(async context =>
{
await context.Response.WriteAsync("map2 run");
});
}));
//Use1
app.Use(async (context, next) =>
{
context.Response.ContentType = "text/plain; charset=utf-8";
await context.Response.WriteAsync("第 1 個Use 開始!\r\n", Encoding.UTF8);
await next();
await context.Response.WriteAsync("第 1 個Use 結束!\r\n", Encoding.UTF8);
});
//Use2
app.Use(async (context, next) =>
{
await context.Response.WriteAsync("第 2 個Use 開始!\r\n", Encoding.UTF8);
await next();
await context.Response.WriteAsync("第 2 個Use 結束!\r\n", Encoding.UTF8);
});
//結束管道處理
app.Run(async context =>
{
await context.Response.WriteAsync("未匹配處理!\r\n", Encoding.UTF8);
});
app.Run();
}
}
最後執行的路徑和最開始的圖是一致的。
爲什麼將context.Response.ContentType = "text/plain; charset=utf-8";
放到第一個 Use 呢,因爲如果放到 Run 裏面會報錯,改變了 Header 標頭。所以理論上也不要在 Use 裏面發送響應WriteAsync,此處爲了演示所以這麼寫。
六、中間件類
上面的代理方法可以移動到類中,這個類就是中間件類。中間件類需要如下要求:
- 具有類型爲 RequestDelegate 的參數的公共構造函數。
- 名爲 Invoke 或 InvokeAsync 的公共方法。 此方法必須:
返回 Task。
接受類型 HttpContext 的第一個參數。
構造函數和 Invoke/InvokeAsync 的其他參數由依賴關係注入 (DI) 填充。
將上面的未匹配路由處理邏輯移動到中間件類中:
- TestMiddleware1:
public class TestMiddleware1
{
private readonly RequestDelegate _next;
public TestMiddleware1(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
context.Response.ContentType = "text/plain; charset=utf-8";
await context.Response.WriteAsync("第 1 個Use 開始!\r\n", Encoding.UTF8);
await _next(context);
await context.Response.WriteAsync("第 1 個Use 結束!\r\n", Encoding.UTF8);
}
}
- TestMiddleware2
public class TestMiddleware2
{
private readonly RequestDelegate _next;
public TestMiddleware2(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
await context.Response.WriteAsync("第 2 個Use 開始!\r\n", Encoding.UTF8);
await _next(context);
await context.Response.WriteAsync("第 2 個Use 結束!\r\n", Encoding.UTF8);
}
}
-
Program
-
運行
此處的中間件使用有順序問題,如果我先app.UseMiddleware<TestMiddleware2>()
因爲 TestMiddleware1 修改了標頭,根據約定是不允許的,所以程序是有報錯。
因此中間件組件的順序定義了針對請求調用這些組件的順序,以及響應的相反順序。 此順序對於安全性、性能和功能至關重要。
七、中間件順序
以上是內置中間件的默認順序規則,具體如何使用內置中間件,可參閱官方資料。
八、寫在最後
以上就是關於中間件的部分知識,結合我自己的理解做了前後銜接的梳理邏輯。
官方網站更多的是講解每個知識點的細節,前後需要結合起來理解,當然我還是強烈建議跟着官方文檔學習,而且是最權威最可信的:ASP.NET Core 中間件
這個系列歷時2年,工作生活都比較忙,也有放縱啥事不相干的時候,中間斷斷續續的,總算是堅持完了。很多東西就是這樣,累了就休息一下貴在堅持,即使再慢,積累的成果也有收穫。