Handle multiple output connections on import (#3857)

* Add XML comments, cleanup

* Incremental work on persisting "lost" connections

* Fixed issues with connections on import

---------

Co-authored-by: Sipke Schoorstra <sipkeschoorstra@outlook.com>
Co-authored-by: Gürkan Güran <gurkanguran@gurkanguransMBP.domain_not_set.invalid>
This commit is contained in:
gurkanguran 2023-03-30 10:19:05 +02:00 committed by GitHub
parent 6a79bb9e64
commit b0e5061dce
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 181 additions and 14 deletions

View file

@ -5,11 +5,16 @@ using Elsa.Workflows.Core.Contracts;
namespace Elsa.Workflows.Core.Activities.Flowchart.Serialization;
/// <summary>
/// A JSON converter for <see cref="Activities.Flowchart"/>.
/// </summary>
public class FlowchartJsonConverter : JsonConverter<Activities.Flowchart>
{
private const string AllActivitiesKey = "AllActivities";
private const string AllConnectionsKey = "AllConnections";
private const string NotFoundConnectionsKey = "NotFoundConnectionsKey";
/// <inheritdoc />
public override Activities.Flowchart Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (!JsonDocument.TryParseValue(ref reader, out var doc))
@ -27,9 +32,14 @@ public class FlowchartJsonConverter : JsonConverter<Activities.Flowchart>
var activityDictionary = activities.ToDictionary(x => x.Id);
connectionSerializerOptions.Converters.Add(new ConnectionJsonConverter(activityDictionary));
var connections = connectionsElement.ValueKind != JsonValueKind.Undefined
? connectionsElement.Deserialize<ICollection<Connection>>(connectionSerializerOptions)?.Where(x => x.Source != null! && x.Target != null!).ToList() ?? new List<Connection>()
: new List<Connection>();
var notFoundConnections = GetNotFoundConnections(doc.RootElement, connectionSerializerOptions, activities, connections);
var connectionsToRestore = FindConnectionsThatCanBeRestored(notFoundConnections, activities);
var connectionsWithRestoredOnes = connections.Except(notFoundConnections).Union(connectionsToRestore).ToList();
var flowChart = new Activities.Flowchart
{
@ -37,17 +47,63 @@ public class FlowchartJsonConverter : JsonConverter<Activities.Flowchart>
Metadata = metadata,
Start = start,
Activities = activities,
Connections = connections,
Connections = connectionsWithRestoredOnes,
CustomProperties =
{
[AllActivitiesKey] = activities.ToList(),
[AllConnectionsKey] = connections.ToList()
[AllConnectionsKey] = connectionsWithRestoredOnes,
[NotFoundConnectionsKey] = notFoundConnections.Except(connectionsToRestore)
}
};
return flowChart;
}
private static List<Connection> GetNotFoundConnections(JsonElement rootElement, JsonSerializerOptions connectionSerializerOptions, IEnumerable<IActivity> activities, IEnumerable<Connection> connections)
{
var applicationPropertiesElement = rootElement.TryGetProperty("applicationProperties", out var applicationPropertiesEl) ? applicationPropertiesEl : default;
var notFoundConnectionsElement = applicationPropertiesElement.ValueKind != JsonValueKind.Undefined ? applicationPropertiesElement.TryGetProperty(NotFoundConnectionsKey, out var notFoundConnectionsEl) ? notFoundConnectionsEl : default : default;
var notFoundConnections = notFoundConnectionsElement.ValueKind != JsonValueKind.Undefined
? notFoundConnectionsElement.Deserialize<ICollection<Connection>>(connectionSerializerOptions)
?.Where(x => x.Source != null! && x.Target != null!).ToList() ?? new List<Connection>()
: new List<Connection>();
// Add connections of NotFoundActivity to the list if they aren't already in it.
var notFoundActivities = activities.Where(x => x is NotFoundActivity).Cast<NotFoundActivity>().ToList();
var notFoundActivityConnections = connections.Where(x => notFoundActivities.Contains(x.Source)).ToList();
foreach (var notFoundConnection in notFoundActivityConnections)
{
if (notFoundConnections.All(x => x.Source != notFoundConnection.Source || x.Target != notFoundConnection.Target))
notFoundConnections.Add(notFoundConnection);
}
return notFoundConnections;
}
private static List<Connection> FindConnectionsThatCanBeRestored(IEnumerable<Connection> notFoundConnections, IEnumerable<IActivity> activities)
{
var connectionsThatCanBeRestored = new List<Connection>();
var foundActivities = activities.Where(x => x is not NotFoundActivity).ToList();
foreach (var notFoundConnection in notFoundConnections.ToList())
{
var missingSource = notFoundConnection.Source;
var missingTarget = notFoundConnection.Target;
var source = foundActivities.FirstOrDefault(x => x.Id == missingSource.Id);
var target = foundActivities.FirstOrDefault(x => x.Id == missingTarget.Id);
if (source == null || target == null) continue;
var connection = notFoundConnection with {Source = source, Target = target};
connectionsThatCanBeRestored.Add(connection);
}
return connectionsThatCanBeRestored;
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, Activities.Flowchart value, JsonSerializerOptions options)
{
var activities = value.Activities;
@ -57,7 +113,7 @@ public class FlowchartJsonConverter : JsonConverter<Activities.Flowchart>
connectionSerializerOptions.Converters.Add(new ConnectionJsonConverter(activityDictionary));
var allActivities = value.CustomProperties.TryGetValue(AllActivitiesKey, out var a) ? a : activities;
var allConnections = value.CustomProperties.TryGetValue(AllConnectionsKey, out var c) ? c : value.Connections;
var allConnections = (ICollection<Connection>)(value.CustomProperties.TryGetValue(AllConnectionsKey, out var c) ? c : value.Connections);
var applicationProperties = new Dictionary<string, object>(value.CustomProperties);
applicationProperties.Remove(AllActivitiesKey);
@ -65,8 +121,8 @@ public class FlowchartJsonConverter : JsonConverter<Activities.Flowchart>
var model = new
{
Type = value.Type,
Version = value.Version,
value.Type,
value.Version,
value.Id,
value.Metadata,
ApplicationProperties = applicationProperties,

View file

@ -1,6 +1,7 @@
using System.ComponentModel;
using Elsa.Workflows.Core.Attributes;
using Elsa.Workflows.Core.Models;
using JetBrains.Annotations;
namespace Elsa.Workflows.Core.Activities;
@ -9,6 +10,7 @@ namespace Elsa.Workflows.Core.Activities;
/// </summary>
[Browsable(false)]
[Activity("Elsa", "System", "A placeholder activity that will be used in case a workflow definition references an activity type that cannot be found.")]
[PublicAPI]
public class NotFoundActivity : CodeActivity
{
/// <inheritdoc />
@ -32,5 +34,8 @@ public class NotFoundActivity : CodeActivity
/// </summary>
public int MissingTypeVersion { get; set; }
public string OriginalActivityJson { get; set; }
/// <summary>
/// The original activity JSON.
/// </summary>
public string OriginalActivityJson { get; set; } = default!;
}

View file

@ -4,11 +4,58 @@ using Elsa.Workflows.Core.Models;
namespace Elsa.Workflows.Core.Contracts;
/// <summary>
/// Creates instances of <see cref="ActivityDescriptor" /> for a given activity type.
/// </summary>
public interface IActivityDescriber
{
/// <summary>
/// Describes the specified activity type.
/// </summary>
/// <param name="activityType">The activity type to describe.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The activity descriptor.</returns>
ValueTask<ActivityDescriptor> DescribeActivityAsync([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type activityType, CancellationToken cancellationToken = default);
/// <summary>
/// Creates an instance of <see cref="OutputDescriptor"/> for the specified property.
/// </summary>
/// <param name="propertyInfo">The property to describe.</param>
/// <returns>The output descriptor.</returns>
OutputDescriptor DescribeOutputProperty(PropertyInfo propertyInfo);
/// <summary>
/// Creates an instance of <see cref="InputDescriptor"/> for the specified property.
/// </summary>
/// <param name="propertyInfo">The property to describe.</param>
/// <returns>The input descriptor.</returns>
InputDescriptor DescribeInputProperty(PropertyInfo propertyInfo);
/// <summary>
/// Describes the input properties of the specified activity type.
/// </summary>
/// <param name="activityType">The activity type to describe.</param>
/// <returns>The input descriptors.</returns>
IEnumerable<InputDescriptor> DescribeInputProperties([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type activityType);
/// <summary>
/// Describes the output properties of the specified activity type.
/// </summary>
/// <param name="activityType">The activity type to describe.</param>
/// <returns>The output descriptors.</returns>
IEnumerable<OutputDescriptor> DescribeOutputProperties([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type activityType);
/// <summary>
/// Gets the input properties of the specified activity type.
/// </summary>
/// <param name="activityType">The activity type.</param>
/// <returns>The input properties.</returns>
IEnumerable<PropertyInfo> GetInputProperties([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type activityType);
/// <summary>
/// Gets the output properties of the specified activity type.
/// </summary>
/// <param name="activityType">The activity type.</param>
/// <returns>The output properties.</returns>
IEnumerable<PropertyInfo> GetOutputProperties([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type activityType);
}

View file

@ -0,0 +1,28 @@
using Elsa.Workflows.Core.Models;
namespace Elsa.Workflows.Core.Contracts;
/// <summary>
/// A registry for activity types that are missing from the workflow definition.
/// </summary>
public interface IMissingActivityTypesRegistry
{
/// <summary>
/// Adds a missing activity type.
/// </summary>
/// <param name="missingActivityType">The missing activity type.</param>
void Add(MissingActivityType missingActivityType);
/// <summary>
/// Removes a missing activity type.
/// </summary>
/// <param name="typeName">The type name of the missing activity type to remove.</param>
/// <param name="version">The version of the missing activity type to remove.</param>
void Remove(string typeName, int version);
/// <summary>
/// Returns a list of missing activity types.
/// </summary>
/// <returns>A list of missing activity types.</returns>
IEnumerable<MissingActivityType> List();
}

View file

@ -0,0 +1,27 @@
namespace Elsa.Workflows.Core.Models;
/// <summary>
/// Represents a missing activity type.
/// </summary>
public class MissingActivityType
{
/// <summary>
/// The type name of the missing activity type.
/// </summary>
public string TypeName { get; set; } = default!;
/// <summary>
/// The version of the missing activity type.
/// </summary>
public int Version { get; set; }
/// <summary>
/// The original activity JSON.
/// </summary>
public string ActivityJson { get; set; } = default!;
/// <summary>
/// The original activity outcomes.
/// </summary>
public ICollection<string> Outcomes { get; set; } = new List<string>();
}

View file

@ -64,7 +64,7 @@ public class ActivityDescriber : IActivityDescriber
Name = x,
DisplayName = x
}).ToDictionary(x => x.Name) ?? new Dictionary<string, Port>();
var allPorts = embeddedPorts.Concat(flowPorts.Values);
var inputProperties = GetInputProperties(activityType).ToList();
var outputProperties = GetOutputProperties(activityType).ToList();
@ -94,10 +94,12 @@ public class ActivityDescriber : IActivityDescriber
return ValueTask.FromResult(descriptor);
}
/// <inheritdoc />
public IEnumerable<PropertyInfo> GetInputProperties([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type activityType) =>
activityType.GetProperties().Where(x => typeof(Input).IsAssignableFrom(x.PropertyType) || x.GetCustomAttribute<InputAttribute>() != null).DistinctBy(x => x.Name);
/// <inheritdoc />
public IEnumerable<PropertyInfo> GetOutputProperties([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type activityType) =>
activityType.GetProperties().Where(x => typeof(Output).IsAssignableFrom(x.PropertyType)).DistinctBy(x => x.Name).ToList();

View file

@ -8,7 +8,6 @@ namespace Elsa.Workflows.Core.Services;
public class ActivityRegistry : IActivityRegistry
{
private readonly IActivityDescriber _activityDescriber;
private readonly IActivityVisitor _activityVisitor;
private readonly ISet<ActivityDescriptor> _manualActivityDescriptors = new HashSet<ActivityDescriptor>();
private readonly IDictionary<Type, ICollection<ActivityDescriptor>> _providedActivityDescriptors = new Dictionary<Type, ICollection<ActivityDescriptor>>();
private readonly IDictionary<(string Type, int Version), ActivityDescriptor> _activityDescriptors = new Dictionary<(string Type, int Version), ActivityDescriptor>();
@ -16,10 +15,9 @@ public class ActivityRegistry : IActivityRegistry
/// <summary>
/// Initializes a new instance of the <see cref="ActivityRegistry"/> class.
/// </summary>
public ActivityRegistry(IActivityDescriber activityDescriber, IActivityVisitor activityVisitor)
public ActivityRegistry(IActivityDescriber activityDescriber)
{
_activityDescriber = activityDescriber;
_activityVisitor = activityVisitor;
}
/// <inheritdoc />

View file

@ -1,7 +1,5 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Serialization;
using Elsa.Common.Models;
using Elsa.Expressions.Contracts;
using Elsa.Expressions.Helpers;
using Elsa.Expressions.Models;

View file

@ -5,10 +5,14 @@ using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Workflows.Management.Serialization.Converters;
/// <summary>
/// Creates instances of <see cref="ActivityJsonConverter"/>.
/// </summary>
public class ActivityJsonConverterFactory : JsonConverterFactory
{
private readonly IServiceProvider _serviceProvider;
/// <inheritdoc />
public ActivityJsonConverterFactory(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
@ -16,7 +20,9 @@ public class ActivityJsonConverterFactory : JsonConverterFactory
// Notice that this factory only creates converters when the type to convert is IActivity.
// The ActivityJsonConverter will create concrete activity objects, which then uses regular serialization
/// <inheritdoc />
public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(IActivity);
/// <inheritdoc />
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) => ActivatorUtilities.CreateInstance<ActivityJsonConverter>(_serviceProvider);
}