Incremental work on transposition
This commit is contained in:
parent
d5826c7fd0
commit
8d3aaea1e4
|
|
@ -15,6 +15,7 @@ using Elsa.Scheduling.Extensions;
|
|||
using Elsa.Liquid.Extensions;
|
||||
using Elsa.Workflows.Core;
|
||||
using Elsa.Workflows.Core.Activities;
|
||||
using Elsa.Workflows.Core.Activities.Flowchart.Activities;
|
||||
using Elsa.Workflows.Core.Serialization;
|
||||
using Elsa.Workflows.Core.Services;
|
||||
using Elsa.Workflows.Management.Extensions;
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ using Elsa.Scheduling.Extensions;
|
|||
using Elsa.WorkflowContexts.Extensions;
|
||||
using Elsa.Workflows.Api.Extensions;
|
||||
using Elsa.Workflows.Core.Activities;
|
||||
using Elsa.Workflows.Core.Activities.Flowchart.Activities;
|
||||
using Elsa.Workflows.Core.Pipelines.WorkflowExecution.Components;
|
||||
using Elsa.Workflows.Core.Serialization;
|
||||
using Elsa.Workflows.Core.Services;
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ namespace Elsa.Features;
|
|||
|
||||
[DependsOn(typeof(MediatorFeature))]
|
||||
[DependsOn(typeof(WorkflowsFeature))]
|
||||
[DependsOn(typeof(FlowchartFeature))]
|
||||
[DependsOn(typeof(WorkflowPersistenceFeature))]
|
||||
[DependsOn(typeof(WorkflowRuntimeFeature))]
|
||||
[DependsOn(typeof(WorkflowManagementFeature))]
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ function getPorts(node: ActivityNode, activity: Activity, descriptors: Array<Act
|
|||
const portProviderRegistry = Container.get(PortProviderRegistry);
|
||||
const transposeHandlerRegistry = Container.get(TransposeHandlerRegistry);
|
||||
const transposeHandler = transposeHandlerRegistry.get(activity.typeName);
|
||||
const portProvider = portProviderRegistry.get(activity.typeName);
|
||||
//const portProvider = portProviderRegistry.get(activity.typeName);
|
||||
const activityDescriptor = descriptors.find(x => x.activityType == activity.typeName);
|
||||
const activityCopy: Activity = {...activity};
|
||||
const untransposedConnections = transposeHandler.untranspose({activity: activityCopy, activityDescriptor});
|
||||
|
|
|
|||
|
|
@ -175,27 +175,27 @@ export class FlowchartComponent implements ContainerActivityComponent {
|
|||
const graphModel = graph.toJSON();
|
||||
const activities = graphModel.cells.filter(x => x.shape == 'activity').map(x => x.data as Activity);
|
||||
const connections = graphModel.cells.filter(x => x.shape == 'elsa-edge' && !!x.data).map(x => x.data as Connection);
|
||||
const remainingConnections: Array<Connection> = []; // The connections remaining after transposition.
|
||||
let remainingActivities: Array<Activity> = [...activities]; // The activities remaining after transposition.
|
||||
const activityDescriptors = descriptorsStore.activityDescriptors;
|
||||
const transposeHandlerRegistry = Container.get(TransposeHandlerRegistry);
|
||||
//const remainingConnections: Array<Connection> = []; // The connections remaining after transposition.
|
||||
//let remainingActivities: Array<Activity> = [...activities]; // The activities remaining after transposition.
|
||||
//const activityDescriptors = descriptorsStore.activityDescriptors;
|
||||
//const transposeHandlerRegistry = Container.get(TransposeHandlerRegistry);
|
||||
|
||||
// Transpose connections to activity outbound port properties.
|
||||
for (const connection of connections) {
|
||||
const source = activities.find(x => x.id == connection.source);
|
||||
const target = activities.find(x => x.id == connection.target);
|
||||
const sourceDescriptor = activityDescriptors.find(x => x.activityType == source.typeName);
|
||||
const targetDescriptor = activityDescriptors.find(x => x.activityType == target.typeName);
|
||||
const transposeHandler = transposeHandlerRegistry.get(source.typeName);
|
||||
|
||||
if (transposeHandler.transpose({source, target, connection, sourceDescriptor, targetDescriptor})) {
|
||||
// Remove the target activity from the list.
|
||||
remainingActivities = remainingActivities.filter(x => x != target);
|
||||
} else {
|
||||
// Keep this connection.
|
||||
remainingConnections.push(connection);
|
||||
}
|
||||
}
|
||||
// // Transpose connections to activity outbound port properties.
|
||||
// for (const connection of connections) {
|
||||
// const source = activities.find(x => x.id == connection.source);
|
||||
// const target = activities.find(x => x.id == connection.target);
|
||||
// const sourceDescriptor = activityDescriptors.find(x => x.activityType == source.typeName);
|
||||
// const targetDescriptor = activityDescriptors.find(x => x.activityType == target.typeName);
|
||||
// const transposeHandler = transposeHandlerRegistry.get(source.typeName);
|
||||
//
|
||||
// if (transposeHandler.transpose({source, target, connection, sourceDescriptor, targetDescriptor})) {
|
||||
// // Remove the target activity from the list.
|
||||
// remainingActivities = remainingActivities.filter(x => x != target);
|
||||
// } else {
|
||||
// // Keep this connection.
|
||||
// remainingConnections.push(connection);
|
||||
// }
|
||||
// }
|
||||
|
||||
let rootActivities = activities.filter(activity => {
|
||||
const hasInboundConnections = connections.find(c => c.target == activity.id) != null;
|
||||
|
|
@ -206,8 +206,8 @@ export class FlowchartComponent implements ContainerActivityComponent {
|
|||
|
||||
return {
|
||||
typeName: 'Elsa.Flowchart',
|
||||
activities: remainingActivities,
|
||||
connections: remainingConnections,
|
||||
activities: activities,
|
||||
connections: connections,
|
||||
id: this.rootId,
|
||||
start: rootActivity?.id,
|
||||
metadata: {},
|
||||
|
|
@ -220,12 +220,14 @@ export class FlowchartComponent implements ContainerActivityComponent {
|
|||
this.rootId = root.id;
|
||||
const descriptors = descriptorsStore.activityDescriptors;
|
||||
const flowchart = root as Flowchart;
|
||||
const flowchartGraph = walkActivities(flowchart, descriptorsStore.activityDescriptors);
|
||||
const flowchartNodes = flattenList(flowchartGraph.children);
|
||||
const transposeHandlerRegistry = Container.get(TransposeHandlerRegistry);
|
||||
const activities = flowchart.activities;
|
||||
const connections = flowchart.connections;
|
||||
//const flowchartGraph = walkActivities(flowchart, descriptorsStore.activityDescriptors);
|
||||
//const flowchartNodes = flattenList(flowchartGraph.children);
|
||||
//const transposeHandlerRegistry = Container.get(TransposeHandlerRegistry);
|
||||
|
||||
// Clear inbound port for start activity.
|
||||
const startActivityNode = flowchartNodes.find(x => x.activity.id === flowchart.start);
|
||||
const startActivityNode = activities.find(x => x.activity.id === flowchart.start);
|
||||
|
||||
if (startActivityNode?.port)
|
||||
delete startActivityNode.port;
|
||||
|
|
@ -233,8 +235,7 @@ export class FlowchartComponent implements ContainerActivityComponent {
|
|||
let edges: Array<Edge.Metadata> = [];
|
||||
|
||||
// Create an X6 node for each activity.
|
||||
const nodes: Array<Node.Metadata> = flowchartNodes.map(activityNode => {
|
||||
const activity = activityNode.activity;
|
||||
const nodes: Array<Node.Metadata> = activities.map(activity => {
|
||||
const position = activity.metadata.designer?.position || {x: 100, y: 100};
|
||||
const {x, y} = position;
|
||||
const descriptor = descriptors.find(x => x.activityType == activity.typeName)
|
||||
|
|
@ -242,9 +243,9 @@ export class FlowchartComponent implements ContainerActivityComponent {
|
|||
});
|
||||
|
||||
// Create X6 edges for each child activity.
|
||||
for (const rootNodes of flowchartNodes) {
|
||||
const childEdges = this.createEdges(rootNodes);
|
||||
edges = [...edges, ...childEdges];
|
||||
for (const connection of connections) {
|
||||
const edge = this.createEdge(connection);
|
||||
edges = [...edges, edge];
|
||||
}
|
||||
|
||||
// Create X6 edges for each connection in the flowchart.
|
||||
|
|
@ -261,23 +262,6 @@ export class FlowchartComponent implements ContainerActivityComponent {
|
|||
this.graph.unfreeze();
|
||||
};
|
||||
|
||||
private createEdges = (activityNode: ActivityNode): Array<Edge.Metadata> => {
|
||||
let edges: Array<Edge.Metadata> = [];
|
||||
|
||||
for (const childNode of activityNode.children) {
|
||||
const edge = this.createEdge({
|
||||
source: activityNode.activity.id,
|
||||
sourcePort: childNode.port,
|
||||
target: childNode.activity.id,
|
||||
targetPort: 'In'
|
||||
});
|
||||
|
||||
edges.push(edge);
|
||||
}
|
||||
|
||||
return edges;
|
||||
}
|
||||
|
||||
createEdge = (connection: Connection): Edge.Metadata => {
|
||||
return {
|
||||
shape: 'elsa-edge',
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ using System.Text.Json.Serialization;
|
|||
using Elsa.AspNetCore;
|
||||
using Elsa.AspNetCore.Attributes;
|
||||
using Elsa.AspNetCore.Models;
|
||||
using Elsa.Workflows.Core.Models;
|
||||
using Elsa.Workflows.Core.Serialization.Converters;
|
||||
using Elsa.Workflows.Core.Services;
|
||||
using Elsa.Workflows.Management.Models;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
using Elsa.Workflows.Core.Activities.Flowchart.Models;
|
||||
using Elsa.Workflows.Core.Attributes;
|
||||
using Elsa.Workflows.Core.Models;
|
||||
using Elsa.Workflows.Core.Services;
|
||||
using Elsa.Workflows.Core.Signals;
|
||||
|
||||
namespace Elsa.Workflows.Core.Activities;
|
||||
|
||||
public record Connection(IActivity Source, IActivity Target, string? SourcePort, string TargetPort);
|
||||
namespace Elsa.Workflows.Core.Activities.Flowchart.Activities;
|
||||
|
||||
[Activity("Elsa", "Workflows", "A flowchart is a collection of activities and connections between them.")]
|
||||
public class Flowchart : Container
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
using Elsa.Workflows.Core.Activities.Flowchart.Models;
|
||||
using Elsa.Workflows.Core.Activities.Flowchart.Transposition.Services;
|
||||
|
||||
namespace Elsa.Workflows.Core.Activities.Flowchart.Handlers;
|
||||
|
||||
public class DefaultTransposeHandler : ITransposeHandler
|
||||
{
|
||||
public bool Transpose(TransposeContext context)
|
||||
{
|
||||
var (connection, sourceDescriptor, _) = context;
|
||||
var (source, target) = connection;
|
||||
var matchingTargetPort = sourceDescriptor.OutPorts.FirstOrDefault(x => x.Name == connection.SourcePort);
|
||||
|
||||
if (matchingTargetPort == null)
|
||||
return false;
|
||||
|
||||
// Assign the target activity directly to the outbound port of the source activity.
|
||||
source.SetPropertyValue(connection.SourcePort!, target);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
using System.Reflection;
|
||||
using Elsa.Workflows.Core.Activities.Flowchart.Models;
|
||||
using Elsa.Workflows.Core.Activities.Flowchart.Services;
|
||||
using Elsa.Workflows.Core.Attributes;
|
||||
using Elsa.Workflows.Core.Helpers;
|
||||
using Elsa.Workflows.Core.Models;
|
||||
using Elsa.Workflows.Core.Services;
|
||||
|
||||
namespace Elsa.Workflows.Core.Activities.Flowchart.Implementations;
|
||||
|
||||
public class ActivityNodeDescriber : IActivityNodeDescriber
|
||||
{
|
||||
public ActivityNodeDescriptor DescribeActivity(Type activityType)
|
||||
{
|
||||
var fullTypeName = ActivityTypeNameHelper.GenerateTypeName(activityType);
|
||||
|
||||
var outboundPorts =
|
||||
from prop in activityType.GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
||||
where typeof(IActivity).IsAssignableFrom(prop.PropertyType) || typeof(IEnumerable<IActivity>).IsAssignableFrom(prop.PropertyType)
|
||||
let portAttr = prop.GetCustomAttribute<OutboundAttribute>()
|
||||
where portAttr != null
|
||||
select new Port
|
||||
{
|
||||
Name = portAttr.Name ?? prop.Name,
|
||||
DisplayName = portAttr.DisplayName ?? portAttr.Name ?? prop.Name
|
||||
};
|
||||
|
||||
return new ActivityNodeDescriptor(activityType, fullTypeName, outboundPorts.ToList());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
using Elsa.Workflows.Core.Activities.Flowchart.Handlers;
|
||||
using Elsa.Workflows.Core.Activities.Flowchart.Services;
|
||||
using Elsa.Workflows.Core.Activities.Flowchart.Transposition.Services;
|
||||
|
||||
namespace Elsa.Workflows.Core.Activities.Flowchart.Implementations;
|
||||
|
||||
public class TransposeHandlerRegistry : ITransposeHandlerRegistry
|
||||
{
|
||||
private readonly IDictionary<string, Func<ITransposeHandler>> _dictionary = new Dictionary<string, Func<ITransposeHandler>>();
|
||||
public void Add(string activityType, Func<ITransposeHandler> handlerFactory) => _dictionary[activityType] = handlerFactory;
|
||||
public bool TryGet(string activityType, out Func<ITransposeHandler> handler) => _dictionary.TryGetValue(activityType, out handler!);
|
||||
public ITransposeHandler Create(string activityType) => TryGet(activityType, out var h) ? h() : new DefaultTransposeHandler();
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
using Elsa.Workflows.Core.Models;
|
||||
|
||||
namespace Elsa.Workflows.Core.Activities.Flowchart.Models;
|
||||
|
||||
public record ActivityNodeDescriptor(Type ActivityRuntimeType, string ActivityTypeName, ICollection<Port> OutPorts);
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
using Elsa.Workflows.Core.Services;
|
||||
|
||||
namespace Elsa.Workflows.Core.Activities.Flowchart.Models;
|
||||
|
||||
public record Connection(IActivity Source, IActivity Target, string? SourcePort, string TargetPort)
|
||||
{
|
||||
public void Deconstruct(out IActivity source, out IActivity target)
|
||||
{
|
||||
source = Source;
|
||||
target = Target;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
using Elsa.Workflows.Core.Models;
|
||||
using Elsa.Workflows.Core.Services;
|
||||
|
||||
namespace Elsa.Workflows.Core.Activities.Flowchart.Models;
|
||||
|
||||
public record TransposeContext(Connection Connection, ActivityNodeDescriptor SourceDescriptor, ActivityNodeDescriptor TargetDescriptor);
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Elsa.Workflows.Core.Activities;
|
||||
using Elsa.Workflows.Core.Activities.Flowchart.Models;
|
||||
using Elsa.Workflows.Core.Services;
|
||||
|
||||
namespace Elsa.Workflows.Core.Serialization.Converters;
|
||||
namespace Elsa.Workflows.Core.Activities.Flowchart.Serialization;
|
||||
|
||||
public class ConnectionJsonConverter : JsonConverter<Connection>
|
||||
{
|
||||
|
|
@ -14,7 +14,7 @@ public class ConnectionJsonConverter : JsonConverter<Connection>
|
|||
_activities = activities;
|
||||
}
|
||||
|
||||
public override Connection? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
public override Connection Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (!JsonDocument.TryParseValue(ref reader, out var doc))
|
||||
throw new JsonException("Failed to parse JsonDocument");
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Elsa.Workflows.Core.Activities.Flowchart.Models;
|
||||
using Elsa.Workflows.Core.Activities.Flowchart.Services;
|
||||
using Elsa.Workflows.Core.Activities.Flowchart.Transposition.Services;
|
||||
using Elsa.Workflows.Core.Serialization.Converters;
|
||||
using Elsa.Workflows.Core.Services;
|
||||
|
||||
namespace Elsa.Workflows.Core.Activities.Flowchart.Serialization;
|
||||
|
||||
public class FlowchartJsonConverter : JsonConverter<Activities.Flowchart>
|
||||
{
|
||||
private readonly IActivityWalker _activityWalker;
|
||||
private readonly IActivityNodeDescriber _activityNodeDescriber;
|
||||
|
||||
public FlowchartJsonConverter(IActivityWalker activityWalker, IActivityNodeDescriber activityNodeDescriber)
|
||||
{
|
||||
_activityWalker = activityWalker;
|
||||
_activityNodeDescriber = activityNodeDescriber;
|
||||
}
|
||||
|
||||
public override Activities.Flowchart? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (!JsonDocument.TryParseValue(ref reader, out var doc))
|
||||
throw new JsonException("Failed to parse JsonDocument");
|
||||
|
||||
var connectionsElement = doc.RootElement.GetProperty("connections");
|
||||
var metadataElement = doc.RootElement.GetProperty("metadata");
|
||||
var activitiesElement = doc.RootElement.GetProperty("activities");
|
||||
var id = doc.RootElement.GetProperty("id").GetString()!;
|
||||
var startId = doc.RootElement.TryGetProperty("start", out var startElement) ? startElement.GetString() : default;
|
||||
var activities = activitiesElement.Deserialize<ICollection<IActivity>>(options) ?? new List<IActivity>();
|
||||
var activityTypes = activities.Select(x => x.GetType()).Distinct().ToList();
|
||||
var activityNodeDescriptors = activityTypes.Select(_activityNodeDescriber.DescribeActivity).ToDictionary(x => x.ActivityRuntimeType);
|
||||
var metadata = metadataElement.Deserialize<IDictionary<string, object>>(options) ?? new Dictionary<string, object>();
|
||||
var start = activities.FirstOrDefault(x => x.Id == startId) ?? activities.FirstOrDefault();
|
||||
var connectionSerializerOptions = new JsonSerializerOptions(options);
|
||||
var activityDictionary = activities.ToDictionary(x => x.Id);
|
||||
connectionSerializerOptions.Converters.Add(new ConnectionJsonConverter(activityDictionary));
|
||||
|
||||
var connections = connectionsElement.Deserialize<ICollection<Connection>>(connectionSerializerOptions) ?? new List<Connection>();
|
||||
var transposedStart = Transpose(start, activityDictionary, connections, activityNodeDescriptors);
|
||||
|
||||
return new Activities.Flowchart
|
||||
{
|
||||
Id = id,
|
||||
Metadata = metadata,
|
||||
Activities = activities,
|
||||
Connections = connections,
|
||||
Start = start,
|
||||
};
|
||||
}
|
||||
|
||||
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 model = new
|
||||
{
|
||||
value.TypeName,
|
||||
value.Id,
|
||||
value.Metadata,
|
||||
value.ApplicationProperties,
|
||||
Start = value.Start?.Id,
|
||||
value.Activities,
|
||||
value.Connections
|
||||
};
|
||||
|
||||
JsonSerializer.Serialize(writer, model, connectionSerializerOptions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starting from the root, find each outbound connection and transpose each connection by constructing a sub-flow in case the source port is an actual outbound property on the source activity.
|
||||
/// For example, the <see cref="ForEach"/> activity has a <see cref="ForEach.Body"/> outbound activity property.
|
||||
/// We follow the outbound connection, take the target activity, and assign it to the Body property.
|
||||
/// </summary>
|
||||
private IActivity? Transpose(IActivity? start, IDictionary<string, IActivity> activities, ICollection<Connection> connections, IDictionary<Type, ActivityNodeDescriptor> descriptors)
|
||||
{
|
||||
if (start == null)
|
||||
return null;
|
||||
|
||||
var outboundConnections = connections.Where(x => x.Source == start).ToList();
|
||||
|
||||
|
||||
foreach (var connection in outboundConnections)
|
||||
{
|
||||
var transposeContext = new TransposeContext(connection,)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Elsa.Workflows.Core.Serialization.Converters;
|
||||
using Elsa.Workflows.Core.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Elsa.Workflows.Core.Activities.Flowchart.Serialization;
|
||||
|
||||
/// <summary>
|
||||
/// Add additional <see cref="JsonConverter"/> objects.
|
||||
/// </summary>
|
||||
public class FlowchartSerializationOptionConfigurator : ISerializationOptionsConfigurator
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
public FlowchartSerializationOptionConfigurator(IServiceProvider serviceProvider) => _serviceProvider = serviceProvider;
|
||||
public void Configure(JsonSerializerOptions options) => options.Converters.Add(Create<FlowchartJsonConverter>());
|
||||
private T Create<T>() => ActivatorUtilities.CreateInstance<T>(_serviceProvider);
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
using Elsa.Workflows.Core.Activities.Flowchart.Models;
|
||||
|
||||
namespace Elsa.Workflows.Core.Activities.Flowchart.Services;
|
||||
|
||||
public interface IActivityNodeDescriber
|
||||
{
|
||||
ActivityNodeDescriptor DescribeActivity(Type activityType);
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
using Elsa.Workflows.Core.Activities.Flowchart.Activities;
|
||||
using Elsa.Workflows.Core.Activities.Flowchart.Models;
|
||||
|
||||
namespace Elsa.Workflows.Core.Activities.Flowchart.Transposition.Services;
|
||||
|
||||
public interface ITransposeHandler
|
||||
{
|
||||
bool Transpose(TransposeContext context);
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
using Elsa.Workflows.Core.Activities.Flowchart.Transposition.Services;
|
||||
|
||||
namespace Elsa.Workflows.Core.Activities.Flowchart.Services;
|
||||
|
||||
public interface ITransposeHandlerRegistry
|
||||
{
|
||||
void Add(string activityType, Func<ITransposeHandler> handlerFactory);
|
||||
bool TryGet(string activityType, out Func<ITransposeHandler>? handler);
|
||||
ITransposeHandler Create(string activityType);
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@ using Elsa.Workflows.Core.Services;
|
|||
|
||||
namespace Elsa.Workflows.Core.ActivityNodeResolvers;
|
||||
|
||||
public class OutboundActivityNodeResolver : IActivityNodeResolver
|
||||
public class OutboundActivityPortResolver : IActivityPortResolver
|
||||
{
|
||||
public int Priority => -1;
|
||||
public bool GetSupportsActivity(IActivity activity) => activity is Activity;
|
||||
|
|
@ -16,6 +16,18 @@ public class OutboundActivityNodeResolver : IActivityNodeResolver
|
|||
.Select(x => x!)
|
||||
.ToHashSet();
|
||||
|
||||
public IEnumerable<PropertyInfo> GetPorts(Type activityType)
|
||||
{
|
||||
var outboundPortProperties =
|
||||
from prop in activityType.GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
||||
where typeof(IActivity).IsAssignableFrom(prop.PropertyType) || typeof(IEnumerable<IActivity>).IsAssignableFrom(prop.PropertyType)
|
||||
let portAttr = prop.GetCustomAttribute<OutboundAttribute>()
|
||||
where portAttr != null
|
||||
select prop;
|
||||
|
||||
return outboundPortProperties;
|
||||
}
|
||||
|
||||
private static IEnumerable<IActivity?> GetSinglePorts(IActivity activity)
|
||||
{
|
||||
var activityType = activity.GetType();
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
using System.Reflection;
|
||||
using Elsa.Workflows.Core.Activities;
|
||||
using Elsa.Workflows.Core.Services;
|
||||
|
||||
|
|
@ -6,7 +7,7 @@ namespace Elsa.Workflows.Core.ActivityNodeResolvers;
|
|||
/// <summary>
|
||||
/// Returns a list of outbound activities for a given <see cref="Switch"/> activity's branches.
|
||||
/// </summary>
|
||||
public class SwitchActivityNodeResolver : IActivityNodeResolver
|
||||
public class SwitchActivityPortResolver : IActivityPortResolver
|
||||
{
|
||||
public int Priority => 0;
|
||||
public bool GetSupportsActivity(IActivity activity) => activity is Switch;
|
||||
|
|
@ -22,4 +23,9 @@ public class SwitchActivityNodeResolver : IActivityNodeResolver
|
|||
if (@switch.Default != null)
|
||||
yield return @switch.Default;
|
||||
}
|
||||
|
||||
public IEnumerable<PropertyInfo> GetPorts(Type activityType)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,17 @@
|
|||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using System.Windows.Markup;
|
||||
|
||||
namespace Elsa.Workflows.Core
|
||||
{
|
||||
public static class PropertyAccessorExtensions
|
||||
{
|
||||
public static void SetPropertyValue(this object target, string propertyName, object? value)
|
||||
{
|
||||
var property = target.GetType().GetProperty(propertyName)!;
|
||||
property.SetValue(target, value);
|
||||
}
|
||||
|
||||
public static void SetPropertyValue<T, TProperty>(this T target, Expression<Func<T, TProperty>> expression, TProperty value)
|
||||
{
|
||||
var property = expression.GetProperty();
|
||||
|
|
|
|||
28
src/modules/Elsa.Workflows.Core/Features/FlowchartFeature.cs
Normal file
28
src/modules/Elsa.Workflows.Core/Features/FlowchartFeature.cs
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
using Elsa.Features.Abstractions;
|
||||
using Elsa.Features.Services;
|
||||
using Elsa.Workflows.Core.Activities.Flowchart.Handlers;
|
||||
using Elsa.Workflows.Core.Activities.Flowchart.Implementations;
|
||||
using Elsa.Workflows.Core.Activities.Flowchart.Serialization;
|
||||
using Elsa.Workflows.Core.Activities.Flowchart.Services;
|
||||
using Elsa.Workflows.Core.Activities.Flowchart.Transposition.Services;
|
||||
using Elsa.Workflows.Core.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Elsa.Workflows.Core.Features;
|
||||
|
||||
public class FlowchartFeature : FeatureBase
|
||||
{
|
||||
public FlowchartFeature(IModule module) : base(module)
|
||||
{
|
||||
}
|
||||
|
||||
public override void Apply()
|
||||
{
|
||||
Services
|
||||
.AddSingleton<ITransposeHandlerRegistry, TransposeHandlerRegistry>()
|
||||
.AddSingleton<ITransposeHandler, DefaultTransposeHandler>()
|
||||
.AddSingleton<IActivityNodeDescriber, ActivityNodeDescriber>()
|
||||
.AddSingleton<ISerializationOptionsConfigurator, FlowchartSerializationOptionConfigurator>()
|
||||
;
|
||||
}
|
||||
}
|
||||
|
|
@ -72,8 +72,8 @@ public class WorkflowsFeature : FeatureBase
|
|||
.AddSingleton<IWorkflowExecutionPipeline, WorkflowExecutionPipeline>()
|
||||
|
||||
// Built-in activity services.
|
||||
.AddSingleton<IActivityNodeResolver, OutboundActivityNodeResolver>()
|
||||
.AddSingleton<IActivityNodeResolver, SwitchActivityNodeResolver>()
|
||||
.AddSingleton<IActivityPortResolver, OutboundActivityPortResolver>()
|
||||
.AddSingleton<IActivityPortResolver, SwitchActivityPortResolver>()
|
||||
.AddSingleton<ISerializationOptionsConfigurator, CustomSerializationOptionConfigurator>()
|
||||
|
||||
// Stream providers.
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ namespace Elsa.Workflows.Core.Implementations;
|
|||
|
||||
public class ActivityWalker : IActivityWalker
|
||||
{
|
||||
private readonly IEnumerable<IActivityNodeResolver> _portResolvers;
|
||||
private readonly IEnumerable<IActivityPortResolver> _portResolvers;
|
||||
|
||||
public ActivityWalker(IEnumerable<IActivityNodeResolver> portResolvers)
|
||||
public ActivityWalker(IEnumerable<IActivityPortResolver> portResolvers)
|
||||
{
|
||||
_portResolvers = portResolvers.OrderByDescending(x => x.Priority).ToList();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ using System.Text.Json;
|
|||
using System.Text.Json.Serialization;
|
||||
using Elsa.Workflows.Core.Services;
|
||||
|
||||
namespace Elsa.Workflows.Management.Models;
|
||||
namespace Elsa.Workflows.Core.Models;
|
||||
|
||||
public class ActivityDescriptor
|
||||
{
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
namespace Elsa.Workflows.Management.Models;
|
||||
namespace Elsa.Workflows.Core.Models;
|
||||
|
||||
public enum ActivityKind
|
||||
{
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
namespace Elsa.Workflows.Management.Models;
|
||||
namespace Elsa.Workflows.Core.Models;
|
||||
|
||||
public class InputDescriptor : PropertyDescriptor
|
||||
{
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
namespace Elsa.Workflows.Management.Models;
|
||||
namespace Elsa.Workflows.Core.Models;
|
||||
|
||||
public class OutputDescriptor : PropertyDescriptor
|
||||
{
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
namespace Elsa.Workflows.Management.Models;
|
||||
namespace Elsa.Workflows.Core.Models;
|
||||
|
||||
public class Port
|
||||
{
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
namespace Elsa.Workflows.Management.Models;
|
||||
namespace Elsa.Workflows.Core.Models;
|
||||
|
||||
public abstract class PropertyDescriptor
|
||||
{
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Elsa.Workflows.Core.Activities;
|
||||
using Elsa.Workflows.Core.Services;
|
||||
|
||||
namespace Elsa.Workflows.Core.Serialization.Converters;
|
||||
|
||||
public class FlowchartJsonConverter : JsonConverter<Flowchart>
|
||||
{
|
||||
private readonly IActivityWalker _activityWalker;
|
||||
|
||||
public FlowchartJsonConverter(IActivityWalker activityWalker)
|
||||
{
|
||||
_activityWalker = activityWalker;
|
||||
}
|
||||
|
||||
public override Flowchart? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (!JsonDocument.TryParseValue(ref reader, out var doc))
|
||||
throw new JsonException("Failed to parse JsonDocument");
|
||||
|
||||
var connectionsElement = doc.RootElement.GetProperty("connections");
|
||||
var metadataElement = doc.RootElement.GetProperty("metadata");
|
||||
var activitiesElement = doc.RootElement.GetProperty("activities");
|
||||
var id = doc.RootElement.GetProperty("id").GetString()!;
|
||||
var startId = doc.RootElement.TryGetProperty("start", out var startElement) ? startElement.GetString() : default;
|
||||
var rootActivities = activitiesElement.Deserialize<ICollection<IActivity>>(options) ?? new List<IActivity>();
|
||||
var metadata = metadataElement.Deserialize<IDictionary<string, object>>(options) ?? new Dictionary<string, object>();
|
||||
var start = rootActivities.FirstOrDefault(x => x.Id == startId) ?? rootActivities.FirstOrDefault();
|
||||
var connectionSerializerOptions = new JsonSerializerOptions(options);
|
||||
var activities = WalkActivityTree(rootActivities).ToList();
|
||||
var activityDictionary = activities.ToDictionary(x => x.Id);
|
||||
connectionSerializerOptions.Converters.Add(new ConnectionJsonConverter(activityDictionary));
|
||||
|
||||
var connections = connectionsElement.Deserialize<ICollection<Connection>>(connectionSerializerOptions) ?? new List<Connection>();
|
||||
|
||||
return new Flowchart
|
||||
{
|
||||
Id = id,
|
||||
Metadata = metadata,
|
||||
Activities = rootActivities,
|
||||
Connections = connections,
|
||||
Start = start,
|
||||
};
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, 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 model = new
|
||||
{
|
||||
value.TypeName,
|
||||
value.Id,
|
||||
value.Metadata,
|
||||
value.ApplicationProperties,
|
||||
Start = value.Start?.Id,
|
||||
value.Activities,
|
||||
value.Connections
|
||||
};
|
||||
|
||||
JsonSerializer.Serialize(writer, model, connectionSerializerOptions);
|
||||
}
|
||||
|
||||
private IEnumerable<IActivity> WalkActivityTree(IEnumerable<IActivity> rootActivities) => rootActivities
|
||||
.Select(rootActivity => _activityWalker.Walk(rootActivity))
|
||||
.SelectMany(node => node.Flatten().Select(x => x.Activity));
|
||||
}
|
||||
|
|
@ -21,7 +21,6 @@ public class CustomSerializationOptionConfigurator : ISerializationOptionsConfig
|
|||
public void Configure(JsonSerializerOptions options)
|
||||
{
|
||||
options.Converters.Add(Create<VariableConverter>());
|
||||
options.Converters.Add(Create<FlowchartJsonConverter>());
|
||||
}
|
||||
|
||||
private T Create<T>() => ActivatorUtilities.CreateInstance<T>(_serviceProvider);
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
using System.Reflection;
|
||||
|
||||
namespace Elsa.Workflows.Core.Services;
|
||||
|
||||
/// <summary>
|
||||
/// A node resolver inspects a given activity and returns its outbound "ports". A port is an activity that this activity connects to.
|
||||
/// Node resolvers are used to inspect a workflow structure and build a graph of nodes from it for easy node traversal.
|
||||
/// </summary>
|
||||
public interface IActivityNodeResolver
|
||||
public interface IActivityPortResolver
|
||||
{
|
||||
int Priority { get; }
|
||||
bool GetSupportsActivity(IActivity activity);
|
||||
IEnumerable<IActivity> GetPorts(IActivity activity);
|
||||
IEnumerable<PropertyInfo> GetPorts(Type activityType);
|
||||
}
|
||||
|
|
@ -8,4 +8,5 @@ namespace Elsa.Workflows.Core.Services;
|
|||
public interface IActivityWalker
|
||||
{
|
||||
ActivityNode Walk(IActivity activity);
|
||||
|
||||
}
|
||||
|
|
@ -30,7 +30,7 @@ public class ActivityDescriber : IActivityDescriber
|
|||
var activityAttr = activityType.GetCustomAttribute<ActivityAttribute>();
|
||||
var ns = activityAttr?.Namespace ?? ActivityTypeNameHelper.GenerateNamespace(activityType);
|
||||
var typeName = activityAttr?.TypeName ?? activityType.Name;
|
||||
var fullTypeName = $"{ns}.{typeName}";
|
||||
var fullTypeName = ActivityTypeNameHelper.GenerateTypeName(activityType);
|
||||
var displayNameAttr = activityType.GetCustomAttribute<DisplayNameAttribute>();
|
||||
var displayName = displayNameAttr?.DisplayName ?? activityAttr?.DisplayName ?? typeName.Humanize(LetterCasing.Title);
|
||||
var categoryAttr = activityType.GetCustomAttribute<CategoryAttribute>();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Text.Json;
|
||||
using Elsa.Workflows.Core.Models;
|
||||
using Elsa.Workflows.Core.Services;
|
||||
using Elsa.Workflows.Management.Models;
|
||||
using Elsa.Workflows.Management.Services;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using Elsa.Workflows.Core.Models;
|
||||
using Elsa.Workflows.Management.Models;
|
||||
using Elsa.Workflows.Management.Services;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
using Elsa.Workflows.Core.Models;
|
||||
using Elsa.Workflows.Management.Models;
|
||||
using Elsa.Workflows.Management.Options;
|
||||
using Elsa.Workflows.Management.Services;
|
||||
|
|
@ -23,8 +24,8 @@ public class TypedActivityProvider : IActivityProvider
|
|||
var descriptors = await DescribeActivityTypesAsync(activityTypes, cancellationToken).ToListAsync(cancellationToken);
|
||||
return descriptors;
|
||||
}
|
||||
|
||||
public async IAsyncEnumerable<ActivityDescriptor> DescribeActivityTypesAsync(IEnumerable<Type> activityTypes, [EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
|
||||
private async IAsyncEnumerable<ActivityDescriptor> DescribeActivityTypesAsync(IEnumerable<Type> activityTypes, [EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (var activityType in activityTypes)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Elsa.Workflows.Core.Models;
|
||||
using Elsa.Workflows.Core.Services;
|
||||
using Elsa.Workflows.Management.Models;
|
||||
using Elsa.Workflows.Management.Services;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using Elsa.Workflows.Core.Models;
|
||||
using Elsa.Workflows.Management.Models;
|
||||
|
||||
namespace Elsa.Workflows.Management.Services;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using Elsa.Workflows.Core.Models;
|
||||
using Elsa.Workflows.Core.Services;
|
||||
using Elsa.Workflows.Management.Models;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using Elsa.Workflows.Core.Models;
|
||||
using Elsa.Workflows.Management.Models;
|
||||
|
||||
namespace Elsa.Workflows.Management.Services;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using Elsa.Workflows.Core.Models;
|
||||
using Elsa.Workflows.Management.Models;
|
||||
|
||||
namespace Elsa.Workflows.Management.Services;
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ using Elsa.Scheduling.Extensions;
|
|||
using Elsa.Workflows.Api.Extensions;
|
||||
using Elsa.Workflows.Core;
|
||||
using Elsa.Workflows.Core.Activities;
|
||||
using Elsa.Workflows.Core.Activities.Flowchart.Activities;
|
||||
using Elsa.Workflows.Core.Pipelines.WorkflowExecution.Components;
|
||||
using Elsa.Workflows.Core.Serialization;
|
||||
using Elsa.Workflows.Core.Services;
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ using Elsa.Scheduling.Activities;
|
|||
using Elsa.Scheduling.Extensions;
|
||||
using Elsa.Workflows.Api.Extensions;
|
||||
using Elsa.Workflows.Core.Activities;
|
||||
using Elsa.Workflows.Core.Activities.Flowchart.Activities;
|
||||
using Elsa.Workflows.Management.Extensions;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
using Elsa.Workflows.Core.Activities;
|
||||
using Elsa.Workflows.Core.Activities.Flowchart.Activities;
|
||||
using Elsa.Workflows.Core.Activities.Flowchart.Models;
|
||||
using Elsa.Workflows.Core.Models;
|
||||
using Elsa.Workflows.Core.Services;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue