< 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
74%
Covered lines: 148
Uncovered lines: 51
Coverable lines: 199
Total lines: 439
Line coverage: 74.3%
Branch coverage
76%
Covered branches: 144
Total branches: 189
Branch coverage: 76.1%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
Read(...)65.15%1636671.83%
Write(...)75%763668.62%
IsPrimitive()100%2424100%
ReadType(...)85.18%372776.19%
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;
 10
 11namespace Elsa.Workflows.Serialization.Converters;
 12
 13/// <summary>
 14/// Reads objects as primitive types rather than <see cref="JsonElement"/> values while also maintaining the .NET type n
 15/// </summary>
 16public class PolymorphicObjectConverter : JsonConverter<object>
 17{
 18    private const string TypePropertyName = "_type";
 19    private const string ItemsPropertyName = "_items";
 20    private const string IslandPropertyName = "_island";
 21    private const string IdPropertyName = "$id";
 22    private const string RefPropertyName = "$ref";
 23    private const string ValuesPropertyName = "$values";
 24
 25    /// <inheritdoc />
 26    public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
 27    {
 94479428        var newOptions = options.Clone();
 29
 94479430        if (reader.TokenType != JsonTokenType.StartObject && reader.TokenType != JsonTokenType.StartArray)
 81500631            return ReadPrimitive(ref reader, newOptions);
 32
 12978833        var targetType = ReadType(reader, options);
 12978834        if (targetType == null)
 1528335            return ReadObject(ref reader, newOptions);
 36
 37        // If the target type is not an IEnumerable, or is a dictionary, deserialize the object directly.
 11450538        var isEnumerable = typeof(IEnumerable).IsAssignableFrom(targetType);
 39
 11450540        if (!isEnumerable)
 41        {
 42            try
 43            {
 3495644                return JsonSerializer.Deserialize(ref reader, targetType, newOptions)!;
 45            }
 046            catch (Exception e) when (e is NotSupportedException or TargetException)
 47            {
 048                return default!;
 49            }
 50        }
 51
 52        // If the target type is a Newtonsoft.JObject, parse the JSON island.
 7954953        var isNewtonsoftObject = targetType == typeof(JObject);
 54
 7954955        if (isNewtonsoftObject)
 56        {
 057            var parsedModel = JsonElement.ParseValue(ref reader);
 058            var newtonsoftJson = parsedModel.GetProperty(IslandPropertyName).GetString();
 059            return !string.IsNullOrWhiteSpace(newtonsoftJson) ? JObject.Parse(newtonsoftJson) : new JObject();
 60        }
 61
 62        // If the target type is a Newtonsoft.JArray, parse the JSON island.
 7954963        var isNewtonsoftArray = targetType == typeof(JArray);
 64
 7954965        if (isNewtonsoftArray)
 66        {
 067            var parsedModel = JsonElement.ParseValue(ref reader);
 068            var newtonsoftJson = parsedModel.GetProperty(IslandPropertyName).GetString();
 069            return !string.IsNullOrWhiteSpace(newtonsoftJson) ? JArray.Parse(newtonsoftJson) : new JArray();
 70        }
 71
 72        // If the target type is a System.Text.JsonObject, parse the JSON island.
 7954973        var isJsonObject = targetType == typeof(JsonObject);
 74
 7954975        if (isJsonObject)
 76        {
 077            var parsedModel = JsonElement.ParseValue(ref reader);
 078            var systemTextJson = parsedModel.GetProperty(IslandPropertyName).GetString();
 079            return !string.IsNullOrWhiteSpace(systemTextJson) ? JsonNode.Parse(systemTextJson)! : new JsonObject();
 80        }
 81
 7954982        var isJsonArray = targetType == typeof(JsonArray);
 83
 7954984        if (isJsonArray)
 85        {
 086            var parsedModel = JsonElement.ParseValue(ref reader);
 087            var systemTextJson = parsedModel.GetProperty(IslandPropertyName).GetString();
 088            return !string.IsNullOrWhiteSpace(systemTextJson) ? JsonNode.Parse(systemTextJson)! : new JsonArray();
 89        }
 90
 7954991        var isDictionary = typeof(IDictionary).IsAssignableFrom(targetType);
 7954992        if (isDictionary)
 93        {
 94            // Remove the _type property name from the JSON, if any.
 2351995            var parsedNode = JsonNode.Parse(ref reader)!;
 4703896            if (parsedNode is JsonObject parsedModel) parsedModel.Remove(TypePropertyName);
 2351997            return parsedNode.Deserialize(targetType, newOptions)!;
 98        }
 99
 56030100        var isCollection = typeof(ICollection).IsAssignableFrom(targetType);
 101
 102        // Otherwise, deserialize the object as an array.
 56030103        var elementType = targetType.IsArray
 56030104            ? targetType.GetElementType()
 56030105            : targetType.GenericTypeArguments.FirstOrDefault() ??
 56030106              (isCollection // Could be a class derived from Collection<T> or List<T>.
 56030107                  ? targetType.BaseType?.GenericTypeArguments[0]
 56030108                  : targetType.GenericTypeArguments.FirstOrDefault()
 56030109                    ?? typeof(object));
 56030110        if (elementType == null)
 0111            throw new InvalidOperationException($"Cannot determine the element type of array '{targetType}'.");
 112
 56030113        var model = JsonElement.ParseValue(ref reader);
 56030114        var referenceResolver = (newOptions.ReferenceHandler as CrossScopedReferenceHandler)?.GetResolver();
 115
 56030116        if (model.TryGetProperty(RefPropertyName, out var refProperty))
 117        {
 0118            var refId = refProperty.GetString()!;
 0119            return referenceResolver?.ResolveReference(refId)!;
 120        }
 121
 56030122        var values = model.TryGetProperty(ItemsPropertyName, out var itemsProp) ? itemsProp.EnumerateArray().ToList() : 
 56030123        var id = model.TryGetProperty(IdPropertyName, out var idProp) ? idProp.GetString() : default;
 56030124        var collection = targetType.IsArray ? Array.CreateInstance(elementType, values.Count) : Activator.CreateInstance
 56030125        var index = 0;
 126
 56030127        if (id != null)
 56013128            referenceResolver?.AddReference(id, collection);
 129
 56030130        var isHashSet = targetType.GenericTypeArguments.Length == 1 && typeof(ISet<>).MakeGenericType(targetType.Generic
 56030131        var addSetMethod = targetType.GetMethod("Add", [elementType])!;
 132
 224170133        foreach (var element in values)
 134        {
 56055135            var deserializedElement = JsonSerializer.Deserialize(JsonSerializer.Serialize(element), elementType, newOpti
 56055136            if (collection is Array array)
 137            {
 36138                array.SetValue(deserializedElement, index++);
 139            }
 56019140            else if (isHashSet)
 141            {
 0142                addSetMethod.Invoke(collection, [
 0143                    deserializedElement
 0144                ]);
 145            }
 56019146            else if (collection is IList list)
 147            {
 56019148                list.Add(deserializedElement);
 149            }
 150        }
 151
 56030152        return collection;
 34956153    }
 154
 155    /// <inheritdoc />
 156    public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options)
 157    {
 13343158        if (value == null!)
 159        {
 98160            writer.WriteNullValue();
 98161            return;
 162        }
 163
 13245164        var newOptions = options.Clone();
 13245165        var type = value.GetType();
 166
 167        // If the type is a primitive type or an enumerable of a primitive type, serialize the value directly.
 168        bool IsPrimitive(Type valueType)
 169        {
 13245170            return type.IsPrimitive
 13245171                   || valueType == typeof(string)
 13245172                   || valueType == typeof(decimal)
 13245173                   || valueType == typeof(DateTimeOffset)
 13245174                   || valueType == typeof(DateTime)
 13245175                   || valueType == typeof(DateOnly)
 13245176                   || valueType == typeof(TimeOnly)
 13245177                   || valueType == typeof(JsonElement)
 13245178                   || valueType == typeof(Guid)
 13245179                   || valueType == typeof(TimeSpan)
 13245180                   || valueType == typeof(Uri)
 13245181                   || valueType == typeof(Version)
 13245182                   || valueType.IsEnum;
 183        }
 184
 13245185        if (IsPrimitive(type))
 186        {
 187            // Remove the converter so that we don't end up in an infinite loop.
 27394188            newOptions.Converters.RemoveWhere(x => x is PolymorphicObjectConverterFactory);
 189
 190            // Serialize the value directly.
 2184191            JsonSerializer.Serialize(writer, value, newOptions);
 2184192            return;
 193        }
 194
 195        // Special case for Newtonsoft.Json and System.Text.Json types.
 196        // Newtonsoft.Json types are not supported by the System.Text.Json serializer and should be written as a string 
 197        // We include metadata about the type so that we can deserialize it later.
 11061198        if (type == typeof(JObject) || type == typeof(JArray) || type == typeof(JsonObject) || type == typeof(JsonArray)
 199        {
 17200            writer.WriteStartObject();
 17201            writer.WriteString(IslandPropertyName, value.ToString());
 17202            writer.WriteString(TypePropertyName, type.GetSimpleAssemblyQualifiedName());
 17203            writer.WriteEndObject();
 17204            return;
 205        }
 206
 207        // Determine if the value is going to be serialized for the first time.
 208        // Later on, we need to know this information to determine if we need to write the type name or not, so that we 
 11044209        var shouldWriteTypeField = true;
 11044210        var referenceResolver = (CustomPreserveReferenceResolver?)(newOptions.ReferenceHandler as CrossScopedReferenceHa
 211
 11044212        if (referenceResolver != null)
 213        {
 138214            var exists = referenceResolver.HasReference(value);
 138215            shouldWriteTypeField = !exists;
 216        }
 217
 218        // Before we serialize the value, check to see if it's an ExpandoObject.
 219        // If it is, we need to sanitize its property names, because they can contain invalid characters.
 11044220        if (value is ExpandoObject)
 221        {
 0222            var sanitized = new ExpandoObject();
 0223            var dictionary = (IDictionary<string, object?>)sanitized;
 0224            var expando = (IDictionary<string, object?>)value;
 225
 0226            foreach (var kvp in expando)
 227            {
 0228                var key = EscapeKey(kvp.Key);
 0229                dictionary[key] = kvp.Value;
 230            }
 231
 0232            value = sanitized;
 233        }
 234
 11044235        var jsonElement = JsonDocument.Parse(JsonSerializer.Serialize(value, type, newOptions)).RootElement;
 236
 237        // If the value is a string, serialize it directly.
 11044238        if (jsonElement.ValueKind == JsonValueKind.String)
 239        {
 240            // Serialize the value directly.
 0241            JsonSerializer.Serialize(writer, jsonElement, newOptions);
 0242            return;
 243        }
 244
 245        // If the value was serialized as a primitive by another converter,
 246        // write it directly instead of assuming an object structure.
 11044247        if (jsonElement.ValueKind != JsonValueKind.Object &&
 11044248            jsonElement.ValueKind != JsonValueKind.Array)
 249        {
 0250            jsonElement.WriteTo(writer);
 0251            return;
 252        }
 253
 11044254        writer.WriteStartObject();
 255
 11044256        if (jsonElement.ValueKind == JsonValueKind.Array)
 257        {
 741258            writer.WritePropertyName(ItemsPropertyName);
 741259            jsonElement.WriteTo(writer);
 260        }
 261        else
 262        {
 60710263            foreach (var property in jsonElement.EnumerateObject().Where(property => !property.NameEquals(TypePropertyNa
 264            {
 13368265                writer.WritePropertyName(property.Name);
 13368266                property.Value.WriteTo(writer);
 267            }
 268        }
 269
 11044270        if (type != typeof(ExpandoObject))
 271        {
 11044272            if (shouldWriteTypeField)
 273            {
 11044274                if (newOptions.Converters.OfType<TypeJsonConverter>().FirstOrDefault() is { } typeJsonConverter)
 275                {
 11044276                    writer.WritePropertyName(TypePropertyName);
 11044277                    typeJsonConverter.Write(writer, type, newOptions);
 278                }
 279                else
 280                {
 0281                    writer.WriteString(TypePropertyName, type.GetSimpleAssemblyQualifiedName());
 282                }
 283            }
 284        }
 285
 11044286        writer.WriteEndObject();
 11044287    }
 288
 289    private Type? ReadType(Utf8JsonReader reader, JsonSerializerOptions options)
 290    {
 129788291        if (reader.TokenType != JsonTokenType.StartObject)
 2653292            return null;
 293
 127135294        reader.Read(); // Move to the first token inside the object.
 127135295        string? typeName = null;
 296
 297        // Read while we haven't reached the end of the object.
 718375298        while (reader.TokenType != JsonTokenType.EndObject)
 299        {
 300            // If we find the _type property, read its value and break out of the loop.
 705745301            if (reader.TokenType == JsonTokenType.PropertyName && reader.ValueTextEquals(TypePropertyName))
 302            {
 114505303                reader.Read(); // Move to the value of the _type property
 114505304                if (options.Converters.OfType<TypeJsonConverter>().FirstOrDefault() is { } typeJsonConverter)
 305                {
 114505306                    return typeJsonConverter.Read(ref reader, typeof(Type), options);
 307                }
 308                else
 309                {
 0310                    typeName = reader.GetString();
 311                }
 0312                break;
 313            }
 314
 315            // Skip through nested objects and arrays.
 591240316            if (reader.TokenType is JsonTokenType.StartObject or JsonTokenType.StartArray)
 317            {
 64308318                var depth = 1;
 319
 214337320                while (depth > 0 && reader.Read())
 321                {
 150029322                    switch (reader.TokenType)
 323                    {
 324                        case JsonTokenType.StartObject:
 325                        case JsonTokenType.StartArray:
 0326                            depth++;
 0327                            break;
 328
 329                        case JsonTokenType.EndObject:
 330                        case JsonTokenType.EndArray:
 64308331                            depth--;
 332                            break;
 333                    }
 334                }
 335            }
 336
 591240337            reader.Read(); // Move to the next token
 338        }
 339
 340        // If we found the _type property, attempt to resolve the type.
 12630341        var targetType = typeName != null ? Type.GetType(typeName) : default;
 0342        return targetType;
 343    }
 344
 345    private static object ReadPrimitive(ref Utf8JsonReader reader, JsonSerializerOptions options)
 346    {
 815006347        return (reader.TokenType switch
 815006348        {
 531349            JsonTokenType.True => true,
 289640350            JsonTokenType.False => false,
 133089351            JsonTokenType.Number when reader.TryGetInt64(out var l) => l,
 6191352            JsonTokenType.Number => reader.GetDouble(),
 62957353            JsonTokenType.String => reader.GetString(),
 392238354            JsonTokenType.Null => null,
 0355            _ => throw new JsonException("Not a primitive type.")
 815006356        })!;
 357    }
 358
 359    private object ReadObject(ref Utf8JsonReader reader, JsonSerializerOptions options)
 360    {
 15283361        switch (reader.TokenType)
 362        {
 363            case JsonTokenType.StartArray:
 364                {
 2653365                    var list = new List<object>();
 2653366                    while (reader.Read())
 367                    {
 2653368                        switch (reader.TokenType)
 369                        {
 370                            default:
 0371                                list.Add(Read(ref reader, typeof(object), options));
 0372                                break;
 373
 374                            case JsonTokenType.EndArray:
 2653375                                return list;
 376                        }
 377                    }
 378
 0379                    throw new JsonException();
 380                }
 381            case JsonTokenType.StartObject:
 12630382                var dict = new ExpandoObject() as IDictionary<string, object>;
 12630383                var referenceResolver = (CustomPreserveReferenceResolver)(options.ReferenceHandler as CrossScopedReferen
 36577384                while (reader.Read())
 385                {
 36577386                    switch (reader.TokenType)
 387                    {
 388                        case JsonTokenType.EndObject:
 389                            // If the object contains a single entry with a key of $ref, return the referenced object.
 12630390                            if (dict.Count == 1 && dict.TryGetValue(RefPropertyName, out var referencedObject))
 0391                                return referencedObject;
 12630392                            return dict;
 393
 394                        case JsonTokenType.PropertyName:
 23947395                            var key = reader.GetString()!;
 23947396                            reader.Read();
 23947397                            if (referenceResolver != null && key == RefPropertyName)
 398                            {
 0399                                var referenceId = reader.GetString();
 0400                                var reference = referenceResolver.ResolveReference(referenceId!);
 0401                                dict.Add(key, reference);
 402                            }
 23947403                            else if (referenceResolver != null && key == IdPropertyName)
 404                            {
 0405                                var referenceId = reader.GetString()!;
 406
 407                                // Attempt to add the reference; if not found, we can ignore it and assume that the user
 0408                                referenceResolver.TryAddReference(referenceId, dict);
 409                            }
 410                            else
 411                            {
 23947412                                var value = Read(ref reader, typeof(object), options);
 23947413                                var unescapedKey = UnescapeKey(key);
 23947414                                dict.Add(unescapedKey, value);
 415                            }
 416
 23947417                            break;
 418
 419                        default:
 0420                            throw new JsonException();
 421                    }
 422                }
 423
 0424                throw new JsonException();
 425            default:
 0426                throw new JsonException($"Unknown token {reader.TokenType}");
 427        }
 428    }
 429
 430    private string EscapeKey(string key)
 431    {
 0432        return key.Replace("$", @"\\$");
 433    }
 434
 435    private string UnescapeKey(string key)
 436    {
 23947437        return key.Replace(@"\\$", "$");
 438    }
 439}