# 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 組成,而且一個接著一個的呼叫。下圖黑色箭頭為執行的過程:

例外處理的 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 的執行順序,就是根據 `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`