Skip to content

Commit

Permalink
Flowchart variables are not serialized by FlowchartJsonConverter (#5533)
Browse files Browse the repository at this point in the history
* Flowchart variables are not serialized by FlowchartJsonConverter

* Added unit tests to ensure that serialized/deserialized container is equivalent to original, and fixed issues found

---------

Co-authored-by: Bob Hauser <rhauser@kinaxis.com>
Co-authored-by: Sipke Schoorstra <sipkeschoorstra@outlook.com>
  • Loading branch information
3 people committed Jun 15, 2024
1 parent 74fc0c9 commit ee733e3
Show file tree
Hide file tree
Showing 4 changed files with 254 additions and 27 deletions.
3 changes: 3 additions & 0 deletions src/modules/Elsa.Workflows.Core/Abstractions/Activity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Elsa.Workflows.Contracts;
using Elsa.Workflows.Helpers;
using Elsa.Workflows.Models;
using Elsa.Workflows.Serialization.Converters;
using JetBrains.Annotations;

namespace Elsa.Workflows;
Expand Down Expand Up @@ -75,13 +76,15 @@ public bool RunAsynchronously
}

/// <inheritdoc />
[JsonConverter(typeof(PolymorphicObjectConverterFactory))]
public IDictionary<string, object> CustomProperties { get; set; } = new Dictionary<string, object>();

/// <inheritdoc />
[JsonIgnore]
public IDictionary<string, object> SyntheticProperties { get; set; } = new Dictionary<string, object>();

/// <inheritdoc />
[JsonConverter(typeof(PolymorphicObjectConverterFactory))]
public IDictionary<string, object> Metadata { get; set; } = new Dictionary<string, object>();

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Elsa.Extensions;
using Elsa.Workflows.Activities.Flowchart.Models;
using Elsa.Workflows.Contracts;
using Elsa.Workflows.Contracts;
using Elsa.Workflows.Memory;
using Elsa.Workflows.Serialization.Converters;

namespace Elsa.Workflows.Activities.Flowchart.Serialization;

