在 ASP.NET Core Web API 中實現可選更新功能
TLDR
- 透過自定義
OptionalValue<T>struct,可精確區分「不更新欄位」與「更新為 null」。 - 針對
[FromBody]請求,需實作JsonConverter與JsonConverterFactory來處理 JSON 序列化。 - 針對
[FromForm]請求,需實作IModelBinder與IModelBinderProvider來處理資料繫結。 - 為了支援 Data Annotation 驗證,需實作
IModelValidator與IModelValidatorProvider。 - 透過
ISchemaFilter與IOperationFilter可確保 Swagger 文件正確顯示 API 規格。
在 RESTful API 的 PATCH 操作中,常見的痛點是無法區分「客戶端未傳遞該欄位(不更新)」與「客戶端傳遞了 null(將值更新為 null)」。若使用傳統的 null 判斷,對於 struct 型別(如 int, DateTime)將會產生歧義。
解決此問題的核心思路是引入一個封裝型別 OptionalValue<T>,由後端判斷該屬性是否被明確賦值。
可選屬性型別設計
使用 readonly record struct 建立 OptionalValue<T>,利用 HasValue 布林值標記該欄位是否包含有效資料,避免使用 null 帶來的型別混淆。
public readonly record struct OptionalValue<T> {
private readonly T value;
public OptionalValue(T value) {
HasValue = true;
this.value = value;
}
public static OptionalValue<T> Empty() => new();
[ValidateNever]
public bool HasValue { get; }
[ValidateNever]
public T Value {
get {
if (!HasValue) {
throw new InvalidOperationException("OptionalValue object must have a value.");
}
return value;
}
}
public static implicit operator OptionalValue<T>(T value) {
return new OptionalValue<T>(value);
}
}處理 FromBody 請求
當 API 使用 [FromBody] 時,需要透過 JsonConverter 處理 JSON 格式,將複雜的物件結構簡化為直接對應屬性值。
- 什麼情況下會遇到這個問題:當前端透過 JSON 格式傳遞資料,且後端需要區分欄位是否存在於 JSON 內容中時。
需實作 JsonConverter<OptionalValue<T>> 與 JsonConverterFactory,並在 Program.cs 中註冊:
builder.Services.AddControllers()
.AddJsonOptions(opts => {
opts.JsonSerializerOptions.Converters.Add(new OptionalValueJsonConverterFactory());
});處理 FromForm 請求
當 API 使用 [FromForm] 時,ASP.NET Core 預設無法正確解析 OptionalValue<T>。
- 什麼情況下會遇到這個問題:當前端使用
multipart/form-data或application/x-www-form-urlencoded傳遞表單資料時。
需實作 IModelBinder 與 IModelBinderProvider,並在 Program.cs 中註冊:
builder.Services.AddControllers(options => {
options.ModelBinderProviders.Insert(0, new OptionalValueModelBinderProvider());
});處理資料驗證
由於 OptionalValue<T> 是客製化型別,標準的 Data Annotation 無法直接作用於其內部屬性。需實作 IModelValidator 來攔截驗證邏輯。
- 什麼情況下會遇到這個問題:當 DTO 屬性上標註了
[Required]或[Range]等驗證屬性,且希望在有值時才進行驗證。
透過 OptionalValueModelValidatorProvider 註冊驗證器,確保當 HasValue 為 true 時,才執行對應的 ValidationAttribute 檢查。
處理 Swagger Schema
為了讓 Swagger 文件能正確呈現 API 規格,需透過 ISchemaFilter 與 IOperationFilter 修改產出的 swagger.json。
- 什麼情況下會遇到這個問題:當 API 啟用 Swagger/OpenAPI 文件產生,且不希望文件顯示
OptionalValue的內部結構(如HasValue屬性)。
builder.Services.AddSwaggerGen(opts => {
opts.SchemaFilter<OptionalValueSchemaFilter>();
opts.OperationFilter<OptionalValueOperationFilter>();
});範例專案
本文的可執行範例:CloudyWing/OptionalPatchApi。
異動歷程
- 2024-10-21 初版文件建立。
- 2026-05-17 補上 GitHub 範例專案連結。