筆記目錄

Skip to content

在 ASP.NET Core Web API 中實現可選更新功能

TLDR

  • 透過自定義 OptionalValue<T> struct,可精確區分「不更新欄位」與「更新為 null」。
  • 針對 [FromBody] 請求,需實作 JsonConverterJsonConverterFactory 來處理 JSON 序列化。
  • 針對 [FromForm] 請求,需實作 IModelBinderIModelBinderProvider 來處理資料繫結。
  • 為了支援 Data Annotation 驗證,需實作 IModelValidatorIModelValidatorProvider
  • 透過 ISchemaFilterIOperationFilter 可確保 Swagger 文件正確顯示 API 規格。

在 RESTful API 的 PATCH 操作中,常見的痛點是無法區分「客戶端未傳遞該欄位(不更新)」與「客戶端傳遞了 null(將值更新為 null)」。若使用傳統的 null 判斷,對於 struct 型別(如 int, DateTime)將會產生歧義。

解決此問題的核心思路是引入一個封裝型別 OptionalValue<T>,由後端判斷該屬性是否被明確賦值。

可選屬性型別設計

使用 readonly record struct 建立 OptionalValue<T>,利用 HasValue 布林值標記該欄位是否包含有效資料,避免使用 null 帶來的型別混淆。

csharp
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 中註冊:

csharp
builder.Services.AddControllers()
    .AddJsonOptions(opts => {
        opts.JsonSerializerOptions.Converters.Add(new OptionalValueJsonConverterFactory());
    });

處理 FromForm 請求

當 API 使用 [FromForm] 時,ASP.NET Core 預設無法正確解析 OptionalValue<T>

  • 什麼情況下會遇到這個問題:當前端使用 multipart/form-dataapplication/x-www-form-urlencoded 傳遞表單資料時。

需實作 IModelBinderIModelBinderProvider,並在 Program.cs 中註冊:

csharp
builder.Services.AddControllers(options => {
    options.ModelBinderProviders.Insert(0, new OptionalValueModelBinderProvider());
});

處理資料驗證

由於 OptionalValue<T> 是客製化型別,標準的 Data Annotation 無法直接作用於其內部屬性。需實作 IModelValidator 來攔截驗證邏輯。

  • 什麼情況下會遇到這個問題:當 DTO 屬性上標註了 [Required][Range] 等驗證屬性,且希望在有值時才進行驗證。

透過 OptionalValueModelValidatorProvider 註冊驗證器,確保當 HasValuetrue 時,才執行對應的 ValidationAttribute 檢查。

處理 Swagger Schema

為了讓 Swagger 文件能正確呈現 API 規格,需透過 ISchemaFilterIOperationFilter 修改產出的 swagger.json

  • 什麼情況下會遇到這個問題:當 API 啟用 Swagger/OpenAPI 文件產生,且不希望文件顯示 OptionalValue 的內部結構(如 HasValue 屬性)。
csharp
builder.Services.AddSwaggerGen(opts => {
    opts.SchemaFilter<OptionalValueSchemaFilter>();
    opts.OperationFilter<OptionalValueOperationFilter>();
});

範例專案

本文的可執行範例:CloudyWing/OptionalPatchApi

異動歷程

  • 2024-10-21 初版文件建立。
  • 2026-05-17 補上 GitHub 範例專案連結。