< 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: 221
Line coverage: 84.2%
Branch coverage
58%
Covered branches: 65
Total branches: 112
Branch coverage: 58%
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.65%4864640.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;
 8using JetBrains.Annotations;
 9using Microsoft.Extensions.Logging;
 10
 11namespace Elsa.Workflows.Activities.Flowchart.Serialization;
 12
 13/// <summary>
 14/// A JSON converter for <see cref="Activities.Flowchart"/>.
 15/// </summary>
 16[UsedImplicitly]
 191317public class FlowchartJsonConverter(IIdentityGenerator identityGenerator, IWellKnownTypeRegistry wellKnownTypeRegistry, 
 18{
 19    private const string AllActivitiesKey = "allActivities";
 20    private const string AllConnectionsKey = "allConnections";
 21    private const string NotFoundConnectionsKey = "notFoundConnections";
 22
 23    /// <inheritdoc />
 24    public override Activities.Flowchart Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions optio
 25    {
 140026        if (!JsonDocument.TryParseValue(ref reader, out var doc))
 027            throw new JsonException("Failed to parse JsonDocument");
 28
 140029        var id = doc.RootElement.TryGetProperty("id", out var idAttribute) ? idAttribute.GetString()! : identityGenerato
 140030        var nodeId = doc.RootElement.TryGetProperty("nodeId", out var nodeIdAttribute) ? nodeIdAttribute.GetString() : n
 140031        var name = doc.RootElement.TryGetProperty("name", out var nameElement) ? nameElement.GetString() : null;
 140032        var type = doc.RootElement.TryGetProperty("type", out var typeElement) ? typeElement.GetString() : null;
 140033        var version = doc.RootElement.TryGetProperty("version", out var versionElement) ? versionElement.GetInt32() : 1;
 140034        var runAsynchronously = doc.RootElement.TryGetProperty("runAsynchronously", out var runAsyncElement) && runAsync
 35
 140036        var connectionsElement = doc.RootElement.TryGetProperty("connections", out var connectionsEl) ? connectionsEl : 
 140037        var activitiesElement = doc.RootElement.TryGetProperty("activities", out var activitiesEl) ? activitiesEl : defa
 140038        var activities = activitiesElement.ValueKind != JsonValueKind.Undefined ? activitiesElement.Deserialize<ICollect
 350339        var activityDictionary = activities.ToDictionary(x => x.Id);
 140040        var connections = DeserializeConnections(connectionsElement, activityDictionary, options);
 140041        var notFoundConnections = GetNotFoundConnections(doc.RootElement, activityDictionary, connections, options);
 140042        var connectionsToRestore = FindConnectionsThatCanBeRestored(notFoundConnections, activities);
 140043        var connectionComparer = new ConnectionComparer();
 140044        var connectionsWithRestoredOnes = connections.Except(notFoundConnections, connectionComparer).Union(connectionsT
 45
 140046        var variablesElement = doc.RootElement.TryGetProperty("variables", out var variablesEl) ? variablesEl : default;
 140047        var variables = variablesElement.ValueKind != JsonValueKind.Undefined ? variablesElement.Deserialize<ICollection
 48
 140049        var polymorphicOptions = options.Clone();
 140050        polymorphicOptions.Converters.Add(new PolymorphicDictionaryConverter(options, wellKnownTypeRegistry));
 51
 140052        var metadataElement = doc.RootElement.TryGetProperty("metadata", out var metadataEl) ? metadataEl : default;
 140053        var metadata = metadataElement.ValueKind != JsonValueKind.Undefined ? metadataElement.Deserialize<IDictionary<st
 54
 140055        var customPropertiesElement = doc.RootElement.TryGetProperty("customProperties", out var customPropertiesEl) ? c
 140056        var customProperties = customPropertiesEl.ValueKind != JsonValueKind.Undefined ? customPropertiesElement.Deseria
 140057        customProperties[AllActivitiesKey] = activities.ToList();
 140058        customProperties[AllConnectionsKey] = connectionsWithRestoredOnes;
 140059        customProperties[NotFoundConnectionsKey] = notFoundConnections.Except(connectionsToRestore, connectionComparer).
 60
 140061        var flowChart = new Activities.Flowchart
 140062        {
 140063            Id = id,
 140064            NodeId = nodeId!,
 140065            Name = name,
 140066            Type = type!,
 140067            RunAsynchronously = runAsynchronously,
 140068            Version = version,
 140069            CustomProperties = customProperties,
 140070            Metadata = metadata,
 140071            Activities = activities,
 140072            Variables = variables,
 140073            Connections = connectionsWithRestoredOnes,
 140074        };
 75
 140076        return flowChart;
 77    }
 78
 79    /// <inheritdoc />
 80    public override void Write(Utf8JsonWriter writer, Activities.Flowchart value, JsonSerializerOptions options)
 81    {
 47982        var activities = value.Activities;
 134983        var activityDictionary = activities.ToDictionary(x => x.Id);
 84
 47985        var customProperties = new Dictionary<string, object>(value.CustomProperties);
 47986        var allActivities = customProperties.GetValueOrDefault(AllActivitiesKey, activities);
 47987        var allConnections = (ICollection<Connection>)(customProperties.TryGetValue(AllConnectionsKey, out var c) ? c : 
 88
 47989        customProperties.Remove(AllActivitiesKey);
 47990        customProperties.Remove(AllConnectionsKey);
 91
 47992        var model = new
 47993        {
 47994            value.Id,
 47995            value.NodeId,
 47996            value.Name,
 47997            value.Type,
 47998            value.Version,
 47999            CustomProperties = customProperties,
 479100            value.Metadata,
 479101            Activities = allActivities,
 479102            value.Variables,
 479103            Connections = allConnections,
 479104        };
 105
 479106        var flowchartSerializerOptions = new JsonSerializerOptions(options);
 479107        flowchartSerializerOptions.Converters.Add(new ConnectionJsonConverter(activityDictionary, loggerFactory));
 479108        flowchartSerializerOptions.Converters.Add(new PolymorphicDictionaryConverter(options, wellKnownTypeRegistry));
 109
 479110        JsonSerializer.Serialize(writer, model, flowchartSerializerOptions);
 479111    }
 112
 113    private ICollection<Connection> GetNotFoundConnections(JsonElement rootElement, IDictionary<string, IActivity> activ
 114    {
 1400115        var customPropertiesElement = rootElement.TryGetProperty("customProperties", out var customPropertiesEl) ? custo
 116
 1400117        var notFoundConnectionsElement =
 1400118            customPropertiesElement.ValueKind != JsonValueKind.Undefined
 1400119                ? customPropertiesElement.TryGetProperty(NotFoundConnectionsKey, out var notFoundConnectionsEl)
 1400120                    ? notFoundConnectionsEl
 1400121                    : default
 1400122                : default;
 1400123        var notFoundConnections = notFoundConnectionsElement.ValueKind != JsonValueKind.Undefined ? DeserializeConnectio
 124
 125        // Add connections of NotFoundActivity to the list if they aren't already in it.
 3503126        var notFoundActivities = activities.Values.Where(x => x is NotFoundActivity).Cast<NotFoundActivity>().ToList();
 2103127        var notFoundActivityConnections = connections.Where(x => notFoundActivities.Contains(x.Source.Activity)).ToList(
 128
 2802129        foreach (var notFoundConnection in notFoundActivityConnections)
 130        {
 1131            if (notFoundConnections.All(x => x.Source != notFoundConnection.Source || x.Target != notFoundConnection.Tar
 1132                notFoundConnections.Add(notFoundConnection);
 133        }
 134
 1400135        return notFoundConnections;
 136    }
 137
 138    private List<Connection> FindConnectionsThatCanBeRestored(IEnumerable<Connection> notFoundConnections, IEnumerable<I
 139    {
 1400140        var connectionsThatCanBeRestored = new List<Connection>();
 3503141        var foundActivities = activities.Where(x => x is not NotFoundActivity).ToList();
 142
 2802143        foreach (var notFoundConnection in notFoundConnections.ToList())
 144        {
 1145            var missingSource = notFoundConnection.Source;
 1146            var missingTarget = notFoundConnection.Target;
 147
 148            // ReSharper disable ConditionalAccessQualifierIsNonNullableAccordingToAPIContract
 149            // Activity might be null in case of JSON missing information.
 1150            var source = foundActivities.FirstOrDefault(x => x.Id == missingSource.Activity?.Id);
 1151            var target = foundActivities.FirstOrDefault(x => x.Id == missingTarget.Activity?.Id);
 152            // ReSharper restore ConditionalAccessQualifierIsNonNullableAccordingToAPIContract
 153
 1154            if (source == null || target == null)
 155                continue;
 156
 0157            var connection = new Connection(new Endpoint(source, missingSource.Port), new Endpoint(target, missingTarget
 0158            connectionsThatCanBeRestored.Add(connection);
 159        }
 160
 1400161        return connectionsThatCanBeRestored;
 162    }
 163
 164    private ICollection<Connection> DeserializeConnections(JsonElement connectionsElement, IDictionary<string, IActivity
 165    {
 166        // 1) Nothing → empty
 2692167        if (connectionsElement.ValueKind == JsonValueKind.Undefined || connectionsElement.ValueKind == JsonValueKind.Nul
 0168            return new List<Connection>();
 169
 170        // 2) OData‐style wrapper: { "$values": [ … ] }
 2692171        if (connectionsElement.ValueKind == JsonValueKind.Object && connectionsElement.TryGetProperty("$values", out var
 172        {
 0173            connectionsElement = valuesEl;
 174        }
 175        // 3) Single‐object (old style): wrap into a 1‑element array if it has a "source" property
 2692176        else if (connectionsElement.ValueKind == JsonValueKind.Object && connectionsElement.TryGetProperty("source", out
 177        {
 0178            using var tmp = JsonDocument.Parse($"[{connectionsElement.GetRawText()}]");
 0179            connectionsElement = tmp.RootElement;
 180        }
 181
 182        // 4) If it’s still not an array, bail
 2692183        if (connectionsElement.ValueKind != JsonValueKind.Array)
 0184            return new List<Connection>();
 185
 186        // Shortcut: detect the classic flat‐connection JSON and parse manually
 2692187        var arr = connectionsElement.EnumerateArray().ToArray();
 2692188        if (arr.Length > 0 && arr[0].TryGetProperty("source", out var srcProp) && srcProp.ValueKind == JsonValueKind.Str
 189        {
 0190            var list = new List<Connection>();
 191
 0192            foreach (var el in arr)
 193            {
 0194                var srcId = el.GetProperty("source").GetString()!;
 0195                var tgtId = el.GetProperty("target").GetString()!;
 0196                var srcPort = el.TryGetProperty("sourcePort", out var sp) && sp.ValueKind == JsonValueKind.String ? sp.G
 0197                var tgtPort = el.TryGetProperty("targetPort", out var tp) && tp.ValueKind == JsonValueKind.String ? tp.G
 198
 0199                var srcAct = activityDictionary[srcId];
 0200                var tgtAct = activityDictionary[tgtId];
 0201                list.Add(new(new Endpoint(srcAct, srcPort), new Endpoint(tgtAct, tgtPort)));
 202            }
 203
 0204            return list;
 205        }
 206
 207        // Otherwise, it's an array of nested‐object connections → delegate to your converters
 2692208        var serializer = new JsonSerializerOptions(options);
 209
 210        // Legacy check: look for "sourcePort" on the first item to choose the old converter
 2692211        if (arr.Length > 0 && arr[0].TryGetProperty("sourcePort", out _))
 0212            serializer.Converters.Add(new ObsoleteConnectionJsonConverter(activityDictionary));
 213        else
 2692214            serializer.Converters.Add(new ConnectionJsonConverter(activityDictionary, loggerFactory));
 215
 2692216        var raw = connectionsElement.Deserialize<ICollection<Connection?>>(serializer) ?? new List<Connection?>();
 217
 218        // drop any half‑baked entries
 3395219        return raw.Where(c => c != null && c.Source.Activity != null! && c.Target.Activity != null!).Cast<Connection>().
 220    }
 221}