I've created a solution that works with System.Text.Json using a JsonConverter
DTO class:
public class MyDataDto : PatchRequest<MyDataDto>
{
public string? Field1 { get; set; }
public string? Field2 { get; set; }
}
PatchRequest class:
public abstract class PatchRequest
{
private readonly List<string> _setProperties = new();
public void MarkPropertyAsSet(string propertyName) => _setProperties.Add(propertyName);
public bool IsSet(string propertyName) => _setProperties.Contains(propertyName);
}
public abstract class PatchRequest<T> : PatchRequest where T : PatchRequest<T>
{
public bool IsSet<TProperty>(Expression<Func<T, TProperty>> expression)
=> IsSet((expression.Body as MemberExpression).Member.Name);
}
JsonConverter:
public class PatchRequestConverter : JsonConverter<PatchRequest>
{
public override bool CanConvert(Type typeToConvert) =>
typeof(PatchRequest).IsAssignableFrom(typeToConvert);
public override PatchRequest Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartObject)
throw new JsonException();
var patchRequest = (PatchRequest)Activator.CreateInstance(typeToConvert)!;
var properties = typeToConvert
.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.SetProperty | BindingFlags.GetProperty)
.ToDictionary(p => options.PropertyNamingPolicy?.ConvertName(p.Name) ?? p.Name);
while (reader.Read())
switch (reader.TokenType)
{
case JsonTokenType.EndObject:
return patchRequest;
case JsonTokenType.PropertyName:
var property = properties[reader.GetString()!];
reader.Read();
property.SetValue(patchRequest, JsonSerializer.Deserialize(ref reader, property.PropertyType, options));
patchRequest.MarkPropertyAsSet(property.Name);
continue;
}
throw new JsonException();
}
public override void Write(Utf8JsonWriter writer, PatchRequest value, JsonSerializerOptions options) =>
JsonSerializer.Serialize(writer, value, value.GetType(), options);
}
Register the JsonConverter like:
builder.Services.Configure<JsonOptions>(options =>
options.JsonSerializerOptions.Converters.Add(new PatchRequestConverter());
);
Use in in a API controller like:
public async Task<ActionResult> PatchMyDataAsync([FromBody] MyDataDto myDataDto)
{
var field1IsSet = myDataDto.IsSet(c => c.Field1);
var field2IsSet = myDataDto.IsSet(nameof(c.Field2));
//...
}