.net 溫故知新【17】:Asp.Net Core WebAPI 中間件

一、前言

到這篇文章爲止,關於.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 委託始終爲終端,用於終止管道。

中間件的執行順序過程如下:

image

三、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

image

  • 請求map2 我們輸出:map2 run

image

Asp.Net Core MapControllers 的擴展方法也是類似道理,用來匹配路由調用處理程序。

四、Run

在上面的 Map 後面我們使用的處理方法中 Run 用於終止管道。也就是說在該管道中如果調用了 Run 那麼就直接返回了,即使你後面還添加了 Use 也不會執行。

app.Run(async context =>
{
    await context.Response.WriteAsync("map1 run");
});

Map 相當於是迎客進門,Map 上了就用指定的管道進行處理,如果沒有 Map 上就調用主管道,也就是主管道上的其他中間件也會執行處理。比如我們再加一個 Run 用於沒匹配上路由也輸出點信息。

image

加了context.Response.ContentType = "text/plain; charset=utf-8"; 不然中文會亂碼。

image

因爲 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();
        }
    }

最後執行的路徑和最開始的圖是一致的。

image

爲什麼將context.Response.ContentType = "text/plain; charset=utf-8"; 放到第一個 Use 呢,因爲如果放到 Run 裏面會報錯,改變了 Header 標頭。所以理論上也不要在 Use 裏面發送響應WriteAsync,此處爲了演示所以這麼寫。

image

六、中間件類

上面的代理方法可以移動到類中,這個類就是中間件類。中間件類需要如下要求:

  • 具有類型爲 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
    image

  • 運行
    image

此處的中間件使用有順序問題,如果我先app.UseMiddleware<TestMiddleware2>() 因爲 TestMiddleware1 修改了標頭,根據約定是不允許的,所以程序是有報錯。

image

因此中間件組件的順序定義了針對請求調用這些組件的順序,以及響應的相反順序。 此順序對於安全性、性能和功能至關重要。

七、中間件順序

image

以上是內置中間件的默認順序規則,具體如何使用內置中間件,可參閱官方資料。

八、寫在最後

以上就是關於中間件的部分知識,結合我自己的理解做了前後銜接的梳理邏輯。

官方網站更多的是講解每個知識點的細節,前後需要結合起來理解,當然我還是強烈建議跟着官方文檔學習,而且是最權威最可信的:ASP.NET Core 中間件

這個系列歷時2年,工作生活都比較忙,也有放縱啥事不相干的時候,中間斷斷續續的,總算是堅持完了。很多東西就是這樣,累了就休息一下貴在堅持,即使再慢,積累的成果也有收穫。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章