On this page

Skip to content

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 implement JsonConverter and JsonConverterFactory to handle JSON serialization.
  • For [FromForm] requests, you must implement IModelBinder and IModelBinderProvider to handle data binding.
  • To support Data Annotation validation, you must implement IModelValidator and IModelValidatorProvider.
  • Through ISchemaFilter and IOperationFilter, 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.

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);
    }
}

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:

csharp
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-data or application/x-www-form-urlencoded to send form data.

You need to implement IModelBinder and IModelBinderProvider, and register them in Program.cs:

csharp
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 the HasValue property).
csharp
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.