diff --git a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Serialization/FlowchartJsonConverter.cs b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Serialization/FlowchartJsonConverter.cs index f2cd5fd77..4cde5d8a6 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Serialization/FlowchartJsonConverter.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Serialization/FlowchartJsonConverter.cs @@ -5,11 +5,16 @@ using Elsa.Workflows.Core.Contracts; namespace Elsa.Workflows.Core.Activities.Flowchart.Serialization; +/// +/// A JSON converter for . +/// public class FlowchartJsonConverter : JsonConverter { private const string AllActivitiesKey = "AllActivities"; private const string AllConnectionsKey = "AllConnections"; + private const string NotFoundConnectionsKey = "NotFoundConnectionsKey"; + /// 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 var activityDictionary = activities.ToDictionary(x => x.Id); connectionSerializerOptions.Converters.Add(new ConnectionJsonConverter(activityDictionary)); + var connections = connectionsElement.ValueKind != JsonValueKind.Undefined ? connectionsElement.Deserialize>(connectionSerializerOptions)?.Where(x => x.Source != null! && x.Target != null!).ToList() ?? new List() : new List(); + + 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 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 GetNotFoundConnections(JsonElement rootElement, JsonSerializerOptions connectionSerializerOptions, IEnumerable activities, IEnumerable 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>(connectionSerializerOptions) + ?.Where(x => x.Source != null! && x.Target != null!).ToList() ?? new List() + : new List(); + + // Add connections of NotFoundActivity to the list if they aren't already in it. + var notFoundActivities = activities.Where(x => x is NotFoundActivity).Cast().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 FindConnectionsThatCanBeRestored(IEnumerable notFoundConnections, IEnumerable activities) + { + var connectionsThatCanBeRestored = new List(); + 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; + } + + /// public override void Write(Utf8JsonWriter writer, Activities.Flowchart value, JsonSerializerOptions options) { var activities = value.Activities; @@ -57,7 +113,7 @@ public class FlowchartJsonConverter : JsonConverter 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)(value.CustomProperties.TryGetValue(AllConnectionsKey, out var c) ? c : value.Connections); var applicationProperties = new Dictionary(value.CustomProperties); applicationProperties.Remove(AllActivitiesKey); @@ -65,8 +121,8 @@ public class FlowchartJsonConverter : JsonConverter var model = new { - Type = value.Type, - Version = value.Version, + value.Type, + value.Version, value.Id, value.Metadata, ApplicationProperties = applicationProperties, diff --git a/src/modules/Elsa.Workflows.Core/Activities/NotFoundActivity.cs b/src/modules/Elsa.Workflows.Core/Activities/NotFoundActivity.cs index 24ff531b3..72f8eb2f9 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/NotFoundActivity.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/NotFoundActivity.cs @@ -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; /// [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 { /// @@ -32,5 +34,8 @@ public class NotFoundActivity : CodeActivity /// public int MissingTypeVersion { get; set; } - public string OriginalActivityJson { get; set; } + /// + /// The original activity JSON. + /// + public string OriginalActivityJson { get; set; } = default!; } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Contracts/IActivityDescriber.cs b/src/modules/Elsa.Workflows.Core/Contracts/IActivityDescriber.cs index adae9b03b..ddef3d6ab 100644 --- a/src/modules/Elsa.Workflows.Core/Contracts/IActivityDescriber.cs +++ b/src/modules/Elsa.Workflows.Core/Contracts/IActivityDescriber.cs @@ -4,11 +4,58 @@ using Elsa.Workflows.Core.Models; namespace Elsa.Workflows.Core.Contracts; +/// +/// Creates instances of for a given activity type. +/// public interface IActivityDescriber { + /// + /// Describes the specified activity type. + /// + /// The activity type to describe. + /// The cancellation token. + /// The activity descriptor. ValueTask DescribeActivityAsync([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type activityType, CancellationToken cancellationToken = default); + + /// + /// Creates an instance of for the specified property. + /// + /// The property to describe. + /// The output descriptor. OutputDescriptor DescribeOutputProperty(PropertyInfo propertyInfo); + + /// + /// Creates an instance of for the specified property. + /// + /// The property to describe. + /// The input descriptor. InputDescriptor DescribeInputProperty(PropertyInfo propertyInfo); + + /// + /// Describes the input properties of the specified activity type. + /// + /// The activity type to describe. + /// The input descriptors. IEnumerable DescribeInputProperties([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type activityType); + + /// + /// Describes the output properties of the specified activity type. + /// + /// The activity type to describe. + /// The output descriptors. IEnumerable DescribeOutputProperties([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type activityType); + + /// + /// Gets the input properties of the specified activity type. + /// + /// The activity type. + /// The input properties. + IEnumerable GetInputProperties([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type activityType); + + /// + /// Gets the output properties of the specified activity type. + /// + /// The activity type. + /// The output properties. + IEnumerable GetOutputProperties([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type activityType); } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Contracts/IMissingActivityTypesRegistry.cs b/src/modules/Elsa.Workflows.Core/Contracts/IMissingActivityTypesRegistry.cs new file mode 100644 index 000000000..815c84d2b --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/Contracts/IMissingActivityTypesRegistry.cs @@ -0,0 +1,28 @@ +using Elsa.Workflows.Core.Models; + +namespace Elsa.Workflows.Core.Contracts; + +/// +/// A registry for activity types that are missing from the workflow definition. +/// +public interface IMissingActivityTypesRegistry +{ + /// + /// Adds a missing activity type. + /// + /// The missing activity type. + void Add(MissingActivityType missingActivityType); + + /// + /// Removes a missing activity type. + /// + /// The type name of the missing activity type to remove. + /// The version of the missing activity type to remove. + void Remove(string typeName, int version); + + /// + /// Returns a list of missing activity types. + /// + /// A list of missing activity types. + IEnumerable List(); +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Models/MissingActivityType.cs b/src/modules/Elsa.Workflows.Core/Models/MissingActivityType.cs new file mode 100644 index 000000000..f39e2bc06 --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/Models/MissingActivityType.cs @@ -0,0 +1,27 @@ +namespace Elsa.Workflows.Core.Models; + +/// +/// Represents a missing activity type. +/// +public class MissingActivityType +{ + /// + /// The type name of the missing activity type. + /// + public string TypeName { get; set; } = default!; + + /// + /// The version of the missing activity type. + /// + public int Version { get; set; } + + /// + /// The original activity JSON. + /// + public string ActivityJson { get; set; } = default!; + + /// + /// The original activity outcomes. + /// + public ICollection Outcomes { get; set; } = new List(); +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Services/ActivityDescriber.cs b/src/modules/Elsa.Workflows.Core/Services/ActivityDescriber.cs index 78bccc612..56f1ec9d5 100644 --- a/src/modules/Elsa.Workflows.Core/Services/ActivityDescriber.cs +++ b/src/modules/Elsa.Workflows.Core/Services/ActivityDescriber.cs @@ -64,7 +64,7 @@ public class ActivityDescriber : IActivityDescriber Name = x, DisplayName = x }).ToDictionary(x => x.Name) ?? new Dictionary(); - + 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); } - + + /// public IEnumerable GetInputProperties([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type activityType) => activityType.GetProperties().Where(x => typeof(Input).IsAssignableFrom(x.PropertyType) || x.GetCustomAttribute() != null).DistinctBy(x => x.Name); - + + /// public IEnumerable GetOutputProperties([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type activityType) => activityType.GetProperties().Where(x => typeof(Output).IsAssignableFrom(x.PropertyType)).DistinctBy(x => x.Name).ToList(); diff --git a/src/modules/Elsa.Workflows.Core/Services/ActivityRegistry.cs b/src/modules/Elsa.Workflows.Core/Services/ActivityRegistry.cs index d6533e289..d11d8691a 100644 --- a/src/modules/Elsa.Workflows.Core/Services/ActivityRegistry.cs +++ b/src/modules/Elsa.Workflows.Core/Services/ActivityRegistry.cs @@ -8,7 +8,6 @@ namespace Elsa.Workflows.Core.Services; public class ActivityRegistry : IActivityRegistry { private readonly IActivityDescriber _activityDescriber; - private readonly IActivityVisitor _activityVisitor; private readonly ISet _manualActivityDescriptors = new HashSet(); private readonly IDictionary> _providedActivityDescriptors = new Dictionary>(); private readonly IDictionary<(string Type, int Version), ActivityDescriptor> _activityDescriptors = new Dictionary<(string Type, int Version), ActivityDescriptor>(); @@ -16,10 +15,9 @@ public class ActivityRegistry : IActivityRegistry /// /// Initializes a new instance of the class. /// - public ActivityRegistry(IActivityDescriber activityDescriber, IActivityVisitor activityVisitor) + public ActivityRegistry(IActivityDescriber activityDescriber) { _activityDescriber = activityDescriber; - _activityVisitor = activityVisitor; } /// diff --git a/src/modules/Elsa.Workflows.Management/Serialization/Converters/ActivityJsonConverter.cs b/src/modules/Elsa.Workflows.Management/Serialization/Converters/ActivityJsonConverter.cs index 68aa9d340..be3d952aa 100644 --- a/src/modules/Elsa.Workflows.Management/Serialization/Converters/ActivityJsonConverter.cs +++ b/src/modules/Elsa.Workflows.Management/Serialization/Converters/ActivityJsonConverter.cs @@ -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; diff --git a/src/modules/Elsa.Workflows.Management/Serialization/Converters/ActivityJsonConverterFactory.cs b/src/modules/Elsa.Workflows.Management/Serialization/Converters/ActivityJsonConverterFactory.cs index 39d6de4a8..e85731a5d 100644 --- a/src/modules/Elsa.Workflows.Management/Serialization/Converters/ActivityJsonConverterFactory.cs +++ b/src/modules/Elsa.Workflows.Management/Serialization/Converters/ActivityJsonConverterFactory.cs @@ -5,10 +5,14 @@ using Microsoft.Extensions.DependencyInjection; namespace Elsa.Workflows.Management.Serialization.Converters; +/// +/// Creates instances of . +/// public class ActivityJsonConverterFactory : JsonConverterFactory { private readonly IServiceProvider _serviceProvider; + /// 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 + /// public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(IActivity); + /// public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) => ActivatorUtilities.CreateInstance(_serviceProvider); } \ No newline at end of file