• <button id="eiyoe"><acronym id="eiyoe"></acronym></button>
    <em id="eiyoe"></em>

  • <rp id="eiyoe"><acronym id="eiyoe"><input id="eiyoe"></input></acronym></rp>
      查看: 128|回復: 0
      上一主題 下一主題

      理解ASP.NET Core 中間件(Middleware)

      79910

      主題

      0

      好友

      積分

      離線 發信

      跳轉到指定樓層
      樓主
      發表于 2021-09-17 10:50 | 只看該作者 | 倒序瀏覽
      目錄
      • 中間件
      • 中間件管道
        • Run
        • Use
        • UseWhen
        • Map
        • MapWhen
        • Run & Use & UseWhen & Map & Map
      • 編寫中間件并激活
        • 基于約定的中間件
        • 基于工廠的中間件
        • 基于約定的中間件 VS 基于工廠的中間件

      中間件

      先借用微軟官方文檔的一張圖:

      可以看到,中間件實際上是一種配置在HTTP請求管道中,用來處理請求和響應的組件。它可以:

      • 決定是否將請求傳遞到管道中的下一個中間件
      • 可以在管道中的下一個中間件處理之前和之后進行操作

      此外,中間件的注冊是有順序的,書寫代碼時一定要注意!

      中間件管道

      Run

      該方法為HTTP請求管道添加一個中間件,并標識該中間件為管道終點,稱為終端中間件。也就是說,該中間件就是管道的末尾,在該中間件之后注冊的中間件將永遠都不會被執行。所以,該方法一般只會書寫在Configure方法末尾。

      public class Startup
      {
          public void Configure(IApplicationBuilder app)
          {
              app.Run(async context =>
              {
                  await context.Response.WriteAsync("Hello, World!");
              });
          }
      }

      Use

      通過該方法快捷的注冊一個匿名的中間件

      public class Startup
      {
          public void Configure(IApplicationBuilder app)
          {
              app.Use(async (context, next) =>
              {
                  // 下一個中間件處理之前的操作
                  Console.WriteLine("Use Begin");
                  
                  await next();
                  
                  // 下一個中間件處理完成后的操作
                  Console.WriteLine("Use End");
              });
          }
      }
      

      注意:

      • 1.如果要將請求發送到管道中的下一個中間件,一定要記得調用next.Invoke / next(),否則會導致管道短路,后續的中間件將不會被執行
      • 2.在中間件中,如果已經開始給客戶端發送Response,請千萬不要調用next.Invoke / next(),也不要對Response進行任何更改,否則,將拋出異常。
      • 3.可以通過context.Response.HasStarted來判斷響應是否已開始。

      以下都是錯誤的代碼寫法

      錯誤1:

      public class Startup
      {
          public void Configure(IApplicationBuilder app)
          {
              app.Use(async (context, next) =>
              {
                  await context.Response.WriteAsync("Use");
                  await next();
              });
      
              app.Run(context =>
              {
                  // 由于上方的中間件已經開始 Response,此處更改 Response Header 會拋出異常
                  context.Response.Headers.Add("test", "test");
                  return Task.CompletedTask;
              });
          }
      }

      錯誤2

      public class Startup
      {
          public void Configure(IApplicationBuilder app)
          {
              app.Use(async (context, next) =>
              {
                  await context.Response.WriteAsync("Use");
                  
                  // 即使沒有調用 next.Invoke / next(),也不能在 Response 開始后對 Response 進行更改
                  context.Response.Headers.Add("test", "test");
              });
          }
      }

      UseWhen

      通過該方法針對不同的邏輯條件創建管道分支。需要注意的是:

      進入了管道分支后,如果管道分支不存在管道短路或終端中間件,則會再次返回到主管道。

      當使用PathString時,路徑必須以“/”開頭,且允許只有一個'/'字符

      支持嵌套,即UseWhen中嵌套UseWhen等

      支持同時匹配多個段,如 /get/user

      public class Startup
      {
          public void Configure(IApplicationBuilder app)
          {
              // /get 或 /get/xxx 都會進入該管道分支
              app.UseWhen(context => context.Request.Path.StartsWithSegments("/get"), app =>
              {
                  app.Use(async (context, next) =>
                  {
                      Console.WriteLine("UseWhen:Use");
      
                      await next();
                  });
              });
              
              app.Use(async (context, next) =>
              {
                  Console.WriteLine("Use");
      
                  await next();
              });
      
              app.Run(async context =>
              {
                  Console.WriteLine("Run");
      
                  await context.Response.WriteAsync("Hello World!");
              });
          }
      }

      當訪問 /get 時,輸出如下:

      UseWhen:Use
      Use
      Run

      如果你發現輸出了兩遍,別慌,看看是不是瀏覽器發送了兩次請求,分別是 /get 和 /favicon.ico

      Map

      • 通過該方法針對不同的請求路徑創建管道分支。需要注意的是:
      • 一旦進入了管道分支,則不會再回到主管道。
      • 使用該方法時,會將匹配的路徑從HttpRequest.Path 中刪除,并將其追加到HttpRequest.PathBase中。
      • 路徑必須以“/”開頭,且不能只有一個'/'字符
      • 支持嵌套,即Map中嵌套Map、MapWhen(接下來會講)等
      • 支持同時匹配多個段,如 /post/user
      public class Startup
      {
          public void Configure(IApplicationBuilder app)
          {
              // 訪問 /get 時會進入該管道分支
              // 訪問 /get/xxx 時會進入該管道分支
              app.Map("/get", app =>
              {
                  app.Use(async (context, next) =>
                  {
                      Console.WriteLine("Map get: Use");
                      Console.WriteLine($"Request Path: {context.Request.Path}"); 
                      Console.WriteLine($"Request PathBase: {context.Request.PathBase}");
              
                      await next();
                  });
              
                  app.Run(async context =>
                  {
                      Console.WriteLine("Map get: Run");
              
                      await context.Response.WriteAsync("Hello World!");
                  });
              
              });
              
              // 訪問 /post/user 時會進入該管道分支
              // 訪問 /post/user/xxx 時會進入該管道分支
              app.Map("/post/user", app =>
              {
                  // 訪問 /post/user/student 時會進入該管道分支
                  // 訪問 /post/user/student/1 時會進入該管道分支
                  app.Map("/student", app =>
                  {
                      app.Run(async context =>
                      {
                          Console.WriteLine("Map /post/user/student: Run");
                          Console.WriteLine($"Request Path: {context.Request.Path}");
                          Console.WriteLine($"Request PathBase: {context.Request.PathBase}");
              
                          await context.Response.WriteAsync("Hello World!");
                      });
                  });
                  
                  app.Use(async (context, next) =>
                  {
                      Console.WriteLine("Map post/user: Use");
                      Console.WriteLine($"Request Path: {context.Request.Path}");
                      Console.WriteLine($"Request PathBase: {context.Request.PathBase}");
                      
                      await next();
                  });
              
                  app.Run(async context =>
                  {
                      Console.WriteLine("Map post/user: Run");
              
                      await context.Response.WriteAsync("Hello World!");
                  });
              });
          }
      }

      當你訪問 /get/user 時,輸出如下:

      Map get: Use
      Request Path: /user
      Request PathBase: /get
      Map get: Run

      當你訪問 /post/user/student/1 時,輸出如下:

      Map /post/user/student: Run
      Request Path: /1
      Request PathBase: /post/user/student

      其他情況交給你自己去嘗試啦!

      MapWhen

      Map類似,只不過MapWhen不是基于路徑,而是基于邏輯條件創建管道分支。注意事項如下:

      • 一旦進入了管道分支,則不會再回到主管道。
      • 當使用PathString時,路徑必須以“/”開頭,且允許只有一個'/'字符
      • HttpRequest.PathHttpRequest.PathBase不會像Map那樣進行特別處理
      • 支持嵌套,即MapWhen中嵌套MapWhen、Map等
      • 支持同時匹配多個段,如 /get/user
      public class Startup
      {
          public void Configure(IApplicationBuilder app)
          {
              // /get 或 /get/xxx 都會進入該管道分支
              app.MapWhen(context => context.Request.Path.StartsWithSegments("/get"), app =>
              {
                  app.MapWhen(context => context.Request.Path.ToString().Contains("user"), app =>
                  {
                      app.Use(async (context, next) =>
                      {
                          Console.WriteLine("MapWhen get user: Use");
      
                          await next();
                      });
                  });
              
                  app.Use(async (context, next) =>
                  {
                      Console.WriteLine("MapWhen get: Use");
              
                      await next();
                  });
              
                  app.Run(async context =>
                  {
                      Console.WriteLine("MapWhen get: Run");
              
                      await context.Response.WriteAsync("Hello World!");
                  });
              });
          }
      }

      當你訪問 /get/user 時,輸出如下:

      MapWhen get user: Use

      可以看到,即使該管道分支沒有終端中間件,也不會回到主管道。

      Run & Use & UseWhen & Map & Map

      一下子接觸了4個命名相似的、與中間件管道有關的API,不知道你有沒有暈倒,沒關系,我來幫大家總結一下:

      • Run用于注冊終端中間件,Use用來注冊匿名中間件,UseWhen、Map、MapWhen用于創建管道分支。
      • UseWhen進入管道分支后,如果管道分支中不存在短路或終端中間件,則會返回到主管道。MapMapWhen進入管道分支后,無論如何,都不會再返回到主管道。
      • UseWhenMapWhen基于邏輯條件來創建管道分支,而Map基于請求路徑來創建管道分支,且會對HttpRequest.PathHttpRequest.PathBase進行處理。

      編寫中間件并激活

      上面已經提到過的RunUse就不再贅述了。

      基于約定的中間件

      “約定大于配置”,先來個約法三章:

      • 1.擁有公共(public)構造函數,且該構造函數至少包含一個類型為RequestDelegate的參數
      • 2.擁有名為InvokeInvokeAsync的公共(public)方法,必須包含一個類型為HttpContext的方法參數,且該參數必須位于第一個參數的位置,另外該方法必須返回Task類型。
      • 3.構造函數中的其他參數可以通過依賴注入(DI)填充,也可以通過UseMiddleware傳參進行填充。

      通過DI填充時,只能接收 Transient 和 Singleton 的DI參數。這是由于中間件是在應用啟動時構造的(而不是按請求構造),所以當出現 Scoped 參數時,構造函數內的DI參數生命周期與其他不共享,如果想要共享,則必須將Scoped DI參數添加到Invoke/InvokeAsync來進行使用。

      通過UseMiddleware傳參時,構造函數內的DI參數和非DI參數順序沒有要求,傳入UseMiddleware內的參數順序也沒有要求,但是我建議將非DI參數放到前面,DI參數放到后面。(這一塊感覺微軟做的好牛皮)

      • 4.Invoke/InvokeAsync的其他參數也能夠通過依賴注入(DI)填充,可以接收 Transient、Scoped 和 Singleton 的DI參數。

      一個簡單的中間件如下:

      public class MyMiddleware
      {
          // 用于調用管道中的下一個中間件
          private readonly RequestDelegate _next;
      
          public MyMiddleware(
              RequestDelegate next,
              ITransientService transientService,
              ISingletonService singletonService)
          {
              _next = next;
          }
      
          public async Task InvokeAsync(
              HttpContext context,
              ITransientService transientService,
              IScopedService scopedService,
              ISingletonService singletonService)
          {
              // 下一個中間件處理之前的操作
              Console.WriteLine("MyMiddleware Begin");
              
              await _next(context);
              
              // 下一個中間件處理完成后的操作
              Console.WriteLine("MyMiddleware End");
          }
      }

      然后,你可以通過UseMiddleware方法將其添加到管道中

      public class Startup
      {
          public void Configure(IApplicationBuilder app)
          {
              app.UseMiddleware<MyMiddleware>();
          }
      }

      不過,一般不推薦直接使用UseMiddleware,而是將其封裝到擴展方法中

      public static class AppMiddlewareApplicationBuilderExtensions
      {
          public static IApplicationBuilder UseMy(this IApplicationBuilder app) => app.UseMiddleware<MyMiddleware>();
      }
      
      public class Startup
      {
          public void Configure(IApplicationBuilder app)
          {
              app.UseMy();
          }
      }
      

      基于工廠的中間件

      優勢:

      • 按照請求進行激活。這個就是說,上面基于約定的中間件實例是單例的,但是基于工廠的中間件,可以在依賴注入時設置中間件實例的生命周期。
      • 使中間件強類型化(因為其實現了接口IMiddleware

      該方式的實現基于IMiddlewareFactoryIMiddleware。先來看一下接口定義:

      public interface IMiddlewareFactory
      {
          IMiddleware? Create(Type middlewareType);
      
          void Release(IMiddleware middleware);
      }
      
      public interface IMiddleware
      {
          Task InvokeAsync(HttpContext context, RequestDelegate next);
      }

      你有沒有想過當我們調用UseMiddleware時,它是如何工作的呢?事實上,UseMiddleware擴展方法會先檢查中間件是否實現了IMiddleware接口。 如果實現了,則使用容器中注冊的IMiddlewareFactory實例來解析該IMiddleware的實例(這下你知道為什么稱為“基于工廠的中間件”了吧)。如果沒實現,那么就使用基于約定的中間件邏輯來激活中間件。

      注意,基于工廠的中間件,在應用的服務容器中一般注冊為 Scoped 或 Transient 服務。

      這樣的話,咱們就可以放心的將 Scoped 服務注入到中間件的構造函數中了。

      接下來,咱們就來實現一個基于工廠的中間件:

      public class YourMiddleware : IMiddleware
      {
          public async Task InvokeAsync(HttpContext context, RequestDelegate next)
          {
              // 下一個中間件處理之前的操作
              Console.WriteLine("YourMiddleware Begin");
      
              await next(context);
      
              // 下一個中間件處理完成后的操作
              Console.WriteLine("YourMiddleware End");
          }
      }
      
      public static class AppMiddlewareApplicationBuilderExtensions
      {
          public static IApplicationBuilder UseYour(this IApplicationBuilder app) => app.UseMiddleware<YourMiddleware>();
      }
      

      然后,在ConfigureServices中添加中間件依賴注入

      public class Startup
      {
          public void ConfigureServices(IServiceCollection services)
          {
              services.AddTransient<YourMiddleware>();
          }
      }

      最后,在Configure中使用中間件

      public class Startup
      {
          public void Configure(IApplicationBuilder app)
          {
              app.UseYour();
          }
      }

      微軟提供了IMiddlewareFactory的默認實現:

      public class MiddlewareFactory : IMiddlewareFactory
      {
          // The default middleware factory is just an IServiceProvider proxy.
          // This should be registered as a scoped service so that the middleware instances
          // don't end up being singletons.
          // 默認的中間件工廠僅僅是一個 IServiceProvider 的代理
          // 該工廠應該注冊為 Scoped 服務,這樣中間件實例就不會成為單例
          private readonly IServiceProvider _serviceProvider;
      
          public MiddlewareFactory(IServiceProvider serviceProvider)
          {
              _serviceProvider = serviceProvider;
          }
      
          public IMiddleware? Create(Type middlewareType)
          {
              return _serviceProvider.GetRequiredService(middlewareType) as IMiddleware;
          }
      
          public void Release(IMiddleware middleware)
          {
              // The container owns the lifetime of the service
              // DI容器來管理服務的生命周期
          }
      }

      可以看到,該工廠使用過DI容器來解析出服務實例的。因此,當使用基于工廠的中間件時,是無法通過UseMiddleware向中間件的構造函數傳參的。

      基于約定的中間件 VS 基于工廠的中間件

      • 基于約定的中間件實例都是 Singleton;而基于工廠的中間件實例可以是 Singleton、Scoped 和 Transient(當然,不建議注冊為 Singleton)
      • 基于約定的中間件實例構造函數中可以通過依賴注入傳參,也可以用過UseMiddleware傳參;而基于工廠的中間件只能通過依賴注入傳參
      • 基于約定的中間件實例可以在Invoke/InvokeAsync中添加更多的依賴注入參數;而基于工廠的中間件只能按照IMiddleware的接口定義進行實現。

      到此這篇關于理解ASP.NET Core 中間件(Middleware)的文章就介紹到這了,更多相關ASP.NET Core Middleware內容請搜索腳本之家以前的文章或繼續瀏覽下面的相關文章希望大家以后多多支持腳本之家!

      來源:http://www.jb51.net/article/221607.htm