Fix JSON serialization issues

This commit is contained in:
Sipke Schoorstra 2023-03-22 19:28:38 +01:00
parent 8ba344f95a
commit 8289b03f79
10 changed files with 43 additions and 70 deletions

View file

@ -22,7 +22,7 @@ public record ObjectConverterOptions(JsonSerializerOptions? SerializerOptions =
public static class ObjectConverter
{
public static Result TryConvertTo<T>(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<string, object>).IsAssignableFrom(underlyingSourceType) && underlyingTargetType.IsClass)
{
if (value is IDictionary<string, object> dictionary && typeof(IDictionary<string, object>).IsAssignableFrom(underlyingTargetType))
return new Dictionary<string, object>(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<string, object>).IsAssignableFrom(underlyingTargetType))
return new Dictionary<string, object>((IDictionary<string, object>)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);

View file

@ -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,

View file

@ -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,

View file

@ -1,36 +0,0 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Elsa.Extensions;
namespace Elsa.Workflows.Core.Serialization.Converters;
/// <summary>
/// A converter that stores type information in order to deserialize the object back into the same type.
/// </summary>
public class PolymorphicConverter : JsonConverter<object>
{
/// <inheritdoc />
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);
}
/// <inheritdoc />
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!;
}
}

View file

@ -11,7 +11,7 @@ public class PolymorphicDictionaryConverter : JsonConverter<IDictionary<string,
public PolymorphicDictionaryConverter(JsonSerializerOptions options)
{
var newOptions = new JsonSerializerOptions(options);
newOptions.Converters.Insert(0, new SystemObjectWithTypeHandlingConverterFactory());
newOptions.Converters.Insert(0, new PolymorphicObjectConverterFactory());
_objectConverter = (JsonConverter<object>)newOptions.GetConverter(typeof(object));
}

View file

@ -8,13 +8,13 @@ namespace Elsa.Workflows.Core.Serialization.Converters;
/// <summary>
/// Used for reading objects as primitive types rather than <see cref="JsonElement"/> values while also maintaining the .NET type name for reconstructing the actual type.
/// </summary>
public class SystemObjectWithTypeHandlingConverter : JsonConverter<object>
public class PolymorphicObjectConverter : JsonConverter<object>
{
private const string TypePropertyName = "_type";
private const string ItemsPropertyName = "_items";
/// <inheritdoc />
public SystemObjectWithTypeHandlingConverter()
public PolymorphicObjectConverter()
{
}
@ -33,6 +33,7 @@ public class SystemObjectWithTypeHandlingConverter : JsonConverter<object>
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<object>
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<object>
}
}
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();
}
}

View file

@ -6,11 +6,11 @@ namespace Elsa.Workflows.Core.Serialization.Converters;
/// <summary>
/// A JSON converter for <see cref="System.Object"/> objects.
/// </summary>
public class SystemObjectWithTypeHandlingConverterFactory : JsonConverterFactory
public class PolymorphicObjectConverterFactory : JsonConverterFactory
{
/// <inheritdoc />
public override bool CanConvert(Type typeToConvert) => true;
/// <inheritdoc />
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) => new SystemObjectWithTypeHandlingConverter();
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) => new PolymorphicObjectConverter();
}

View file

@ -23,7 +23,6 @@ public class PropertyBagConverter : JsonConverter<PropertyBag>
{
var newOptions = new JsonSerializerOptions();
newOptions.Converters.Add(new PolymorphicDictionaryConverterFactory());
JsonSerializer.Serialize(writer, value.Dictionary, newOptions);
}
}

View file

@ -27,6 +27,6 @@ public class SystemObjectPrimitiveConverter : JsonConverter<object>
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options)
{
throw new InvalidOperationException("Should not get here.");
JsonSerializer.Serialize(writer, value, value.GetType());
}
}

View file

@ -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
{
/// <summary>
/// The key used to store the variables dictionary in the workflow state.
/// The key used to store the variables propertyBag in the workflow state.
/// </summary>
public const string VariablesDictionaryStateKey = "PersistentVariablesDictionary";
@ -26,7 +27,7 @@ public class WorkflowStorageDriver : IStorageDriver
public ValueTask<object?> 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<string, object> GetVariablesDictionary(StorageDriverContext context) => context.ExecutionContext.Properties.GetOrAdd(VariablesDictionaryStateKey, () => new Dictionary<string, object>());
private void SetVariablesDictionary(StorageDriverContext context, IDictionary<string, object> 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<IDictionary<string, object>> update)
{
var dictionary = GetVariablesDictionary(context);
update(dictionary);
update(dictionary.Dictionary);
SetVariablesDictionary(context, dictionary);
}
}