Incremental work on transposition

This commit is contained in:
Sipke Schoorstra 2022-06-11 20:04:16 +02:00
parent d5826c7fd0
commit 8d3aaea1e4
47 changed files with 353 additions and 143 deletions

View file

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

View file

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

View file

@ -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))]

View file

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

View file

@ -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',

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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>()
;
}
}

View file

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

View file

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

View file

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

View file

@ -1,4 +1,4 @@
namespace Elsa.Workflows.Management.Models;
namespace Elsa.Workflows.Core.Models;
public enum ActivityKind
{

View file

@ -1,4 +1,4 @@
namespace Elsa.Workflows.Management.Models;
namespace Elsa.Workflows.Core.Models;
public class InputDescriptor : PropertyDescriptor
{

View file

@ -1,4 +1,4 @@
namespace Elsa.Workflows.Management.Models;
namespace Elsa.Workflows.Core.Models;
public class OutputDescriptor : PropertyDescriptor
{

View file

@ -1,4 +1,4 @@
namespace Elsa.Workflows.Management.Models;
namespace Elsa.Workflows.Core.Models;
public class Port
{

View file

@ -1,4 +1,4 @@
namespace Elsa.Workflows.Management.Models;
namespace Elsa.Workflows.Core.Models;
public abstract class PropertyDescriptor
{

View file

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

View file

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

View file

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

View file

@ -8,4 +8,5 @@ namespace Elsa.Workflows.Core.Services;
public interface IActivityWalker
{
ActivityNode Walk(IActivity activity);
}

View file

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

View file

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

View file

@ -1,3 +1,4 @@
using Elsa.Workflows.Core.Models;
using Elsa.Workflows.Management.Models;
using Elsa.Workflows.Management.Services;

View file

@ -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)
{

View file

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

View file

@ -1,3 +1,4 @@
using Elsa.Workflows.Core.Models;
using Elsa.Workflows.Management.Models;
namespace Elsa.Workflows.Management.Services;

View file

@ -1,3 +1,4 @@
using Elsa.Workflows.Core.Models;
using Elsa.Workflows.Core.Services;
using Elsa.Workflows.Management.Models;

View file

@ -1,3 +1,4 @@
using Elsa.Workflows.Core.Models;
using Elsa.Workflows.Management.Models;
namespace Elsa.Workflows.Management.Services;

View file

@ -1,3 +1,4 @@
using Elsa.Workflows.Core.Models;
using Elsa.Workflows.Management.Models;
namespace Elsa.Workflows.Management.Services;

View file

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

View file

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

View file

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