# ASP<span/>.NET Core Middleware [Microsoft documentation](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?view=aspnetcore-6.0) ::: spoiler Runtime Runtime and SDK Version: `.NET 6` ::: Middleware(中介軟體)是組合進應用程式 pipeline(管線),負責處理請求(request)和回覆(response)的軟體。每個元件(component)都能 - 選擇是否將請求交給下一個在 pipeline 內的元件。 - 能夠在 pipeline 中的下個元件執行之前和執行之後進行一些處理。 Request delegates(請求委派)被用來建立 request pipeline。Request delegates 會處理每個 HTTP 請求。 Request delegates 用 [Run](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.builder.runextensions.run)、[Map](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.builder.mapextensions.map) 和 [Use](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.builder.useextensions.use) 的 extension methods 來設定。每個獨立的 request delegate 可以被指定為行內(in-line)匿名方法(稱為 in-line middleware),或是將其定義為可重複使用的 class。這些可重用的 classes 和 in-line anonymous method 稱為 *middleware*,也稱為 *middleware component*。每個在 request pipeline 內的中介軟體元件負責調用 pipeline 中的下一個元件或是提早中斷(short-circuiting,直譯為短路) pipeline。當一個中介軟體 short circuit,它會被稱為 *terminal middleware*,因為它避免了更多和更內部的 middleware 處理請求。 Request pipeline 由一連串的 request delegates 組成,而且一個接著一個的呼叫。下圖黑色箭頭為執行的過程: ![Request delegate pipeline](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware/index/_static/request-delegate-pipeline.png) 例外處理的 delegate 應該早一點在 pipeline 呼叫,這樣它們才能接到該 pipeline 中稍後發生的 exception。 Minimal API 類型 (使用 `app.Use()`) 的中介軟體: ```csharp= var builder = WebApplication.CreateBuilder(args); var app = builder.Build(); app.Use(async (context, next) => { // Do work that can write to the Response. await next.Invoke(); // Do logging or other work that doesn't write to the Response. }); app.Run(); ``` ## Middleware 執行順序 ![Middleware order](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware/index/_static/middleware-pipeline.svg) 上圖 middleware 的執行順序,就是根據 `Program.cs` 呼叫的順序決定的,因此只要你更改程式碼呼叫 middleware 的順序,上圖的順序就會跟著改變。 ```csharp if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Home/Error"); // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); // app.UseCookiePolicy(); app.UseRouting(); // app.UseRequestLocalization(); // app.UseCors(); app.UseAuthentication(); app.UseAuthorization(); // app.UseSession(); // app.UseResponseCompression(); // app.UseResponseCaching(); app.MapControllerRoute( "default", "{controller=Home}/{action=Index}/{id?}"); ``` :::success 若想知道上述的 middleware 在做甚麼,可以參考[內建的 middleware](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?view=aspnetcore-6.0#built-in-middleware)。 ::: ## Custom middleware ### Basic middleware Program.cs ```csharp var builder = WebApplication.CreateBuilder(args); ... var app = builder.Build(); ... // 將客製的 middleware 加入管線 app.UseMiddleware<M1>(); app.UseMiddleware<SampleMiddleware>(); app.UseMiddleware<M3>(); ... app.Run(); ``` Minimum middleware class ```csharp public class SampleMiddleware { private readonly RequestDelegate _next; public SampleMiddleware(RequestDelegate next) { _next = next; } public async Task InvokeAsync(HttpContext context) { // Call the next delegate/middleware in the pipeline. await _next.Invoke(context); } } ``` :::info Constructor 和 Invoke method 的參數(parameters)都是由 [Dependency Injection](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-6.0) 機制取得。 ::: ### Advanced middleware Modify HTTP request and response. Program.cs ```csharp var builder = WebApplication.CreateBuilder(args); builder.Services .AddControllers() .AddJsonOptions(options => { options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault; options.JsonSerializerOptions.Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping; }); builder.Services .AddSingleton<IRepository<PartnerIp, DbContext>, Repository<PartnerIp, DbContext>>(); builder.Services .AddSingleton<IRepository<Crypto, DbContext>, Repository<Crypto, DbContext>>(); var app = builder.Build(); string publicPaths = "/echo|/ping"; app.UseMiddleware<IpFilterMiddleware>(publicPaths); app.UseMiddleware<PayloadEncryptionMiddleware>(); app.Run(); ``` IpFilterMiddleware.cs ```csharp public class IpFilterMiddleware { private readonly RequestDelegate _next; private readonly IRepository<PartnerIp, DbContext> _ipRepository; private readonly string[] _publicPaths; public IpFilterMiddleware(RequestDelegate next, IDalRepository<PartnerIp, DbContext> ipRepository, string publicPaths) { _next = next; _ipRepository = ipRepository; _publicPaths = publicPaths.Split('|'); } public async Task InvokeAsync(HttpContext context, IOptions<JsonOptions> jsonOptions) { if (context.Request.Path.Value != null && _publicPaths.Contains(context.Request.Path.Value)) { await _next.Invoke(context); return; } var remoteIp = context.Connection.RemoteIpAddress!; if (!_ipRepository.Contains(remoteIp)) { context.Response.StatusCode = (int)HttpStatusCode.Forbidden; await context.Response.WriteAsJsonAsync(new { Message = "No" }, jsonOptions.Value.JsonSerializerOptions); return; } await _next.Invoke(context); _ipRepository.CheckType(context.Response.ContentType); } } ``` PayloadEncryptionMiddleware.cs ```csharp public class PayloadEncryptionMiddleware { private readonly RequestDelegate _next; private readonly IRepository<Crypto, DbContext> _cryptoRepository; public PayloadEncryptionMiddleware(RequestDelegate next, IDalRepository<Crypto, DbContext> cryptoRepository) { _next = next; _cryptoRepository = cryptoRepository; } public async Task InvokeAsync(HttpContext context) { var remoteIp = context.Connection.RemoteIpAddress; byte[] key = _cryptoRepository.GetKey(remoteIp); string requestBody; using (var reader = new StreamReader(context.Request.Body)) { requestBody = await reader.ReadToEndAsync(); } string decryptedPayload = MyAesDecrypt(requestBody, key); byte[] buffer = Encoding.UTF8.GetBytes(decryptedPayload); using var reqStream = new MemoryStream(buffer); context.Request.Body = reqStream; Stream originalResponseBody = context.Response.Body; // Replace response body with a MemoryStream so that we can modify the response await using MemoryStream responseMemStream = new(); context.Response.Body = responseMemStream; await _next.Invoke(context); string responseBody; responseMemStream.Position = 0; using (var sr = new StreamReader(responseMemStream, leaveOpen: true)) { responseBody = await sr.ReadToEndAsync(); } string encryptedPayload = MyAesEncrypt(responseBody, key); // Re-use memory stream responseMemStream.SetLength(0); await using (var sw = new StreamWriter(responseMemStream, leaveOpen: true)) { await sw.WriteAsync(encryptedPayload); } responseMemStream.Position = 0; await responseMemStream.CopyToAsync(originalResponseBody); // Return original response body context.Response.Body = originalResponseBody; } } ``` :::warning 在 `PayloadEncryptionMiddleware` 內可以建立多個 `MemoryStream` 而不需要重用同一個,可以減少程式碼。 ::: 若只需要讀取 Request 的內容的話,可以使用 [Rewind](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.http.httprequestrewindextensions.enablebuffering?view=aspnetcore-6.0) 功能。 ```csharp context.Request.EnableBuffering(); // Leave the body open so the next middleware can read it. using (var reader = new StreamReader( context.Request.Body, encoding: Encoding.UTF8, detectEncodingFromByteOrderMarks: false, bufferSize: bufferSize, leaveOpen: true)) { var body = await reader.ReadToEndAsync(); // Do some processing with body // Reset the request body stream position so the next middleware can read it context.Request.Body.Position = 0; } // Call the next delegate/middleware in the pipeline await _next(context); ``` ###### tags: `asp-net-core` `asp-net` `middleware`