< Summary

Information
Class: Elsa.Workflows.Activities.Flowchart.Serialization.FlowchartJsonConverter
Assembly: Elsa.Workflows.Core
File(s): /home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Serialization/FlowchartJsonConverter.cs
Line coverage
84%
Covered lines: 102
Uncovered lines: 19
Coverable lines: 121
Total lines: 218
Line coverage: 84.2%
Branch coverage
57%
Covered branches: 66
Total branches: 114
Branch coverage: 57.8%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
Read(...)67.5%404097.67%
Write(...)100%22100%
GetNotFoundConnections(...)78.57%1414100%
FindConnectionsThatCanBeRestored(...)40%111081.81%
DeserializeConnections(...)45.83%5274840.74%

File(s)

/home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Serialization/FlowchartJsonConverter.cs

#LineLine coverage
 1using System.Text.Json;
 2using System.Text.Json.Serialization;
 3using Elsa.Expressions.Contracts;
 4using Elsa.Extensions;
 5using Elsa.Workflows.Activities.Flowchart.Models;
 6using Elsa.Workflows.Memory;
 7using Elsa.Workflows.Serialization.Converters;
 8
 9namespace Elsa.Workflows.Activities.Flowchart.Serialization;
 10
 11/// <summary>
 12/// A JSON converter for <see cref="Activities.Flowchart"/>.
 13/// </summary>
 83514public class FlowchartJsonConverter(IIdentityGenerator identityGenerator, IWellKnownTypeRegistry wellKnownTypeRegistry) 
 15{
 16    private const string AllActivitiesKey = "allActivities";
 17    private const string AllConnectionsKey = "allConnections";
 18    private const string NotFoundConnectionsKey = "notFoundConnections";
 19
 20    /// <inheritdoc />
 21    public override Activities.Flowchart Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions optio
 22    {
 103723        if (!JsonDocument.TryParseValue(ref reader, out var doc))
 024            throw new JsonException("Failed to parse JsonDocument");
 25
 103726        var id = doc.RootElement.TryGetProperty("id", out var idAttribute) ? idAttribute.GetString()! : identityGenerato
 103727        var nodeId = doc.RootElement.TryGetProperty("nodeId", out var nodeIdAttribute) ? nodeIdAttribute.GetString() : n
 103728        var name = doc.RootElement.TryGetProperty("name", out var nameElement) ? nameElement.GetString() : null;
 103729        var type = doc.RootElement.TryGetProperty("type", out var typeElement) ? typeElement.GetString() : null;
 103730        var version = doc.RootElement.TryGetProperty("version", out var versionElement) ? versionElement.GetInt32() : 1;
 103731        var runAsynchronously = doc.RootElement.TryGetProperty("runAsynchronously", out var runAsyncElement) && runAsync
 32
 103733        var connectionsElement = doc.RootElement.TryGetProperty("connections", out var connectionsEl) ? connectionsEl : 
 103734        var activitiesElement = doc.RootElement.TryGetProperty("activities", out var activitiesEl) ? activitiesEl : defa
 103735        var activities = activitiesElement.ValueKind != JsonValueKind.Undefined ? activitiesElement.Deserialize<ICollect
 253836        var activityDictionary = activities.ToDictionary(x => x.Id);
 103737        var connections = DeserializeConnections(connectionsElement, activityDictionary, options);
 103738        var notFoundConnections = GetNotFoundConnections(doc.RootElement, activityDictionary, connections, options);
 103739        var connectionsToRestore = FindConnectionsThatCanBeRestored(notFoundConnections, activities);
 103740        var connectionComparer = new ConnectionComparer();
 103741        var connectionsWithRestoredOnes = connections.Except(notFoundConnections, connectionComparer).Union(connectionsT
 42
 103743        var variablesElement = doc.RootElement.TryGetProperty("variables", out var variablesEl) ? variablesEl : default;
 103744        var variables = variablesElement.ValueKind != JsonValueKind.Undefined ? variablesElement.Deserialize<ICollection
 45
 103746        JsonSerializerOptions polymorphicOptions = options.Clone();
 103747        polymorphicOptions.Converters.Add(new PolymorphicDictionaryConverter(options, wellKnownTypeRegistry));
 48
 103749        var metadataElement = doc.RootElement.TryGetProperty("metadata", out var metadataEl) ? metadataEl : default;
 103750        var metadata = metadataElement.ValueKind != JsonValueKind.Undefined ? metadataElement.Deserialize<IDictionary<st
 51
 103752        var customPropertiesElement = doc.RootElement.TryGetProperty("customProperties", out var customPropertiesEl) ? c
 103753        var customProperties = customPropertiesEl.ValueKind != JsonValueKind.Undefined ? customPropertiesElement.Deseria
 103754        customProperties[AllActivitiesKey] = activities.ToList();
 103755        customProperties[AllConnectionsKey] = connectionsWithRestoredOnes;
 103756        customProperties[NotFoundConnectionsKey] = notFoundConnections.Except(connectionsToRestore, connectionComparer).
 57
 103758        var flowChart = new Activities.Flowchart
 103759        {
 103760            Id = id,
 103761            NodeId = nodeId!,
 103762            Name = name,
 103763            Type = type!,
 103764            RunAsynchronously = runAsynchronously,
 103765            Version = version,
 103766            CustomProperties = customProperties,
 103767            Metadata = metadata,
 103768            Activities = activities,
 103769            Variables = variables,
 103770            Connections = connectionsWithRestoredOnes,
 103771        };
 72
 103773        return flowChart;
 74    }
 75
 76    /// <inheritdoc />
 77    public override void Write(Utf8JsonWriter writer, Activities.Flowchart value, JsonSerializerOptions options)
 78    {
 20379        var activities = value.Activities;
 59380        var activityDictionary = activities.ToDictionary(x => x.Id);
 81
 20382        var customProperties = new Dictionary<string, object>(value.CustomProperties);
 20383        var allActivities = customProperties.GetValueOrDefault(AllActivitiesKey, activities);
 20384        var allConnections = (ICollection<Connection>)(customProperties.TryGetValue(AllConnectionsKey, out var c) ? c : 
 85
 20386        customProperties.Remove(AllActivitiesKey);
 20387        customProperties.Remove(AllConnectionsKey);
 88
 20389        var model = new
 20390        {
 20391            value.Id,
 20392            value.NodeId,
 20393            value.Name,
 20394            value.Type,
 20395            value.Version,
 20396            CustomProperties = customProperties,
 20397            value.Metadata,
 20398            Activities = allActivities,
 20399            value.Variables,
 203100            Connections = allConnections,
 203101        };
 102
 203103        var flowchartSerializerOptions = new JsonSerializerOptions(options);
 203104        flowchartSerializerOptions.Converters.Add(new ConnectionJsonConverter(activityDictionary));
 203105        flowchartSerializerOptions.Converters.Add(new PolymorphicDictionaryConverter(options, wellKnownTypeRegistry));
 106
 203107        JsonSerializer.Serialize(writer, model, flowchartSerializerOptions);
 203108    }
 109
 110    private static ICollection<Connection> GetNotFoundConnections(JsonElement rootElement, IDictionary<string, IActivity
 111    {
 1037112        var customPropertiesElement = rootElement.TryGetProperty("customProperties", out var customPropertiesEl) ? custo
 113
 1037114        var notFoundConnectionsElement =
 1037115            customPropertiesElement.ValueKind != JsonValueKind.Undefined
 1037116                ? customPropertiesElement.TryGetProperty(NotFoundConnectionsKey, out var notFoundConnectionsEl)
 1037117                    ? notFoundConnectionsEl
 1037118                    : default
 1037119                : default;
 1037120        var notFoundConnections = notFoundConnectionsElement.ValueKind != JsonValueKind.Undefined ? DeserializeConnectio
 121
 122        // Add connections of NotFoundActivity to the list if they aren't already in it.
 2538123        var notFoundActivities = activities.Values.Where(x => x is NotFoundActivity).Cast<NotFoundActivity>().ToList();
 1501124        var notFoundActivityConnections = connections.Where(x => notFoundActivities.Contains(x.Source.Activity)).ToList(
 125
 2076126        foreach (var notFoundConnection in notFoundActivityConnections)
 127        {
 1128            if (notFoundConnections.All(x => x.Source != notFoundConnection.Source || x.Target != notFoundConnection.Tar
 1129                notFoundConnections.Add(notFoundConnection);
 130        }
 131
 1037132        return notFoundConnections;
 133    }
 134
 135    private static List<Connection> FindConnectionsThatCanBeRestored(IEnumerable<Connection> notFoundConnections, IEnume
 136    {
 1037137        var connectionsThatCanBeRestored = new List<Connection>();
 2538138        var foundActivities = activities.Where(x => x is not NotFoundActivity).ToList();
 139
 2076140        foreach (var notFoundConnection in notFoundConnections.ToList())
 141        {
 1142            var missingSource = notFoundConnection.Source;
 1143            var missingTarget = notFoundConnection.Target;
 144
 145            // ReSharper disable ConditionalAccessQualifierIsNonNullableAccordingToAPIContract
 146            // Activity might be null in case of JSON missing information.
 1147            var source = foundActivities.FirstOrDefault(x => x.Id == missingSource.Activity?.Id);
 1148            var target = foundActivities.FirstOrDefault(x => x.Id == missingTarget.Activity?.Id);
 149            // ReSharper restore ConditionalAccessQualifierIsNonNullableAccordingToAPIContract
 150
 1151            if (source == null || target == null)
 152                continue;
 153
 0154            var connection = new Connection(new Endpoint(source, missingSource.Port), new Endpoint(target, missingTarget
 0155            connectionsThatCanBeRestored.Add(connection);
 156        }
 157
 1037158        return connectionsThatCanBeRestored;
 159    }
 160
 161    private static ICollection<Connection> DeserializeConnections(JsonElement connectionsElement, IDictionary<string, IA
 162    {
 163        // 1) Nothing → empty
 2020164        if (connectionsElement.ValueKind == JsonValueKind.Undefined || connectionsElement.ValueKind == JsonValueKind.Nul
 0165            return new List<Connection>();
 166
 167        // 2) OData‐style wrapper: { "$values": [ … ] }
 2020168        if (connectionsElement.ValueKind == JsonValueKind.Object && connectionsElement.TryGetProperty("$values", out var
 169        {
 0170            connectionsElement = valuesEl;
 171        }
 172        // 3) Single‐object (old style): wrap into a 1‑element array if it has a "source" property
 2020173        else if (connectionsElement.ValueKind == JsonValueKind.Object && connectionsElement.TryGetProperty("source", out
 174        {
 0175            using var tmp = JsonDocument.Parse($"[{connectionsElement.GetRawText()}]");
 0176            connectionsElement = tmp.RootElement;
 177        }
 178
 179        // 4) If it’s still not an array, bail
 2020180        if (connectionsElement.ValueKind != JsonValueKind.Array)
 0181            return new List<Connection>();
 182
 183        // Shortcut: detect the classic flat‐connection JSON and parse manually
 2020184        var arr = connectionsElement.EnumerateArray().ToArray();
 2020185        if (arr.Length > 0 && arr[0].TryGetProperty("source", out var srcProp) && srcProp.ValueKind == JsonValueKind.Str
 186        {
 0187            var list = new List<Connection>();
 188
 0189            foreach (var el in arr)
 190            {
 0191                var srcId = el.GetProperty("source").GetString()!;
 0192                var tgtId = el.GetProperty("target").GetString()!;
 0193                var srcPort = el.TryGetProperty("sourcePort", out var sp) && sp.ValueKind == JsonValueKind.String ? sp.G
 0194                var tgtPort = el.TryGetProperty("targetPort", out var tp) && tp.ValueKind == JsonValueKind.String ? tp.G
 195
 0196                var srcAct = activityDictionary[srcId];
 0197                var tgtAct = activityDictionary[tgtId];
 0198                list.Add(new Connection(new Endpoint(srcAct, srcPort), new Endpoint(tgtAct, tgtPort)));
 199            }
 200
 0201            return list;
 202        }
 203
 204        // Otherwise, it's an array of nested‐object connections → delegate to your converters
 2020205        var serializer = new JsonSerializerOptions(options);
 206
 207        // Legacy check: look for "sourcePort" on the first item to choose the old converter
 2020208        if (arr.Length > 0 && arr[0].TryGetProperty("sourcePort", out _))
 0209            serializer.Converters.Add(new ObsoleteConnectionJsonConverter(activityDictionary));
 210        else
 2020211            serializer.Converters.Add(new ConnectionJsonConverter(activityDictionary));
 212
 2020213        var raw = connectionsElement.Deserialize<ICollection<Connection>>(serializer) ?? new List<Connection>();
 214
 215        // drop any half‑baked entries
 2484216        return raw.Where(c => c.Source?.Activity != null && c.Target?.Activity != null).ToList();
 217    }
 218}