< 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>
 77114public 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    {
 106123        if (!JsonDocument.TryParseValue(ref reader, out var doc))
 024            throw new JsonException("Failed to parse JsonDocument");
 25
 106126        var id = doc.RootElement.TryGetProperty("id", out var idAttribute) ? idAttribute.GetString()! : identityGenerato
 106127        var nodeId = doc.RootElement.TryGetProperty("nodeId", out var nodeIdAttribute) ? nodeIdAttribute.GetString() : n
 106128        var name = doc.RootElement.TryGetProperty("name", out var nameElement) ? nameElement.GetString() : null;
 106129        var type = doc.RootElement.TryGetProperty("type", out var typeElement) ? typeElement.GetString() : null;
 106130        var version = doc.RootElement.TryGetProperty("version", out var versionElement) ? versionElement.GetInt32() : 1;
 106131        var runAsynchronously = doc.RootElement.TryGetProperty("runAsynchronously", out var runAsyncElement) && runAsync
 32
 106133        var connectionsElement = doc.RootElement.TryGetProperty("connections", out var connectionsEl) ? connectionsEl : 
 106134        var activitiesElement = doc.RootElement.TryGetProperty("activities", out var activitiesEl) ? activitiesEl : defa
 106135        var activities = activitiesElement.ValueKind != JsonValueKind.Undefined ? activitiesElement.Deserialize<ICollect
 259436        var activityDictionary = activities.ToDictionary(x => x.Id);
 106137        var connections = DeserializeConnections(connectionsElement, activityDictionary, options);
 106138        var notFoundConnections = GetNotFoundConnections(doc.RootElement, activityDictionary, connections, options);
 106139        var connectionsToRestore = FindConnectionsThatCanBeRestored(notFoundConnections, activities);
 106140        var connectionComparer = new ConnectionComparer();
 106141        var connectionsWithRestoredOnes = connections.Except(notFoundConnections, connectionComparer).Union(connectionsT
 42
 106143        var variablesElement = doc.RootElement.TryGetProperty("variables", out var variablesEl) ? variablesEl : default;
 106144        var variables = variablesElement.ValueKind != JsonValueKind.Undefined ? variablesElement.Deserialize<ICollection
 45
 106146        JsonSerializerOptions polymorphicOptions = options.Clone();
 106147        polymorphicOptions.Converters.Add(new PolymorphicDictionaryConverter(options, wellKnownTypeRegistry));
 48
 106149        var metadataElement = doc.RootElement.TryGetProperty("metadata", out var metadataEl) ? metadataEl : default;
 106150        var metadata = metadataElement.ValueKind != JsonValueKind.Undefined ? metadataElement.Deserialize<IDictionary<st
 51
 106152        var customPropertiesElement = doc.RootElement.TryGetProperty("customProperties", out var customPropertiesEl) ? c
 106153        var customProperties = customPropertiesEl.ValueKind != JsonValueKind.Undefined ? customPropertiesElement.Deseria
 106154        customProperties[AllActivitiesKey] = activities.ToList();
 106155        customProperties[AllConnectionsKey] = connectionsWithRestoredOnes;
 106156        customProperties[NotFoundConnectionsKey] = notFoundConnections.Except(connectionsToRestore, connectionComparer).
 57
 106158        var flowChart = new Activities.Flowchart
 106159        {
 106160            Id = id,
 106161            NodeId = nodeId!,
 106162            Name = name,
 106163            Type = type!,
 106164            RunAsynchronously = runAsynchronously,
 106165            Version = version,
 106166            CustomProperties = customProperties,
 106167            Metadata = metadata,
 106168            Activities = activities,
 106169            Variables = variables,
 106170            Connections = connectionsWithRestoredOnes,
 106171        };
 72
 106173        return flowChart;
 74    }
 75
 76    /// <inheritdoc />
 77    public override void Write(Utf8JsonWriter writer, Activities.Flowchart value, JsonSerializerOptions options)
 78    {
 19579        var activities = value.Activities;
 56180        var activityDictionary = activities.ToDictionary(x => x.Id);
 81
 19582        var customProperties = new Dictionary<string, object>(value.CustomProperties);
 19583        var allActivities = customProperties.GetValueOrDefault(AllActivitiesKey, activities);
 19584        var allConnections = (ICollection<Connection>)(customProperties.TryGetValue(AllConnectionsKey, out var c) ? c : 
 85
 19586        customProperties.Remove(AllActivitiesKey);
 19587        customProperties.Remove(AllConnectionsKey);
 88
 19589        var model = new
 19590        {
 19591            value.Id,
 19592            value.NodeId,
 19593            value.Name,
 19594            value.Type,
 19595            value.Version,
 19596            CustomProperties = customProperties,
 19597            value.Metadata,
 19598            Activities = allActivities,
 19599            value.Variables,
 195100            Connections = allConnections,
 195101        };
 102
 195103        var flowchartSerializerOptions = new JsonSerializerOptions(options);
 195104        flowchartSerializerOptions.Converters.Add(new ConnectionJsonConverter(activityDictionary));
 195105        flowchartSerializerOptions.Converters.Add(new PolymorphicDictionaryConverter(options, wellKnownTypeRegistry));
 106
 195107        JsonSerializer.Serialize(writer, model, flowchartSerializerOptions);
 195108    }
 109
 110    private static ICollection<Connection> GetNotFoundConnections(JsonElement rootElement, IDictionary<string, IActivity
 111    {
 1061112        var customPropertiesElement = rootElement.TryGetProperty("customProperties", out var customPropertiesEl) ? custo
 113
 1061114        var notFoundConnectionsElement =
 1061115            customPropertiesElement.ValueKind != JsonValueKind.Undefined
 1061116                ? customPropertiesElement.TryGetProperty(NotFoundConnectionsKey, out var notFoundConnectionsEl)
 1061117                    ? notFoundConnectionsEl
 1061118                    : default
 1061119                : default;
 1061120        var notFoundConnections = notFoundConnectionsElement.ValueKind != JsonValueKind.Undefined ? DeserializeConnectio
 121
 122        // Add connections of NotFoundActivity to the list if they aren't already in it.
 2594123        var notFoundActivities = activities.Values.Where(x => x is NotFoundActivity).Cast<NotFoundActivity>().ToList();
 1533124        var notFoundActivityConnections = connections.Where(x => notFoundActivities.Contains(x.Source.Activity)).ToList(
 125
 2124126        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
 1061132        return notFoundConnections;
 133    }
 134
 135    private static List<Connection> FindConnectionsThatCanBeRestored(IEnumerable<Connection> notFoundConnections, IEnume
 136    {
 1061137        var connectionsThatCanBeRestored = new List<Connection>();
 2594138        var foundActivities = activities.Where(x => x is not NotFoundActivity).ToList();
 139
 2124140        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
 1061158        return connectionsThatCanBeRestored;
 159    }
 160
 161    private static ICollection<Connection> DeserializeConnections(JsonElement connectionsElement, IDictionary<string, IA
 162    {
 163        // 1) Nothing → empty
 2068164        if (connectionsElement.ValueKind == JsonValueKind.Undefined || connectionsElement.ValueKind == JsonValueKind.Nul
 0165            return new List<Connection>();
 166
 167        // 2) OData‐style wrapper: { "$values": [ … ] }
 2068168        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
 2068173        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
 2068180        if (connectionsElement.ValueKind != JsonValueKind.Array)
 0181            return new List<Connection>();
 182
 183        // Shortcut: detect the classic flat‐connection JSON and parse manually
 2068184        var arr = connectionsElement.EnumerateArray().ToArray();
 2068185        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
 2068205        var serializer = new JsonSerializerOptions(options);
 206
 207        // Legacy check: look for "sourcePort" on the first item to choose the old converter
 2068208        if (arr.Length > 0 && arr[0].TryGetProperty("sourcePort", out _))
 0209            serializer.Converters.Add(new ObsoleteConnectionJsonConverter(activityDictionary));
 210        else
 2068211            serializer.Converters.Add(new ConnectionJsonConverter(activityDictionary));
 212
 2068213        var raw = connectionsElement.Deserialize<ICollection<Connection>>(serializer) ?? new List<Connection>();
 214
 215        // drop any half‑baked entries
 2540216        return raw.Where(c => c.Source?.Activity != null && c.Target?.Activity != null).ToList();
 217    }
 218}