Implementing Optional Update Functionality in ASP.NET Core Web API
TLDR
- By using a custom
OptionalValue<T>struct, you can precisely distinguish between "do not update field" and "update to null." - For
[FromBody]requests, you must implementJsonConverterandJsonConverterFactoryto handle JSON serialization. - For
[FromForm]requests, you must implementIModelBinderandIModelBinderProviderto handle data binding. - To support Data Annotation validation, you must implement
IModelValidatorandIModelValidatorProvider. - Through
ISchemaFilterandIOperationFilter, you can ensure that Swagger documentation correctly displays the API specifications.
In RESTful API PATCH operations, a common pain point is the inability to distinguish between "the client did not send the field (do not update)" and "the client sent null (update the value to null)." If you use traditional null checks, it creates ambiguity for struct types (such as int, DateTime).
The core idea to solve this problem is to introduce a wrapper type OptionalValue<T>, which allows the backend to determine whether the property was explicitly assigned a value.
Optional Property Type Design
Use a readonly record struct to create OptionalValue<T>, utilizing a HasValue boolean to mark whether the field contains valid data, avoiding the type confusion caused by using 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);
}
}Handling FromBody Requests
When an API uses [FromBody], you need to handle the JSON format via JsonConverter to simplify the complex object structure into a direct mapping of property values.
- When will you encounter this issue: When the frontend sends data in JSON format, and the backend needs to distinguish whether a field exists in the JSON content.
You need to implement JsonConverter<OptionalValue<T>> and JsonConverterFactory, and register them in Program.cs:
builder.Services.AddControllers()
.AddJsonOptions(opts => {
opts.JsonSerializerOptions.Converters.Add(new OptionalValueJsonConverterFactory());
});Handling FromForm Requests
When an API uses [FromForm], ASP.NET Core cannot correctly parse OptionalValue<T> by default.
- When will you encounter this issue: When the frontend uses
multipart/form-dataorapplication/x-www-form-urlencodedto send form data.
You need to implement IModelBinder and IModelBinderProvider, and register them in Program.cs:
builder.Services.AddControllers(options => {
options.ModelBinderProviders.Insert(0, new OptionalValueModelBinderProvider());
});Handling Data Validation
Since OptionalValue<T> is a custom type, standard Data Annotations cannot act directly on its internal properties. You need to implement IModelValidator to intercept the validation logic.
- When will you encounter this issue: When a DTO property is marked with validation attributes like
[Required]or[Range], and you only want to perform validation when a value is present.
Register the validator via OptionalValueModelValidatorProvider to ensure that the corresponding ValidationAttribute check is only executed when HasValue is true.
Handling Swagger Schema
To ensure that Swagger documentation correctly presents the API specifications, you need to modify the generated swagger.json via ISchemaFilter and IOperationFilter.
- When will you encounter this issue: When the API has Swagger/OpenAPI documentation generation enabled, and you do not want the documentation to display the internal structure of
OptionalValue(such as theHasValueproperty).
builder.Services.AddSwaggerGen(opts => {
opts.SchemaFilter<OptionalValueSchemaFilter>();
opts.OperationFilter<OptionalValueOperationFilter>();
});Example Project
Executable example for this article: CloudyWing/OptionalPatchApi.
Changelog
- 2024-10-21 Initial document creation.
- 2026-05-17 Added link to the GitHub example project.