< Summary

Information
Class: Elsa.Workflows.Serialization.Converters.PolymorphicObjectConverter
Assembly: Elsa.Workflows.Core
File(s): /home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Workflows.Core/Serialization/Converters/PolymorphicObjectConverter.cs
Line coverage
83%
Covered lines: 178
Uncovered lines: 36
Coverable lines: 214
Total lines: 471
Line coverage: 83.1%
Branch coverage
82%
Covered branches: 163
Total branches: 197
Branch coverage: 82.7%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
.ctor()100%210%
Read(...)65.15%786686.11%
Write(...)76.47%693468.75%
IsPrimitive()100%2424100%
ReadType(...)88.88%292785%
WriteTypeMetadata(...)100%22100%
GetInstantiableTargetType(...)87.5%8885.71%
ReadPrimitive(...)87.5%8890%
ReadObject(...)60.71%732861.29%
EscapeKey(...)100%210%
UnescapeKey(...)100%11100%

File(s)

/home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Workflows.Core/Serialization/Converters/PolymorphicObjectConverter.cs

#LineLine coverage
 1using System.Collections;
 2using System.Dynamic;
 3using System.Reflection;
 4using System.Text.Json;
 5using System.Text.Json.Nodes;
 6using System.Text.Json.Serialization;
 7using Elsa.Extensions;
 8using Elsa.Workflows.Serialization.ReferenceHandlers;
 9using Newtonsoft.Json.Linq;
 10using Elsa.Common.Serialization;
 11
 12namespace Elsa.Workflows.Serialization.Converters;
 13
 14/// <summary>
 15/// Reads objects as primitive types rather than <see cref="JsonElement"/> values while also maintaining the .NET type n
 16/// </summary>
 17public class PolymorphicObjectConverter : JsonConverter<object>
 18{
 19    private const string TypePropertyName = "_type";
 20    private const string ItemsPropertyName = "_items";
 21    private const string IslandPropertyName = "_island";
 22    private const string IdPropertyName = "$id";
 23    private const string RefPropertyName = "$ref";
 24    private const string ValuesPropertyName = "$values";
 25    private readonly ISerializationTypeRegistry _workflowJsonTypeRegistry;
 26
 27    /// <summary>
 28    /// Initializes a new instance of the <see cref="PolymorphicObjectConverter"/> class.
 29    /// </summary>
 5349130    public PolymorphicObjectConverter(ISerializationTypeRegistry workflowJsonTypeRegistry)
 31    {
 5349132        _workflowJsonTypeRegistry = workflowJsonTypeRegistry;
 5349133    }
 34
 35    /// <summary>
 36    /// Initializes a new instance of the <see cref="PolymorphicObjectConverter"/> class.
 37    /// </summary>
 038    public PolymorphicObjectConverter()
 39    {
 040        _workflowJsonTypeRegistry = SerializationTypeRegistry.CreateDefault();
 041    }
 42
 43    /// <inheritdoc />
 44    public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
 45    {
 3829546        var newOptions = options.Clone();
 47
 3829548        if (reader.TokenType != JsonTokenType.StartObject && reader.TokenType != JsonTokenType.StartArray)
 2476349            return ReadPrimitive(ref reader, newOptions);
 50
 1353251        var targetType = ReadType(reader, options);
 1352952        if (targetType == null)
 1192453            return ReadObject(ref reader, newOptions);
 54
 160555        targetType = GetInstantiableTargetType(targetType);
 56
 57        // If the target type is not an IEnumerable, or is a dictionary, deserialize the object directly.
 160458        var isEnumerable = typeof(IEnumerable).IsAssignableFrom(targetType);
 59
 160460        if (!isEnumerable)
 61        {
 62            try
 63            {
 73264                return JsonSerializer.Deserialize(ref reader, targetType, newOptions)!;
 65            }
 066            catch (Exception e) when (e is NotSupportedException or TargetException)
 67            {
 068                return default!;
 69            }
 70        }
 71
 72        // If the target type is a Newtonsoft.JObject, parse the JSON island.
 87273        var isNewtonsoftObject = targetType == typeof(JObject);
 74
 87275        if (isNewtonsoftObject)
 76        {
 177            var parsedModel = JsonElement.ParseValue(ref reader);
 178            var newtonsoftJson = parsedModel.GetProperty(IslandPropertyName).GetString();
 179            return !string.IsNullOrWhiteSpace(newtonsoftJson) ? JObject.Parse(newtonsoftJson) : new JObject();
 80        }
 81
 82        // If the target type is a Newtonsoft.JArray, parse the JSON island.
 87183        var isNewtonsoftArray = targetType == typeof(JArray);
 84
 87185        if (isNewtonsoftArray)
 86        {
 187            var parsedModel = JsonElement.ParseValue(ref reader);
 188            var newtonsoftJson = parsedModel.GetProperty(IslandPropertyName).GetString();
 189            return !string.IsNullOrWhiteSpace(newtonsoftJson) ? JArray.Parse(newtonsoftJson) : new JArray();
 90        }
 91
 92        // If the target type is a System.Text.JsonObject, parse the JSON island.
 87093        var isJsonObject = targetType == typeof(JsonObject);
 94
 87095        if (isJsonObject)
 96        {
 197            var parsedModel = JsonElement.ParseValue(ref reader);
 198            var systemTextJson = parsedModel.GetProperty(IslandPropertyName).GetString();
 199            return !string.IsNullOrWhiteSpace(systemTextJson) ? JsonNode.Parse(systemTextJson)! : new JsonObject();
 100        }
 101
 869102        var isJsonArray = targetType == typeof(JsonArray);
 103
 869104        if (isJsonArray)
 105        {
 1106            var parsedModel = JsonElement.ParseValue(ref reader);
 1107            var systemTextJson = parsedModel.GetProperty(IslandPropertyName).GetString();
 1108            return !string.IsNullOrWhiteSpace(systemTextJson) ? JsonNode.Parse(systemTextJson)! : new JsonArray();
 109        }
 110
 868111        var isDictionary = typeof(IDictionary).IsAssignableFrom(targetType);
 868112        if (isDictionary)
 113        {
 114            // Remove the _type property name from the JSON, if any.
 837115            var parsedNode = JsonNode.Parse(ref reader)!;
 1674116            if (parsedNode is JsonObject parsedModel) parsedModel.Remove(TypePropertyName);
 837117            return parsedNode.Deserialize(targetType, newOptions)!;
 118        }
 119
 31120        var isCollection = typeof(ICollection).IsAssignableFrom(targetType);
 121
 122        // Otherwise, deserialize the object as an array.
 31123        var elementType = targetType.IsArray
 31124            ? targetType.GetElementType()
 31125            : targetType.GenericTypeArguments.FirstOrDefault() ??
 31126              (isCollection // Could be a class derived from Collection<T> or List<T>.
 31127                  ? targetType.BaseType?.GenericTypeArguments[0]
 31128                  : targetType.GenericTypeArguments.FirstOrDefault()
 31129                    ?? typeof(object));
 31130        if (elementType == null)
 0131            throw new InvalidOperationException($"Cannot determine the element type of array '{targetType}'.");
 132
 31133        var model = JsonElement.ParseValue(ref reader);
 31134        var referenceResolver = (newOptions.ReferenceHandler as CrossScopedReferenceHandler)?.GetResolver();
 135
 31136        if (model.TryGetProperty(RefPropertyName, out var refProperty))
 137        {
 0138            var refId = refProperty.GetString()!;
 0139            return referenceResolver?.ResolveReference(refId)!;
 140        }
 141
 31142        var values = model.TryGetProperty(ItemsPropertyName, out var itemsProp) ? itemsProp.EnumerateArray().ToList() : 
 31143        var id = model.TryGetProperty(IdPropertyName, out var idProp) ? idProp.GetString() : default;
 31144        var collection = targetType.IsArray ? Array.CreateInstance(elementType, values.Count) : Activator.CreateInstance
 31145        var index = 0;
 146
 31147        if (id != null)
 5148            referenceResolver?.AddReference(id, collection);
 149
 31150        var isHashSet = targetType.GenericTypeArguments.Length == 1 && typeof(ISet<>).MakeGenericType(targetType.Generic
 31151        var addSetMethod = targetType.GetMethod("Add", [elementType])!;
 152
 168153        foreach (var element in values)
 154        {
 53155            var deserializedElement = JsonSerializer.Deserialize(JsonSerializer.Serialize(element), elementType, newOpti
 53156            if (collection is Array array)
 157            {
 36158                array.SetValue(deserializedElement, index++);
 159            }
 17160            else if (isHashSet)
 161            {
 1162                addSetMethod.Invoke(collection, [
 1163                    deserializedElement
 1164                ]);
 165            }
 16166            else if (collection is IList list)
 167            {
 16168                list.Add(deserializedElement);
 169            }
 170        }
 171
 31172        return collection;
 732173    }
 174
 175    /// <inheritdoc />
 176    public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options)
 177    {
 16283178        if (value == null!)
 179        {
 104180            writer.WriteNullValue();
 104181            return;
 182        }
 183
 16179184        var newOptions = options.Clone();
 16179185        var type = value.GetType();
 186
 187        // If the type is a primitive type or an enumerable of a primitive type, serialize the value directly.
 188        bool IsPrimitive(Type valueType)
 189        {
 16179190            return type.IsPrimitive
 16179191                   || valueType == typeof(string)
 16179192                   || valueType == typeof(decimal)
 16179193                   || valueType == typeof(DateTimeOffset)
 16179194                   || valueType == typeof(DateTime)
 16179195                   || valueType == typeof(DateOnly)
 16179196                   || valueType == typeof(TimeOnly)
 16179197                   || valueType == typeof(JsonElement)
 16179198                   || valueType == typeof(Guid)
 16179199                   || valueType == typeof(TimeSpan)
 16179200                   || valueType == typeof(Uri)
 16179201                   || valueType == typeof(Version)
 16179202                   || valueType.IsEnum;
 203        }
 204
 16179205        if (IsPrimitive(type))
 206        {
 207            // Remove the converter so that we don't end up in an infinite loop.
 41180208            newOptions.Converters.RemoveWhere(x => x is PolymorphicObjectConverterFactory);
 209
 210            // Serialize the value directly.
 3082211            JsonSerializer.Serialize(writer, value, newOptions);
 3082212            return;
 213        }
 214
 215        // Special case for Newtonsoft.Json and System.Text.Json types.
 216        // Newtonsoft.Json types are not supported by the System.Text.Json serializer and should be written as a string 
 217        // We include metadata about the type so that we can deserialize it later.
 13097218        if (type == typeof(JObject) || type == typeof(JArray) || type == typeof(JsonObject) || type == typeof(JsonArray)
 219        {
 21220            writer.WriteStartObject();
 21221            writer.WriteString(IslandPropertyName, value.ToString());
 21222            WriteTypeMetadata(writer, type);
 21223            writer.WriteEndObject();
 21224            return;
 225        }
 226
 227        // Determine if the value is going to be serialized for the first time.
 228        // Later on, we need to know this information to determine if we need to write the type name or not, so that we 
 13076229        var shouldWriteTypeField = true;
 13076230        var referenceResolver = (CustomPreserveReferenceResolver?)(newOptions.ReferenceHandler as CrossScopedReferenceHa
 231
 13076232        if (referenceResolver != null)
 233        {
 130234            var exists = referenceResolver.HasReference(value);
 130235            shouldWriteTypeField = !exists;
 236        }
 237
 238        // Before we serialize the value, check to see if it's an ExpandoObject.
 239        // If it is, we need to sanitize its property names, because they can contain invalid characters.
 13076240        if (value is ExpandoObject)
 241        {
 0242            var sanitized = new ExpandoObject();
 0243            var dictionary = (IDictionary<string, object?>)sanitized;
 0244            var expando = (IDictionary<string, object?>)value;
 245
 0246            foreach (var kvp in expando)
 247            {
 0248                var key = EscapeKey(kvp.Key);
 0249                dictionary[key] = kvp.Value;
 250            }
 251
 0252            value = sanitized;
 253        }
 254
 13076255        var jsonElement = JsonDocument.Parse(JsonSerializer.Serialize(value, type, newOptions)).RootElement;
 256
 257        // If the value is a string, serialize it directly.
 13076258        if (jsonElement.ValueKind == JsonValueKind.String)
 259        {
 260            // Serialize the value directly.
 0261            JsonSerializer.Serialize(writer, jsonElement, newOptions);
 0262            return;
 263        }
 264
 265        // If the value was serialized as a primitive by another converter,
 266        // write it directly instead of assuming an object structure.
 13076267        if (jsonElement.ValueKind != JsonValueKind.Object &&
 13076268            jsonElement.ValueKind != JsonValueKind.Array)
 269        {
 0270            jsonElement.WriteTo(writer);
 0271            return;
 272        }
 273
 13076274        writer.WriteStartObject();
 275
 13076276        if (jsonElement.ValueKind == JsonValueKind.Array)
 277        {
 741278            writer.WritePropertyName(ItemsPropertyName);
 741279            jsonElement.WriteTo(writer);
 280        }
 281        else
 282        {
 86779283            foreach (var property in jsonElement.EnumerateObject().Where(property => !property.NameEquals(TypePropertyNa
 284            {
 20703285                writer.WritePropertyName(property.Name);
 20703286                property.Value.WriteTo(writer);
 287            }
 288        }
 289
 13076290        if (type != typeof(ExpandoObject))
 291        {
 13076292            if (shouldWriteTypeField)
 13076293                WriteTypeMetadata(writer, type);
 294        }
 295
 13076296        writer.WriteEndObject();
 13076297    }
 298
 299    private Type? ReadType(Utf8JsonReader reader, JsonSerializerOptions options)
 300    {
 13532301        if (reader.TokenType != JsonTokenType.StartObject)
 2208302            return null;
 303
 11324304        reader.Read(); // Move to the first token inside the object.
 11324305        string? typeName = null;
 306
 307        // Read while we haven't reached the end of the object.
 52026308        while (reader.TokenType != JsonTokenType.EndObject)
 309        {
 310            // If we find the _type property, read its value and break out of the loop.
 42310311            if (reader.TokenType == JsonTokenType.PropertyName && reader.ValueTextEquals(TypePropertyName))
 312            {
 1608313                reader.Read(); // Move to the value of the _type property
 1608314                if (options.Converters.OfType<TypeJsonConverter>().FirstOrDefault() is { } typeJsonConverter)
 315                {
 1607316                    return typeJsonConverter.Read(ref reader, typeof(Type), options);
 317                }
 318                else
 319                {
 1320                    typeName = reader.GetString();
 321                }
 1322                break;
 323            }
 324
 325            // Skip through nested objects and arrays.
 40702326            if (reader.TokenType is JsonTokenType.StartObject or JsonTokenType.StartArray)
 327            {
 6168328                var depth = 1;
 329
 34841330                while (depth > 0 && reader.Read())
 331                {
 28673332                    switch (reader.TokenType)
 333                    {
 334                        case JsonTokenType.StartObject:
 335                        case JsonTokenType.StartArray:
 0336                            depth++;
 0337                            break;
 338
 339                        case JsonTokenType.EndObject:
 340                        case JsonTokenType.EndArray:
 6168341                            depth--;
 342                            break;
 343                    }
 344                }
 345            }
 346
 40702347            reader.Read(); // Move to the next token
 348        }
 349
 350        // If we found the _type property, attempt to resolve the type.
 9717351        return typeName != null ? SerializationTypeResolver.ResolveType(_workflowJsonTypeRegistry, typeName) : default;
 352    }
 353
 354    private void WriteTypeMetadata(Utf8JsonWriter writer, Type type)
 355    {
 13097356        if (!SerializationTypeResolver.TryGetAlias(_workflowJsonTypeRegistry, type, out var typeAlias))
 2802357            return;
 358
 10295359        writer.WritePropertyName(TypePropertyName);
 10295360        writer.WriteStringValue(typeAlias);
 10295361    }
 362
 363    private static Type GetInstantiableTargetType(Type targetType)
 364    {
 1605365        if (targetType.ContainsGenericParameters)
 0366            throw new JsonException($"Workflow JSON type alias resolved to open generic type '{targetType}'.");
 367
 1605368        if (!targetType.IsInterface && !targetType.IsAbstract)
 1598369            return targetType;
 370
 7371        if (SerializationTypeResolver.TryGetInstantiableCollectionType(targetType, out var instantiableCollectionType))
 6372            return instantiableCollectionType;
 373
 1374        throw new JsonException($"Workflow JSON type alias resolved to non-instantiable type '{targetType}'.");
 375    }
 376
 377    private static object ReadPrimitive(ref Utf8JsonReader reader, JsonSerializerOptions options)
 378    {
 24763379        return (reader.TokenType switch
 24763380        {
 436381            JsonTokenType.True => true,
 8487382            JsonTokenType.False => false,
 16008383            JsonTokenType.Number when reader.TryGetInt64(out var l) => l,
 4704384            JsonTokenType.Number => reader.GetDouble(),
 5307385            JsonTokenType.String => reader.GetString(),
 177386            JsonTokenType.Null => null,
 0387            _ => throw new JsonException("Not a primitive type.")
 24763388        })!;
 389    }
 390
 391    private object ReadObject(ref Utf8JsonReader reader, JsonSerializerOptions options)
 392    {
 11924393        switch (reader.TokenType)
 394        {
 395            case JsonTokenType.StartArray:
 396                {
 2208397                    var list = new List<object>();
 2208398                    while (reader.Read())
 399                    {
 2208400                        switch (reader.TokenType)
 401                        {
 402                            default:
 0403                                list.Add(Read(ref reader, typeof(object), options));
 0404                                break;
 405
 406                            case JsonTokenType.EndArray:
 2208407                                return list;
 408                        }
 409                    }
 410
 0411                    throw new JsonException();
 412                }
 413            case JsonTokenType.StartObject:
 9716414                var dict = new ExpandoObject() as IDictionary<string, object>;
 9716415                var referenceResolver = (CustomPreserveReferenceResolver)(options.ReferenceHandler as CrossScopedReferen
 27965416                while (reader.Read())
 417                {
 27965418                    switch (reader.TokenType)
 419                    {
 420                        case JsonTokenType.EndObject:
 421                            // If the object contains a single entry with a key of $ref, return the referenced object.
 9716422                            if (dict.Count == 1 && dict.TryGetValue(RefPropertyName, out var referencedObject))
 0423                                return referencedObject;
 9716424                            return dict;
 425
 426                        case JsonTokenType.PropertyName:
 18249427                            var key = reader.GetString()!;
 18249428                            reader.Read();
 18249429                            if (referenceResolver != null && key == RefPropertyName)
 430                            {
 0431                                var referenceId = reader.GetString();
 0432                                var reference = referenceResolver.ResolveReference(referenceId!);
 0433                                dict.Add(key, reference);
 434                            }
 18249435                            else if (referenceResolver != null && key == IdPropertyName)
 436                            {
 0437                                var referenceId = reader.GetString()!;
 438
 439                                // Attempt to add the reference; if not found, we can ignore it and assume that the user
 0440                                referenceResolver.TryAddReference(referenceId, dict);
 441                            }
 442                            else
 443                            {
 18249444                                var value = Read(ref reader, typeof(object), options);
 18249445                                var unescapedKey = UnescapeKey(key);
 18249446                                dict.Add(unescapedKey, value);
 447                            }
 448
 18249449                            break;
 450
 451                        default:
 0452                            throw new JsonException();
 453                    }
 454                }
 455
 0456                throw new JsonException();
 457            default:
 0458                throw new JsonException($"Unknown token {reader.TokenType}");
 459        }
 460    }
 461
 462    private string EscapeKey(string key)
 463    {
 0464        return key.Replace("$", @"\\$");
 465    }
 466
 467    private string UnescapeKey(string key)
 468    {
 18249469        return key.Replace(@"\\$", "$");
 470    }
 471}