Merge branch 'main' of github.com:elsa-workflows/elsa-core

This commit is contained in:
Sipke Schoorstra 2024-06-15 19:38:20 +02:00
commit d70451e72b
4 changed files with 253 additions and 26 deletions

View file

@ -5,6 +5,7 @@ using Elsa.Workflows.Behaviors;
using Elsa.Workflows.Contracts;
using Elsa.Workflows.Helpers;
using Elsa.Workflows.Models;
using Elsa.Workflows.Serialization.Converters;
using JetBrains.Annotations;
namespace Elsa.Workflows;
@ -75,6 +76,7 @@ public abstract class Activity : IActivity, ISignalHandler
}
/// <inheritdoc />
[JsonConverter(typeof(PolymorphicObjectConverterFactory))]
public IDictionary<string, object> CustomProperties { get; set; } = new Dictionary<string, object>();
/// <inheritdoc />
@ -82,6 +84,7 @@ public abstract class Activity : IActivity, ISignalHandler
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>

View file

@ -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;
@ -27,35 +30,49 @@ public class FlowchartJsonConverter : JsonConverter<Activities.Flowchart>
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;
@ -65,11 +82,8 @@ public class FlowchartJsonConverter : JsonConverter<Activities.Flowchart>
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);
@ -79,17 +93,23 @@ public class FlowchartJsonConverter : JsonConverter<Activities.Flowchart>
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
};
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, connectionSerializerOptions);
JsonSerializer.Serialize(writer, model, flowchartSerializerOptions);
}
private static ICollection<Connection> GetNotFoundConnections(JsonElement rootElement, IDictionary<string, IActivity> activities, IEnumerable<Connection> connections, JsonSerializerOptions connectionSerializerOptions)

View file

@ -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 />

View file

@ -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);
}
}