From 8289b03f796c3102ec4b7c2ddfcbaf0fde93fcf8 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Wed, 22 Mar 2023 19:28:38 +0100 Subject: [PATCH] Fix JSON serialization issues --- .../Helpers/ObjectConverter.cs | 37 ++++++++++--------- .../Messages/DispatchResumeWorkflows.cs | 2 +- .../DispatchTriggerWorkflowsRequest.cs | 2 +- .../Converters/PolymorphicConverter.cs | 36 ------------------ .../PolymorphicDictionaryConverter.cs | 2 +- ...erter.cs => PolymorphicObjectConverter.cs} | 16 +++++--- ...s => PolymorphicObjectConverterFactory.cs} | 4 +- .../Converters/PropertyBagConverter.cs | 1 - .../SystemObjectPrimitiveConverter.cs | 2 +- .../Services/WorkflowStorageDriver.cs | 11 +++--- 10 files changed, 43 insertions(+), 70 deletions(-) delete mode 100644 src/modules/Elsa.Workflows.Core/Serialization/Converters/PolymorphicConverter.cs rename src/modules/Elsa.Workflows.Core/Serialization/Converters/{SystemObjectWithTypeHandlingConverter.cs => PolymorphicObjectConverter.cs} (85%) rename src/modules/Elsa.Workflows.Core/Serialization/Converters/{SystemObjectWithTypeHandlingConverterFactory.cs => PolymorphicObjectConverterFactory.cs} (70%) diff --git a/src/modules/Elsa.Expressions/Helpers/ObjectConverter.cs b/src/modules/Elsa.Expressions/Helpers/ObjectConverter.cs index 234b55695..ef38cb294 100644 --- a/src/modules/Elsa.Expressions/Helpers/ObjectConverter.cs +++ b/src/modules/Elsa.Expressions/Helpers/ObjectConverter.cs @@ -22,7 +22,7 @@ public record ObjectConverterOptions(JsonSerializerOptions? SerializerOptions = public static class ObjectConverter { public static Result TryConvertTo(this object? value, ObjectConverterOptions? serializerOptions = null) => value.TryConvertTo(typeof(T), serializerOptions); - + public static Result TryConvertTo(this object? value, Type targetType, ObjectConverterOptions? serializerOptions = null) { try @@ -64,10 +64,10 @@ public static class ObjectConverter { if (jsonObject.ValueKind == JsonValueKind.String && underlyingTargetType != typeof(string)) return jsonObject.GetString().ConvertTo(underlyingTargetType); - + return jsonObject.Deserialize(targetType, options); } - + if (underlyingSourceType == typeof(string) && !underlyingTargetType.IsPrimitive && underlyingTargetType != typeof(object)) { var stringValue = (string)value; @@ -75,7 +75,7 @@ public static class ObjectConverter try { var firstChar = stringValue.TrimStart().FirstOrDefault(); - + if (firstChar is '{' or '[') return JsonSerializer.Deserialize(stringValue, underlyingTargetType, options); } @@ -84,7 +84,7 @@ public static class ObjectConverter throw new TypeConversionException($"Failed to deserialize {stringValue} to {underlyingTargetType}", value, underlyingTargetType, e); } } - + if (targetType == typeof(object)) return value; @@ -94,15 +94,18 @@ public static class ObjectConverter if (underlyingSourceType == underlyingTargetType) return value; - if (underlyingSourceType == typeof(ExpandoObject) && underlyingTargetType.IsClass) + if (typeof(IDictionary).IsAssignableFrom(underlyingSourceType) && underlyingTargetType.IsClass) { - if (value is IDictionary dictionary && typeof(IDictionary).IsAssignableFrom(underlyingTargetType)) - return new Dictionary(dictionary); + if (typeof(ExpandoObject) == underlyingTargetType) + { + var expandoJson = JsonSerializer.Serialize(value); + return ConvertTo(expandoJson, underlyingTargetType, converterOptions); + } - var expandoJson = JsonSerializer.Serialize(value); - return ConvertTo(expandoJson, underlyingTargetType, converterOptions); + if (typeof(IDictionary).IsAssignableFrom(underlyingTargetType)) + return new Dictionary((IDictionary)value); } - + var targetTypeConverter = TypeDescriptor.GetConverter(underlyingTargetType); if (targetTypeConverter.CanConvertFrom(underlyingSourceType)) @@ -126,16 +129,16 @@ public static class ObjectConverter if (underlyingSourceType == typeof(double)) return Enum.ToObject(underlyingTargetType, Convert.ChangeType(value, typeof(int))); } - + if (value is string s) { - if(string.IsNullOrWhiteSpace(s)) + if (string.IsNullOrWhiteSpace(s)) return null; - - if(underlyingTargetType == typeof(Type)) + + if (underlyingTargetType == typeof(Type)) return converterOptions?.WellKnownTypeRegistry != null ? converterOptions.WellKnownTypeRegistry.GetTypeOrDefault(s) : Type.GetType(s); } - + if (value is IEnumerable enumerable) { if (underlyingTargetType is { IsGenericType: true }) @@ -147,7 +150,7 @@ public static class ObjectConverter { var collectionType = typeof(List<>).MakeGenericType(desiredCollectionItemType); var collection = (IList)Activator.CreateInstance(collectionType)!; - + foreach (var item in enumerable) { var convertedItem = ConvertTo(item, desiredCollectionItemType); diff --git a/src/modules/Elsa.MassTransit/Messages/DispatchResumeWorkflows.cs b/src/modules/Elsa.MassTransit/Messages/DispatchResumeWorkflows.cs index 25f0847eb..6e2c17def 100644 --- a/src/modules/Elsa.MassTransit/Messages/DispatchResumeWorkflows.cs +++ b/src/modules/Elsa.MassTransit/Messages/DispatchResumeWorkflows.cs @@ -5,7 +5,7 @@ namespace Elsa.MassTransit.Messages; public record DispatchResumeWorkflows( string ActivityTypeName, - [property: JsonConverter(typeof(PolymorphicConverter))] + [property: JsonConverter(typeof(PolymorphicObjectConverterFactory))] object BookmarkPayload, string? CorrelationId, string? WorkflowInstanceId, diff --git a/src/modules/Elsa.MassTransit/Messages/DispatchTriggerWorkflowsRequest.cs b/src/modules/Elsa.MassTransit/Messages/DispatchTriggerWorkflowsRequest.cs index 2b69ba6cc..fc0a0e97e 100644 --- a/src/modules/Elsa.MassTransit/Messages/DispatchTriggerWorkflowsRequest.cs +++ b/src/modules/Elsa.MassTransit/Messages/DispatchTriggerWorkflowsRequest.cs @@ -6,7 +6,7 @@ namespace Elsa.MassTransit.Messages; public record DispatchTriggerWorkflows ( string ActivityTypeName, - [property: JsonConverter(typeof(PolymorphicConverter))] + [property: JsonConverter(typeof(PolymorphicObjectConverterFactory))] object BookmarkPayload, string? CorrelationId, string? WorkflowInstanceId, diff --git a/src/modules/Elsa.Workflows.Core/Serialization/Converters/PolymorphicConverter.cs b/src/modules/Elsa.Workflows.Core/Serialization/Converters/PolymorphicConverter.cs deleted file mode 100644 index 02e41e29b..000000000 --- a/src/modules/Elsa.Workflows.Core/Serialization/Converters/PolymorphicConverter.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Serialization; -using Elsa.Extensions; - -namespace Elsa.Workflows.Core.Serialization.Converters; - -/// -/// A converter that stores type information in order to deserialize the object back into the same type. -/// -public class PolymorphicConverter : JsonConverter -{ - /// - public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options) - { - var typeName = value.GetType().GetSimpleAssemblyQualifiedName(); - var newOptions = new JsonSerializerOptions(options); - newOptions.Converters.RemoveWhere(x => x is PolymorphicConverter); - var wrappedValue = JsonSerializer.SerializeToNode(value, newOptions)!; - wrappedValue["$type"] = typeName; - wrappedValue.WriteTo(writer); - } - - /// - public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - var newOptions = new JsonSerializerOptions(options); - newOptions.Converters.RemoveWhere(x => x is PolymorphicConverter); - - var element = JsonElement.ParseValue(ref reader); - var typeName = element.GetProperty("$type").GetString()!; - var type = Type.GetType(typeName)!; - var value = element.Deserialize(type, newOptions); - - return value!; - } -} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Serialization/Converters/PolymorphicDictionaryConverter.cs b/src/modules/Elsa.Workflows.Core/Serialization/Converters/PolymorphicDictionaryConverter.cs index 33e3f651a..fe82d87eb 100644 --- a/src/modules/Elsa.Workflows.Core/Serialization/Converters/PolymorphicDictionaryConverter.cs +++ b/src/modules/Elsa.Workflows.Core/Serialization/Converters/PolymorphicDictionaryConverter.cs @@ -11,7 +11,7 @@ public class PolymorphicDictionaryConverter : JsonConverter)newOptions.GetConverter(typeof(object)); } diff --git a/src/modules/Elsa.Workflows.Core/Serialization/Converters/SystemObjectWithTypeHandlingConverter.cs b/src/modules/Elsa.Workflows.Core/Serialization/Converters/PolymorphicObjectConverter.cs similarity index 85% rename from src/modules/Elsa.Workflows.Core/Serialization/Converters/SystemObjectWithTypeHandlingConverter.cs rename to src/modules/Elsa.Workflows.Core/Serialization/Converters/PolymorphicObjectConverter.cs index 0a293390e..626e7856c 100644 --- a/src/modules/Elsa.Workflows.Core/Serialization/Converters/SystemObjectWithTypeHandlingConverter.cs +++ b/src/modules/Elsa.Workflows.Core/Serialization/Converters/PolymorphicObjectConverter.cs @@ -8,13 +8,13 @@ namespace Elsa.Workflows.Core.Serialization.Converters; /// /// Used for reading objects as primitive types rather than values while also maintaining the .NET type name for reconstructing the actual type. /// -public class SystemObjectWithTypeHandlingConverter : JsonConverter +public class PolymorphicObjectConverter : JsonConverter { private const string TypePropertyName = "_type"; private const string ItemsPropertyName = "_items"; /// - public SystemObjectWithTypeHandlingConverter() + public PolymorphicObjectConverter() { } @@ -33,6 +33,7 @@ public class SystemObjectWithTypeHandlingConverter : JsonConverter if (!jsonObject.TryGetProperty(TypePropertyName, out var typeNameElement)) { + newOptions.Converters.RemoveWhere(x => x is PolymorphicObjectConverterFactory); return jsonObject.Deserialize(typeof(ExpandoObject), newOptions)!; } @@ -74,9 +75,9 @@ public class SystemObjectWithTypeHandlingConverter : JsonConverter var newOptions = new JsonSerializerOptions(options); var type = value.GetType(); - newOptions.Converters.RemoveWhere(x => x is SystemObjectWithTypeHandlingConverterFactory); + newOptions.Converters.RemoveWhere(x => x is PolymorphicObjectConverterFactory); - if (type.IsPrimitive || value is string or DateTimeOffset ) + if (type.IsPrimitive || value is string or DateTimeOffset or JsonElement) { JsonSerializer.Serialize(writer, value, newOptions); return; @@ -99,7 +100,12 @@ public class SystemObjectWithTypeHandlingConverter : JsonConverter } } - writer.WriteString(TypePropertyName, type.GetSimpleAssemblyQualifiedName()); + if(type != typeof(ExpandoObject)) + { + // Write the type name so that we can reconstruct the actual type when deserializing. + writer.WriteString(TypePropertyName, type.GetSimpleAssemblyQualifiedName()); + } + writer.WriteEndObject(); } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Serialization/Converters/SystemObjectWithTypeHandlingConverterFactory.cs b/src/modules/Elsa.Workflows.Core/Serialization/Converters/PolymorphicObjectConverterFactory.cs similarity index 70% rename from src/modules/Elsa.Workflows.Core/Serialization/Converters/SystemObjectWithTypeHandlingConverterFactory.cs rename to src/modules/Elsa.Workflows.Core/Serialization/Converters/PolymorphicObjectConverterFactory.cs index fa41ad09c..8e964175e 100644 --- a/src/modules/Elsa.Workflows.Core/Serialization/Converters/SystemObjectWithTypeHandlingConverterFactory.cs +++ b/src/modules/Elsa.Workflows.Core/Serialization/Converters/PolymorphicObjectConverterFactory.cs @@ -6,11 +6,11 @@ namespace Elsa.Workflows.Core.Serialization.Converters; /// /// A JSON converter for objects. /// -public class SystemObjectWithTypeHandlingConverterFactory : JsonConverterFactory +public class PolymorphicObjectConverterFactory : JsonConverterFactory { /// public override bool CanConvert(Type typeToConvert) => true; /// - public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) => new SystemObjectWithTypeHandlingConverter(); + public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) => new PolymorphicObjectConverter(); } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Serialization/Converters/PropertyBagConverter.cs b/src/modules/Elsa.Workflows.Core/Serialization/Converters/PropertyBagConverter.cs index 67da6aa61..11c08c06c 100644 --- a/src/modules/Elsa.Workflows.Core/Serialization/Converters/PropertyBagConverter.cs +++ b/src/modules/Elsa.Workflows.Core/Serialization/Converters/PropertyBagConverter.cs @@ -23,7 +23,6 @@ public class PropertyBagConverter : JsonConverter { var newOptions = new JsonSerializerOptions(); newOptions.Converters.Add(new PolymorphicDictionaryConverterFactory()); - JsonSerializer.Serialize(writer, value.Dictionary, newOptions); } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Serialization/Converters/SystemObjectPrimitiveConverter.cs b/src/modules/Elsa.Workflows.Core/Serialization/Converters/SystemObjectPrimitiveConverter.cs index b0ebd820c..1d19dcd39 100644 --- a/src/modules/Elsa.Workflows.Core/Serialization/Converters/SystemObjectPrimitiveConverter.cs +++ b/src/modules/Elsa.Workflows.Core/Serialization/Converters/SystemObjectPrimitiveConverter.cs @@ -27,6 +27,6 @@ public class SystemObjectPrimitiveConverter : JsonConverter /// public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options) { - throw new InvalidOperationException("Should not get here."); + JsonSerializer.Serialize(writer, value, value.GetType()); } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Services/WorkflowStorageDriver.cs b/src/modules/Elsa.Workflows.Core/Services/WorkflowStorageDriver.cs index 0ad48c55a..99bf8b2fa 100644 --- a/src/modules/Elsa.Workflows.Core/Services/WorkflowStorageDriver.cs +++ b/src/modules/Elsa.Workflows.Core/Services/WorkflowStorageDriver.cs @@ -1,6 +1,7 @@ using System.ComponentModel.DataAnnotations; using Elsa.Extensions; using Elsa.Workflows.Core.Contracts; +using Elsa.Workflows.Core.Models; namespace Elsa.Workflows.Core.Services; @@ -11,7 +12,7 @@ namespace Elsa.Workflows.Core.Services; public class WorkflowStorageDriver : IStorageDriver { /// - /// The key used to store the variables dictionary in the workflow state. + /// The key used to store the variables propertyBag in the workflow state. /// public const string VariablesDictionaryStateKey = "PersistentVariablesDictionary"; @@ -26,7 +27,7 @@ public class WorkflowStorageDriver : IStorageDriver public ValueTask ReadAsync(string id, StorageDriverContext context) { var dictionary = GetVariablesDictionary(context); - var value = dictionary.TryGetValue(id, out var v) ? v : default; + var value = dictionary.Dictionary.TryGetValue(id, out var v) ? v : default; return new(value); } @@ -37,13 +38,13 @@ public class WorkflowStorageDriver : IStorageDriver return ValueTask.CompletedTask; } - private IDictionary GetVariablesDictionary(StorageDriverContext context) => context.ExecutionContext.Properties.GetOrAdd(VariablesDictionaryStateKey, () => new Dictionary()); - private void SetVariablesDictionary(StorageDriverContext context, IDictionary dictionary) => context.ExecutionContext.Properties[VariablesDictionaryStateKey] = dictionary; + private PropertyBag GetVariablesDictionary(StorageDriverContext context) => context.ExecutionContext.Properties.GetOrAdd(VariablesDictionaryStateKey, () => new PropertyBag()); + private void SetVariablesDictionary(StorageDriverContext context, PropertyBag propertyBag) => context.ExecutionContext.Properties[VariablesDictionaryStateKey] = propertyBag; private void UpdateVariablesDictionary(StorageDriverContext context, Action> update) { var dictionary = GetVariablesDictionary(context); - update(dictionary); + update(dictionary.Dictionary); SetVariablesDictionary(context, dictionary); } } \ No newline at end of file