Open bkoelman opened 1 week ago
This would be great to use for partial patch, where sending "firstname": null
means something different than omitting it.
Unfortunately, this isn't currently possible, because the client code generated by NSwag performs a comparison before raising the INotifyPropertyChanged.PropertyChanged
event. For example:
[Newtonsoft.Json.JsonProperty("firstName", Required = Newtonsoft.Json.Required.Default,
NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
public string? FirstName
{
get { return _firstName; }
set
{
if (_firstName != value) // <----- this check must be removed, so we can detect null/default assignments
{
_firstName = value;
RaisePropertyChanged();
}
}
}
It would require shipping a custom template for NJsonSchema, based on the default template. If we did, the following would work:
var updatePersonRequest = new UpdatePersonRequestDocument
{
Data = new DataInUpdatePersonRequest
{
Id = "1",
Attributes = new TrackChangesFor<AttributesInUpdatePersonRequest>(_apiClient)
{
Initializer =
{
FirstName = null,
LastName = "last"
}
}.Initializer
}
};
Using the following utilities in the JsonApiDotNetCore.OpenApi.Client.NSwag
package:
public sealed class TrackChangesFor<T>
where T : INotifyPropertyChanged, new()
{
public T Initializer { get; }
public TrackChangesFor(JsonApiClient apiClient)
{
ArgumentNullException.ThrowIfNull(apiClient);
Initializer = new T();
apiClient.Track(Initializer);
}
}
public abstract class JsonApiClient : IJsonApiClient
{
private readonly Dictionary<INotifyPropertyChanged, ISet<string>> _propertyStore = [];
internal void Track<T>(T container)
where T : INotifyPropertyChanged, new()
{
container.PropertyChanged += ContainerOnPropertyChanged;
}
private void ContainerOnPropertyChanged(object? sender, PropertyChangedEventArgs args)
{
if (sender is INotifyPropertyChanged container && args.PropertyName != null)
{
if (!_propertyStore.TryGetValue(container, out ISet<string>? properties))
{
properties = new HashSet<string>();
_propertyStore[container] = properties;
}
properties.Add(args.PropertyName!);
}
}
public void Reset()
{
foreach (INotifyPropertyChanged container in _propertyStore.Keys)
{
container.PropertyChanged -= ContainerOnPropertyChanged;
}
_propertyStore.Clear();
}
protected void SetSerializerSettingsForJsonApi(JsonSerializerSettings serializerSettings)
{
ArgumentNullException.ThrowIfNull(serializerSettings);
serializerSettings.Converters.Add(new PropertyTrackingConverter(this));
}
private sealed class PropertyTrackingConverter : JsonConverter
{
private readonly JsonApiClient _apiClient;
private bool _isSerializing;
public PropertyTrackingConverter(JsonApiClient apiClient)
{
ArgumentNullException.ThrowIfNull(apiClient);
_apiClient = apiClient;
}
public override bool CanConvert(Type objectType)
{
return !_isSerializing &&
_apiClient._propertyStore.Keys.Any(container => container.GetType() == objectType);
}
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue,
JsonSerializer serializer)
{
_isSerializing = true;
var result = serializer.Deserialize(reader, objectType);
_isSerializing = false;
return result;
}
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{
if (value is INotifyPropertyChanged container &&
_apiClient._propertyStore.TryGetValue(container, out ISet<string>? properties))
{
_isSerializing = true;
writer.WriteStartObject();
foreach (string propertyName in properties)
{
PropertyInfo property = container.GetType().GetProperty(propertyName)!;
object? propertyValue = property.GetValue(container);
writer.WritePropertyName(propertyName);
serializer.Serialize(writer, propertyValue);
}
writer.WriteEndObject();
_isSerializing = false;
}
else
{
serializer.Serialize(writer, value);
}
}
}
}
If possible, this smoothens the experience of partial post/patch for atomic operations. Otherwise, the existing extension method must take the operation index as a parameter.