From 352163422bfa5db688cd20360772967bc6f0bd30 Mon Sep 17 00:00:00 2001 From: Ionite Date: Sun, 3 Sep 2023 21:39:37 -0400 Subject: [PATCH] Fix serialization support for JsonPropertyName --- .../ViewModels/Base/LoadableViewModelBase.cs | 160 +++++++++++++----- 1 file changed, 120 insertions(+), 40 deletions(-) diff --git a/StabilityMatrix.Avalonia/ViewModels/Base/LoadableViewModelBase.cs b/StabilityMatrix.Avalonia/ViewModels/Base/LoadableViewModelBase.cs index 857846bd..6aabb15c 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Base/LoadableViewModelBase.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Base/LoadableViewModelBase.cs @@ -21,20 +21,18 @@ public abstract class LoadableViewModelBase : ViewModelBase, IJsonLoadableState typeof(IRelayCommand) }; - private static readonly string[] SerializerIgnoredNames = - { - nameof(HasErrors), - }; + private static readonly string[] SerializerIgnoredNames = { nameof(HasErrors), }; - private static readonly JsonSerializerOptions SerializerOptions = new() - { - IgnoreReadOnlyProperties = true, - }; + private static readonly JsonSerializerOptions SerializerOptions = + new() { IgnoreReadOnlyProperties = true, }; private static bool ShouldIgnoreProperty(PropertyInfo property) { // Skip if read-only and not IJsonLoadableState - if (property.SetMethod is null && !typeof(IJsonLoadableState).IsAssignableFrom(property.PropertyType)) + if ( + property.SetMethod is null + && !typeof(IJsonLoadableState).IsAssignableFrom(property.PropertyType) + ) { Logger.Trace("Skipping {Property} - read-only", property.Name); return true; @@ -48,7 +46,11 @@ public abstract class LoadableViewModelBase : ViewModelBase, IJsonLoadableState // Check not excluded type if (SerializerIgnoredTypes.Contains(property.PropertyType)) { - Logger.Trace("Skipping {Property} - serializer ignored type {Type}", property.Name, property.PropertyType); + Logger.Trace( + "Skipping {Property} - serializer ignored type {Type}", + property.Name, + property.PropertyType + ); return true; } // Check not ignored name @@ -60,7 +62,22 @@ public abstract class LoadableViewModelBase : ViewModelBase, IJsonLoadableState return false; } - + + /// + /// True if we should include property without checking exclusions + /// + private static bool ShouldIncludeProperty(PropertyInfo property) + { + // Has JsonIncludeAttribute + if (property.GetCustomAttributes(typeof(JsonIncludeAttribute), true).Length > 0) + { + Logger.Trace("Including {Property} - has [JsonInclude]", property.Name); + return true; + } + + return false; + } + /// /// Load the state of this view model from a JSON object. /// The default implementation is a mirror of . @@ -81,51 +98,84 @@ public abstract class LoadableViewModelBase : ViewModelBase, IJsonLoadableState foreach (var property in properties) { + var name = property.Name; + + // If JsonPropertyName provided, use that as the key + if ( + property + .GetCustomAttributes(typeof(JsonPropertyNameAttribute), true) + .FirstOrDefault() + is JsonPropertyNameAttribute jsonPropertyName + ) + { + Logger.Trace( + "Deserializing {Property} ({Type}) with JsonPropertyName {JsonPropertyName}", + property.Name, + property.PropertyType, + jsonPropertyName.Name + ); + if (property.GetValue(this) is string jsonPropertyNameValue) + { + name = jsonPropertyNameValue; + } + } + // Check if property is in the JSON object - if (!state.TryGetPropertyValue(property.Name, out var value)) + if (!state.TryGetPropertyValue(name, out var value)) { Logger.Trace("Skipping {Property} - not in JSON object", property.Name); continue; } - + // Check if we should ignore this property - if (ShouldIgnoreProperty(property)) + if (!ShouldIncludeProperty(property) && ShouldIgnoreProperty(property)) { continue; } - + // For types that also implement IJsonLoadableState, defer to their load implementation if (typeof(IJsonLoadableState).IsAssignableFrom(property.PropertyType)) { - Logger.Trace("Loading {Property} ({Type}) with IJsonLoadableState", property.Name, property.PropertyType); - + Logger.Trace( + "Loading {Property} ({Type}) with IJsonLoadableState", + property.Name, + property.PropertyType + ); + // Value must be non-null if (value is null) { - throw new InvalidOperationException($"Property {property.Name} is IJsonLoadableState but value to be loaded is null"); + throw new InvalidOperationException( + $"Property {property.Name} is IJsonLoadableState but value to be loaded is null" + ); } - + // Check if the current object at this property is null if (property.GetValue(this) is not IJsonLoadableState propertyValue) { // If null, it must have a default constructor - if (property.PropertyType.GetConstructor(Type.EmptyTypes) is not { } constructorInfo) + if ( + property.PropertyType.GetConstructor(Type.EmptyTypes) + is not { } constructorInfo + ) { - throw new InvalidOperationException($"Property {property.Name} is IJsonLoadableState but current object is null and has no default constructor"); + throw new InvalidOperationException( + $"Property {property.Name} is IJsonLoadableState but current object is null and has no default constructor" + ); } - + // Create a new instance and set it - propertyValue = (IJsonLoadableState) constructorInfo.Invoke(null); + propertyValue = (IJsonLoadableState)constructorInfo.Invoke(null); property.SetValue(this, propertyValue); } - + // Load the state from the JSON object propertyValue.LoadStateFromJsonObject(value.AsObject()); } else { Logger.Trace("Loading {Property} ({Type})", property.Name, property.PropertyType); - + var propertyValue = value.Deserialize(property.PropertyType, SerializerOptions); property.SetValue(this, propertyValue); } @@ -149,60 +199,90 @@ public abstract class LoadableViewModelBase : ViewModelBase, IJsonLoadableState // Get all of our properties using reflection. var properties = GetType().GetProperties(); Logger.Trace("Serializing {Type} with {Count} properties", GetType(), properties.Length); - + // Create a JSON object to store the state. var state = new JsonObject(); - + // Serialize each property marked with JsonIncludeAttribute. foreach (var property in properties) { - if (ShouldIgnoreProperty(property)) + if (!ShouldIncludeProperty(property) && ShouldIgnoreProperty(property)) { continue; } + var name = property.Name; + + // If JsonPropertyName provided, use that as the key. + if ( + property + .GetCustomAttributes(typeof(JsonPropertyNameAttribute), true) + .FirstOrDefault() + is JsonPropertyNameAttribute jsonPropertyName + ) + { + Logger.Trace( + "Serializing {Property} ({Type}) with JsonPropertyName {JsonPropertyName}", + property.Name, + property.PropertyType, + jsonPropertyName.Name + ); + if (property.GetValue(this) is string value) + { + name = value; + } + } + // For types that also implement IJsonLoadableState, defer to their implementation. if (typeof(IJsonLoadableState).IsAssignableFrom(property.PropertyType)) { - Logger.Trace("Serializing {Property} ({Type}) with IJsonLoadableState", property.Name, property.PropertyType); + Logger.Trace( + "Serializing {Property} ({Type}) with IJsonLoadableState", + property.Name, + property.PropertyType + ); var value = property.GetValue(this); if (value is not null) { - var model = (IJsonLoadableState) value; + var model = (IJsonLoadableState)value; var modelState = model.SaveStateToJsonObject(); - state.Add(property.Name, modelState); + state.Add(name, modelState); } } else { - Logger.Trace("Serializing {Property} ({Type})", property.Name, property.PropertyType); + Logger.Trace( + "Serializing {Property} ({Type})", + property.Name, + property.PropertyType + ); var value = property.GetValue(this); if (value is not null) { - state.Add(property.Name, JsonSerializer.SerializeToNode(value, SerializerOptions)); + state.Add(name, JsonSerializer.SerializeToNode(value, SerializerOptions)); } } } - + return state; } - + /// /// Serialize a model to a JSON object. /// protected static JsonObject SerializeModel(T model) { var node = JsonSerializer.SerializeToNode(model); - return node?.AsObject() ?? throw new - NullReferenceException("Failed to serialize state to JSON object."); + return node?.AsObject() + ?? throw new NullReferenceException("Failed to serialize state to JSON object."); } - + /// /// Deserialize a model from a JSON object. /// protected static T DeserializeModel(JsonObject state) { - return state.Deserialize() ?? throw new - NullReferenceException("Failed to deserialize state from JSON object."); + return state.Deserialize() + ?? throw new NullReferenceException("Failed to deserialize state from JSON object."); } }