# 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`