< 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: 146
Uncovered lines: 49
Coverable lines: 195
Total lines: 430
Line coverage: 74.8%
Branch coverage
76%
Covered branches: 141
Total branches: 185
Branch coverage: 76.2%
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%593270.21%
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    {
 2053328        var newOptions = options.Clone();
 29
 2053330        if (reader.TokenType != JsonTokenType.StartObject && reader.TokenType != JsonTokenType.StartArray)
 1334031            return ReadPrimitive(ref reader, newOptions);
 32
 719333        var targetType = ReadType(reader, options);
 719334        if (targetType == null)
 677135            return ReadObject(ref reader, newOptions);
 36
 37        // If the target type is not an IEnumerable, or is a dictionary, deserialize the object directly.
 42238        var isEnumerable = typeof(IEnumerable).IsAssignableFrom(targetType);
 39
 42240        if (!isEnumerable)
 41        {
 42            try
 43            {
 25744                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.
 16553        var isNewtonsoftObject = targetType == typeof(JObject);
 54
 16555        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.
 16563        var isNewtonsoftArray = targetType == typeof(JArray);
 64
 16565        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.
 16573        var isJsonObject = targetType == typeof(JsonObject);
 74
 16575        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
 16582        var isJsonArray = targetType == typeof(JsonArray);
 83
 16584        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
 16591        var isDictionary = typeof(IDictionary).IsAssignableFrom(targetType);
 16592        if (isDictionary)
 93        {
 94            // Remove the _type property name from the JSON, if any.
 14395            var parsedNode = JsonNode.Parse(ref reader)!;
 28696            if (parsedNode is JsonObject parsedModel) parsedModel.Remove(TypePropertyName);
 14397            return parsedNode.Deserialize(targetType, newOptions)!;
 98        }
 99
 22100        var isCollection = typeof(ICollection).IsAssignableFrom(targetType);
 101
 102        // Otherwise, deserialize the object as an array.
 22103        var elementType = targetType.IsArray
 22104            ? targetType.GetElementType()
 22105            : targetType.GenericTypeArguments.FirstOrDefault() ??
 22106              (isCollection // Could be a class derived from Collection<T> or List<T>.
 22107                  ? targetType.BaseType?.GenericTypeArguments[0]
 22108                  : targetType.GenericTypeArguments.FirstOrDefault()
 22109                    ?? typeof(object));
 22110        if (elementType == null)
 0111            throw new InvalidOperationException($"Cannot determine the element type of array '{targetType}'.");
 112
 22113        var model = JsonElement.ParseValue(ref reader);
 22114        var referenceResolver = (newOptions.ReferenceHandler as CrossScopedReferenceHandler)?.GetResolver();
 115
 22116        if (model.TryGetProperty(RefPropertyName, out var refProperty))
 117        {
 0118            var refId = refProperty.GetString()!;
 0119            return referenceResolver?.ResolveReference(refId)!;
 120        }
 121
 22122        var values = model.TryGetProperty(ItemsPropertyName, out var itemsProp) ? itemsProp.EnumerateArray().ToList() : 
 22123        var id = model.TryGetProperty(IdPropertyName, out var idProp) ? idProp.GetString() : default;
 22124        var collection = targetType.IsArray ? Array.CreateInstance(elementType, values.Count) : Activator.CreateInstance
 22125        var index = 0;
 126
 22127        if (id != null)
 5128            referenceResolver?.AddReference(id, collection);
 129
 22130        var isHashSet = targetType.GenericTypeArguments.Length == 1 && typeof(ISet<>).MakeGenericType(targetType.Generic
 22131        var addSetMethod = targetType.GetMethod("Add", [elementType])!;
 132
 138133        foreach (var element in values)
 134        {
 47135            var deserializedElement = JsonSerializer.Deserialize(JsonSerializer.Serialize(element), elementType, newOpti
 47136            if (collection is Array array)
 137            {
 36138                array.SetValue(deserializedElement, index++);
 139            }
 11140            else if (isHashSet)
 141            {
 0142                addSetMethod.Invoke(collection, [
 0143                    deserializedElement
 0144                ]);
 145            }
 11146            else if (collection is IList list)
 147            {
 11148                list.Add(deserializedElement);
 149            }
 150        }
 151
 22152        return collection;
 257153    }
 154
 155    /// <inheritdoc />
 156    public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options)
 157    {
 9566158        if (value == null!)
 159        {
 91160            writer.WriteNullValue();
 91161            return;
 162        }
 163
 9475164        var newOptions = options.Clone();
 9475165        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        {
 9475170            return type.IsPrimitive
 9475171                   || valueType == typeof(string)
 9475172                   || valueType == typeof(decimal)
 9475173                   || valueType == typeof(DateTimeOffset)
 9475174                   || valueType == typeof(DateTime)
 9475175                   || valueType == typeof(DateOnly)
 9475176                   || valueType == typeof(TimeOnly)
 9475177                   || valueType == typeof(JsonElement)
 9475178                   || valueType == typeof(Guid)
 9475179                   || valueType == typeof(TimeSpan)
 9475180                   || valueType == typeof(Uri)
 9475181                   || valueType == typeof(Version)
 9475182                   || valueType.IsEnum;
 183        }
 184
 9475185        if (IsPrimitive(type))
 186        {
 187            // Remove the converter so that we don't end up in an infinite loop.
 13254188            newOptions.Converters.RemoveWhere(x => x is PolymorphicObjectConverterFactory);
 189
 190            // Serialize the value directly.
 1110191            JsonSerializer.Serialize(writer, value, newOptions);
 1110192            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.
 8365198        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 
 8348209        var shouldWriteTypeField = true;
 8348210        var referenceResolver = (CustomPreserveReferenceResolver?)(newOptions.ReferenceHandler as CrossScopedReferenceHa
 211
 8348212        if (referenceResolver != null)
 213        {
 85214            var exists = referenceResolver.HasReference(value);
 85215            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.
 8348220        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
 8348235        var jsonElement = JsonDocument.Parse(JsonSerializer.Serialize(value, type, newOptions)).RootElement;
 236
 237        // If the value is a string, serialize it directly.
 8348238        if (jsonElement.ValueKind == JsonValueKind.String)
 239        {
 240            // Serialize the value directly.
 0241            JsonSerializer.Serialize(writer, jsonElement, newOptions);
 0242            return;
 243        }
 244
 8348245        writer.WriteStartObject();
 246
 8348247        if (jsonElement.ValueKind == JsonValueKind.Array)
 248        {
 758249            writer.WritePropertyName(ItemsPropertyName);
 758250            jsonElement.WriteTo(writer);
 251        }
 252        else
 253        {
 31575254            foreach (var property in jsonElement.EnumerateObject().Where(property => !property.NameEquals(TypePropertyNa
 255            {
 5465256                writer.WritePropertyName(property.Name);
 5465257                property.Value.WriteTo(writer);
 258            }
 259        }
 260
 8348261        if (type != typeof(ExpandoObject))
 262        {
 8348263            if (shouldWriteTypeField)
 264            {
 8348265                if (newOptions.Converters.OfType<TypeJsonConverter>().FirstOrDefault() is { } typeJsonConverter)
 266                {
 8348267                    writer.WritePropertyName(TypePropertyName);
 8348268                    typeJsonConverter.Write(writer, type, newOptions);
 269                }
 270                else
 271                {
 0272                    writer.WriteString(TypePropertyName, type.GetSimpleAssemblyQualifiedName());
 273                }
 274            }
 275        }
 276
 8348277        writer.WriteEndObject();
 8348278    }
 279
 280    private Type? ReadType(Utf8JsonReader reader, JsonSerializerOptions options)
 281    {
 7193282        if (reader.TokenType != JsonTokenType.StartObject)
 1482283            return null;
 284
 5711285        reader.Read(); // Move to the first token inside the object.
 5711286        string? typeName = null;
 287
 288        // Read while we haven't reached the end of the object.
 27357289        while (reader.TokenType != JsonTokenType.EndObject)
 290        {
 291            // If we find the _type property, read its value and break out of the loop.
 22068292            if (reader.TokenType == JsonTokenType.PropertyName && reader.ValueTextEquals(TypePropertyName))
 293            {
 422294                reader.Read(); // Move to the value of the _type property
 422295                if (options.Converters.OfType<TypeJsonConverter>().FirstOrDefault() is { } typeJsonConverter)
 296                {
 422297                    return typeJsonConverter.Read(ref reader, typeof(Type), options);
 298                }
 299                else
 300                {
 0301                    typeName = reader.GetString();
 302                }
 0303                break;
 304            }
 305
 306            // Skip through nested objects and arrays.
 21646307            if (reader.TokenType is JsonTokenType.StartObject or JsonTokenType.StartArray)
 308            {
 3495309                var depth = 1;
 310
 20105311                while (depth > 0 && reader.Read())
 312                {
 16610313                    switch (reader.TokenType)
 314                    {
 315                        case JsonTokenType.StartObject:
 316                        case JsonTokenType.StartArray:
 0317                            depth++;
 0318                            break;
 319
 320                        case JsonTokenType.EndObject:
 321                        case JsonTokenType.EndArray:
 3495322                            depth--;
 323                            break;
 324                    }
 325                }
 326            }
 327
 21646328            reader.Read(); // Move to the next token
 329        }
 330
 331        // If we found the _type property, attempt to resolve the type.
 5289332        var targetType = typeName != null ? Type.GetType(typeName) : default;
 0333        return targetType;
 334    }
 335
 336    private static object ReadPrimitive(ref Utf8JsonReader reader, JsonSerializerOptions options)
 337    {
 13340338        return (reader.TokenType switch
 13340339        {
 309340            JsonTokenType.True => true,
 4724341            JsonTokenType.False => false,
 9711342            JsonTokenType.Number when reader.TryGetInt64(out var l) => l,
 2779343            JsonTokenType.Number => reader.GetDouble(),
 1961344            JsonTokenType.String => reader.GetString(),
 101345            JsonTokenType.Null => null,
 0346            _ => throw new JsonException("Not a primitive type.")
 13340347        })!;
 348    }
 349
 350    private object ReadObject(ref Utf8JsonReader reader, JsonSerializerOptions options)
 351    {
 6771352        switch (reader.TokenType)
 353        {
 354            case JsonTokenType.StartArray:
 355                {
 1482356                    var list = new List<object>();
 1482357                    while (reader.Read())
 358                    {
 1482359                        switch (reader.TokenType)
 360                        {
 361                            default:
 0362                                list.Add(Read(ref reader, typeof(object), options));
 0363                                break;
 364
 365                            case JsonTokenType.EndArray:
 1482366                                return list;
 367                        }
 368                    }
 369
 0370                    throw new JsonException();
 371                }
 372            case JsonTokenType.StartObject:
 5289373                var dict = new ExpandoObject() as IDictionary<string, object>;
 5289374                var referenceResolver = (CustomPreserveReferenceResolver)(options.ReferenceHandler as CrossScopedReferen
 15523375                while (reader.Read())
 376                {
 15523377                    switch (reader.TokenType)
 378                    {
 379                        case JsonTokenType.EndObject:
 380                            // If the object contains a single entry with a key of $ref, return the referenced object.
 5289381                            if (dict.Count == 1 && dict.TryGetValue(RefPropertyName, out var referencedObject))
 0382                                return referencedObject;
 5289383                            return dict;
 384
 385                        case JsonTokenType.PropertyName:
 10234386                            var key = reader.GetString()!;
 10234387                            reader.Read();
 10234388                            if (referenceResolver != null && key == RefPropertyName)
 389                            {
 0390                                var referenceId = reader.GetString();
 0391                                var reference = referenceResolver.ResolveReference(referenceId!);
 0392                                dict.Add(key, reference);
 393                            }
 10234394                            else if (referenceResolver != null && key == IdPropertyName)
 395                            {
 0396                                var referenceId = reader.GetString()!;
 397
 398                                // Attempt to add the reference; if not found, we can ignore it and assume that the user
 0399                                referenceResolver.TryAddReference(referenceId, dict);
 400                            }
 401                            else
 402                            {
 10234403                                var value = Read(ref reader, typeof(object), options);
 10234404                                var unescapedKey = UnescapeKey(key);
 10234405                                dict.Add(unescapedKey, value);
 406                            }
 407
 10234408                            break;
 409
 410                        default:
 0411                            throw new JsonException();
 412                    }
 413                }
 414
 0415                throw new JsonException();
 416            default:
 0417                throw new JsonException($"Unknown token {reader.TokenType}");
 418        }
 419    }
 420
 421    private string EscapeKey(string key)
 422    {
 0423        return key.Replace("$", @"\\$");
 424    }
 425
 426    private string UnescapeKey(string key)
 427    {
 10234428        return key.Replace(@"\\$", "$");
 429    }
 430}