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:
parent
6a79bb9e64
commit
b0e5061dce
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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!;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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>();
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
Loading…
Reference in a new issue