0%

[DotnetCore]ModelBindingMiddleware

前情提要

延續[DotnetCore]Refit:類ApiGateway實作文章,筆者在開發前台,後台這種隔離架構時,還會有一個需求是,比較正式的API規格,會有一些客製化的Header值必須要辨別,不外乎就是AcesssToken,或者必要的Client資訊,及封包簽章等資訊,以前台轉發到後台這段來說,猶如上一篇提到透過Refit完成。

筆者的思路是這樣,驗證這些Client Request資訊的職責來說,止於前台,不需要再原封不動將客製化的Header資訊送到後台,因為後台商業邏輯處理來說也不需要這些資訊,因此某些Header的值必須下放到Model層級,讓後台收到的Request中已經含有商業邏輯處理的必要資訊。

再則前台這一層必須要驗證Request的合理性,一定會讀取Header資訊,若要把這些Header資訊原封不動的送到後台,反而還要額外動工,第一是要製作成Refit的參數,第二是後台還要再拆解一次Header資訊,完全是做了很多不需要的步驟,於是乎,這篇就誕生啦。

內容

筆者主要目的就是想要把一些Header上的資訊,想要放到RequestBody上,讓API Pipeline走到Action時,已經將Request Model都繫結好了,筆者一開始的方向是Customize Model Binding,找到Microsoft Learn上面的教學https://learn.microsoft.com/en-us/aspnet/core/mvc/advanced/custom-model-binding?view=aspnetcore-6.0,看了一下情境都不太符合,

RequestBaseModel

筆者這邊交代一下,因為這個新系統包含OAuth的驗證,基本上都會有ClientId以及RequestId(識別Request),筆者這邊僅列出BaseModel

1
2
3
4
5
public class BaseRequestDto
{
public string RequestId { get; set; }
public string ClientId { get; set; }
}

HeaderKeyConfigHelper

後面寫到Middleware時會用到讀取Header中的值,基本上都會有一個定義好的Header Key,筆者這邊習慣會定義好const變數

  • 集中Key定義
  • 若有一個以上地方使用後須調整時,只要更改一個地方即可
1
2
3
4
5
6
7
public class HeaderKeyConfigHelper
{
public const string REQUEST_ID= "X-Request-Id";
public const string CLIENT_ID = "X-Client-Id";
// 目前範例未使用到
public const string AUTHORIZATION = "Authorization";
}

HeaderModelBindingMiddleware

這邊主要邏輯是讀出RequestBody,透過dynamicExpandObject,指定客製化欄位的值後再變更其RequestBody

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class HeaderModelBindingMiddleware
{
private readonly RequestDelegate _next;

public HeaderModelBindingMiddleware(RequestDelegate next)
{
_next = next;
}

public async Task InvokeAsync(HttpContext context)
{
// 以上省略
// Get header value
context.Request.Headers.TryGetValue(HeaderKeyConfigHelper.CLIENT_ID, out StringValues clientIdHeaderValue);
context.Request.Headers.TryGetValue(HeaderKeyConfigHelper.REQUEST_ID, out StringValues requestIdHeaderValue);
// Read the request body
var originalRequestBody = context.Request.Body;
using var streamReader = new StreamReader(originalRequestBody);
var requestBodyText = await streamReader.ReadToEndAsync();
dynamic requestModel = JsonSerializer.Deserialize<ExpandoObject>(requestBodyText);
if (clientIdHeaderValue.Count > 0)
{
requestModel.ClientId = clientIdHeaderValue.ToString();
}
if (transactionIdHeaderValue.Count > 0)
{
requestModel.RequestId= requestIdHeaderValue.ToString();
}
// Convert the modified body back to a stream
var modifiedRequestBody = new MemoryStream(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(requestModel)));

// Set the modified request body
context.Request.Body = modifiedRequestBody;

// Continue to the next middleware
await _next(context);
}
}

上述code中透過ExpandObject擴充其屬性,筆者這個版本好處有幾個

  • 若沒有對應的header key也不會壞掉,先判斷有header key對應的value才指定
  • 接著即便是Action所接的Model沒有這些屬性也只是被捨去這些值而已,沒有其他SideEffect

當然這個版本的缺點就是若沒有再用Endpoint過濾的掛上Middleware的話,每個Action都會經過這個Middleware,若沒有需要這些屬性的Model就顯得多了一些步驟做判斷是否存在header Key及對應的值,筆者這個站台的使用情境是滿符合的,因此要考慮用的讀者可能要思考一下使用情境再做使用。

註冊Middleware

就註冊在Startup中就可以了

1
2
3
4
5
6
7
8
9
10
11
public partial class Startup
{
// 以上省略
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// 以上省略
app.UseMiddleware<HeaderModelBindingMiddleware>();
// 以下省略
}
// 以下省略
}

這樣Action接的Model只要有繼承BaseRequestDto就會自動Binding裡面的RequestIdClientId屬性了

結論

筆者因有一個前台站台,不處理邏輯,裡面剛好用到的此篇的技巧,轉發到後台時,不用再另外包裝Header值,轉發到後台後,後台站台也不用再另外解析Header的值做商業邏輯處理,省了不少工,此篇就到這邊了,下次見。

參考