Expand All @@ -27,35 +30,49 @@ public override Activities.Flowchart Read(ref Utf8JsonReader reader, Type typeTo
if (!JsonDocument.TryParseValue(ref reader, out var doc))
throw new JsonException("Failed to parse JsonDocument");

var connectionsElement = doc.RootElement.TryGetProperty("connections", out var connectionsEl) ? connectionsEl : default;
var activitiesElement = doc.RootElement.TryGetProperty("activities", out var activitiesEl) ? activitiesEl : default;
var id = doc.RootElement.TryGetProperty("id", out var idAttribute) ? idAttribute.GetString()! : _identityGenerator.GenerateId();
var nodeId = doc.RootElement.TryGetProperty("nodeId", out var nodeIdAttribute) ? nodeIdAttribute.GetString() : default;
var name = doc.RootElement.TryGetProperty("name", out var nameElement) ? nameElement.GetString() : default;
var type = doc.RootElement.TryGetProperty("type", out var typeElement) ? typeElement.GetString() : default;
var version = doc.RootElement.TryGetProperty("version", out var versionElement) ? versionElement.GetInt32() : 1;

var connectionsElement = doc.RootElement.TryGetProperty("connections", out var connectionsEl) ? connectionsEl : default;
var activitiesElement = doc.RootElement.TryGetProperty("activities", out var activitiesEl) ? activitiesEl : default;
var activities = activitiesElement.ValueKind != JsonValueKind.Undefined ? activitiesElement.Deserialize<ICollection<IActivity>>(options) ?? new List<IActivity>() : new List<IActivity>();
var metadataElement = doc.RootElement.TryGetProperty("metadata", out var metadataEl) ? metadataEl : default;
var metadata = metadataElement.ValueKind != JsonValueKind.Undefined ? metadataElement.Deserialize<IDictionary<string, object>>(options) ?? new Dictionary<string, object>() : new Dictionary<string, object>();
var activityDictionary = activities.ToDictionary(x => x.Id);
var connections = DeserializeConnections(connectionsElement, activityDictionary, options);
var notFoundConnections = GetNotFoundConnections(doc.RootElement, activityDictionary, connections, options);
var connectionsToRestore = FindConnectionsThatCanBeRestored(notFoundConnections, activities);
var connectionComparer = new ConnectionComparer();
var connectionsWithRestoredOnes = connections.Except(notFoundConnections, connectionComparer).Union(connectionsToRestore, connectionComparer).ToList();

var variablesElement = doc.RootElement.TryGetProperty("variables", out var variablesEl) ? variablesEl : default;
var variables = variablesElement.ValueKind != JsonValueKind.Undefined ? variablesElement.Deserialize<ICollection<Variable>>(options) ?? new List<Variable>() : new List<Variable>();

JsonSerializerOptions polymorphicOptions = options.Clone();
polymorphicOptions.Converters.Add(new PolymorphicDictionaryConverter(options));

var metadataElement = doc.RootElement.TryGetProperty("metadata", out var metadataEl) ? metadataEl : default;
var metadata = metadataElement.ValueKind != JsonValueKind.Undefined ? metadataElement.Deserialize<IDictionary<string, object>>(polymorphicOptions) ?? new Dictionary<string, object>() : new Dictionary<string, object>();

var customPropertiesElement = doc.RootElement.TryGetProperty("customProperties", out var customPropertiesEl) ? customPropertiesEl : default;
var customProperties = customPropertiesEl.ValueKind != JsonValueKind.Undefined ? customPropertiesElement.Deserialize<IDictionary<string, object>>(polymorphicOptions) ?? new Dictionary<string, object>() : new Dictionary<string, object>();
customProperties[AllActivitiesKey] = activities.ToList();
customProperties[AllConnectionsKey] = connectionsWithRestoredOnes;
customProperties[NotFoundConnectionsKey] = notFoundConnections.Except(connectionsToRestore, connectionComparer).ToList();

var flowChart = new Activities.Flowchart
{
{
Id = id,
NodeId = nodeId!,
Name = name,
Type = type!,
Version = version,
CustomProperties = customProperties,
Metadata = metadata,
Activities = activities,
Connections = connectionsWithRestoredOnes,
CustomProperties =
{
[AllActivitiesKey] = activities.ToList(),
[AllConnectionsKey] = connectionsWithRestoredOnes,
[NotFoundConnectionsKey] = notFoundConnections.Except(connectionsToRestore, connectionComparer).ToList()
}
Variables = variables,
Connections = connectionsWithRestoredOnes,
};

return flowChart;
Expand All @@ -65,11 +82,8 @@ public override Activities.Flowchart Read(ref Utf8JsonReader reader, Type typeTo
public override void Write(Utf8JsonWriter writer, Activities.Flowchart value, JsonSerializerOptions options)
{
var activities = value.Activities;
var connectionSerializerOptions = new JsonSerializerOptions(options);
var activityDictionary = activities.ToDictionary(x => x.Id);

connectionSerializerOptions.Converters.Add(new ConnectionJsonConverter(activityDictionary));

var customProperties = new Dictionary<string, object>(value.CustomProperties);
var allActivities = customProperties.GetValueOrDefault(AllActivitiesKey, activities);
var allConnections = (ICollection<Connection>)(customProperties.TryGetValue(AllConnectionsKey, out var c) ? c : value.Connections);
Expand All @@ -79,17 +93,23 @@ public override void Write(Utf8JsonWriter writer, Activities.Flowchart value, Js

var model = new
{
value.Type,
value.Version,
value.Id,
value.NodeId,
value.Metadata,
value.Name,
value.Type,
value.Version,
CustomProperties = customProperties,
value.Metadata,
Activities = allActivities,
Connections = allConnections
};

JsonSerializer.Serialize(writer, model, connectionSerializerOptions);
value.Variables,
Connections = allConnections,
};

var flowchartSerializerOptions = new JsonSerializerOptions(options);
flowchartSerializerOptions.Converters.Add(new ConnectionJsonConverter(activityDictionary));
flowchartSerializerOptions.Converters.Add(new PolymorphicDictionaryConverter(options));

JsonSerializer.Serialize(writer, model, flowchartSerializerOptions);
}

private static ICollection<Connection> GetNotFoundConnections(JsonElement rootElement, IDictionary<string, IActivity> activities, IEnumerable<Connection> connections, JsonSerializerOptions connectionSerializerOptions)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,17 @@ public class PolymorphicObjectConverterFactory : JsonConverterFactory
/// <inheritdoc />
public override bool CanConvert(Type typeToConvert)
{
var canConvert = typeToConvert.IsClass
if (typeToConvert.IsClass
&& typeToConvert == typeof(object)
|| typeToConvert == typeof(ExpandoObject)
|| typeToConvert == typeof(Dictionary<string, object>);

return canConvert;
|| typeToConvert == typeof(Dictionary<string, object>))
return true;

if (typeToConvert.IsInterface
&& typeToConvert == typeof(IDictionary<string, object>))
return true;

return false;
}

/// <inheritdoc />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
using Elsa.Expressions.Models;
using Elsa.Testing.Shared;
using Elsa.Workflows.Activities;
using Elsa.Workflows.Activities.Flowchart.Activities;
using Elsa.Workflows.Activities.Flowchart.Models;
using Elsa.Workflows.Contracts;
using Elsa.Workflows.Memory;
using Elsa.Workflows.Models;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
using Xunit.Abstractions;

namespace Elsa.Workflows.IntegrationTests.Serialization.ContainerSerialization;

public class Tests
{
private readonly IServiceProvider _services;
private readonly IActivitySerializer _activitySerializer;

public Tests(ITestOutputHelper testOutputHelper)
{
_services = new TestApplicationBuilder(testOutputHelper).Build();
_activitySerializer = _services.GetService<IActivitySerializer>()!;
}

[Fact]
public async void SerializeFlowchartContainerTest()
{
await _services.PopulateRegistriesAsync();

// Arrange

var start = new Start()
{
Id = "start",
Name = "Start",
};
var writeLine = new WriteLine(new Input<string>(new Expression("JavaScript", "getVariable('TextVar')")))
{
Id = "writeLine",
Name = "WriteLine",
Version = 3,
};
var end = new End()
{
Id = "end",
Name = "end",
};
var container = new Flowchart()
{
Id = "flowchart",
Name = "Flowchart",
Type = "Elsa.Flowchart",
Version = 42,
CustomProperties = new Dictionary<string, object>()
{
{ "purpose", "somePurpose" }
},
Metadata = new Dictionary<string, object>()
{
{ "int", 10 },
{ "bool", false },
{ "string", "str" },
},
Activities = new List<IActivity>() {
start,
writeLine,
end
},
Variables = new List<Variable>() {
new Variable<string>("TextVar", "This is the text to write")
},
Connections = new List<Connection>()
{
new Connection(start, writeLine),
new Connection(writeLine, end),
},
};

// Act

var serialized = _activitySerializer.Serialize(container);
var deserializedContainer = _activitySerializer.Deserialize(serialized) as Container;

// Assert

ValidateContainer(container, deserializedContainer);
}

[Fact]
public async void SerializeSequenceContainerTest()
{
await _services.PopulateRegistriesAsync();

// Arrange

var container = new Sequence()
{
Id = "sequence",
Name = "Sequence",
Type = "Elsa.Sequence",
Version = 42,
Variables = new List<Variable>() {
new Variable<string>("TextVar", "This is the text to write")
},
Activities = new List<IActivity>() {
new WriteLine(new Input<string>(new Expression("JavaScript", "getVariable('TextVar')")))
{
Id = "writeLine",
Name = "WriteLine",
CanStartWorkflow = true,
},
},
CustomProperties = new Dictionary<string, object>()
{
{ "purpose", "somePurpose" }
},
Metadata = new Dictionary<string, object>()
{
{ "int", 10 },
{ "bool", false },
{ "string", "str"},
}
};

// Act

var serialized = _activitySerializer.Serialize(container);
var deserializedContainer = _activitySerializer.Deserialize(serialized) as Container;

// Assert

ValidateContainer(container, deserializedContainer);
}

[Fact]
public async void SerializeParallelContainerTest()
{
await _services.PopulateRegistriesAsync();

// Arrange

var container = new Workflows.Activities.Parallel()
{
Id = "parallel",
Name = "Parallel",
Type = "Elsa.Parallel",
Version = 42,
Variables = new List<Variable>() {
new Variable<string>("TextVar", "This is the text to write")
},
Activities = new List<IActivity>() {
new WriteLine(new Input<string>(new Expression("JavaScript", "getVariable('TextVar')")))
{
Id = "writeLine",
Name = "WriteLine",
CanStartWorkflow = true,
},
},
CustomProperties = new Dictionary<string, object>()
{
{ "purpose", "somePurpose" }
},
Metadata = new Dictionary<string, object>()
{
{ "int", 10 },
{ "bool", false },
{ "string", "str"},
}
};

// Act

var serialized = _activitySerializer.Serialize(container);
var deserializedContainer = _activitySerializer.Deserialize(serialized) as Container;

// Assert

ValidateContainer(container, deserializedContainer);
}

private static void ValidateContainer(Container container, Container? deserializedContainer)
{
if (deserializedContainer == null)
throw new ArgumentNullException(nameof(deserializedContainer));

// Assert.Equivalent has trouble with the Behavior.Owner reference - since these aren't serialzied anyway, ignore them
deserializedContainer.Behaviors.Clear();
container.Behaviors.Clear();
foreach (Activity activity in deserializedContainer.Activities)
activity.Behaviors.Clear();
foreach (Activity activity in container.Activities)
activity.Behaviors.Clear();

// strict:false here allows "actual" to have extra public members that aren't part of "expected", and collection
// comparison allows "actual" to have more data in it than is present in "expected".
Assert.Equivalent(container, deserializedContainer, strict: false);
}
}

0 comments on commit ee733e3

Please sign in to comment.