diff --git a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Serialization/ConnectionJsonConverter.cs b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Serialization/ConnectionJsonConverter.cs index 6c4656b1c..e78aece55 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Serialization/ConnectionJsonConverter.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Serialization/ConnectionJsonConverter.cs @@ -1,4 +1,4 @@ -using System.Text.Json; +using System.Text.Json; using System.Text.Json.Serialization; using Elsa.Workflows.Activities.Flowchart.Models; @@ -26,26 +26,63 @@ public class ConnectionJsonConverter : JsonConverter if (!JsonDocument.TryParseValue(ref reader, out var doc)) throw new JsonException("Failed to parse JsonDocument"); - var sourceElement = doc.RootElement.GetProperty("source"); - var targetElement = doc.RootElement.GetProperty("target"); - var sourceId = sourceElement.GetProperty("activity").GetString()!; - var targetId = targetElement.TryGetProperty("activity", out var targetIdValue) ? targetIdValue.GetString() : null; - var sourcePort = sourceElement.TryGetProperty("port", out var sourcePortValue) ? sourcePortValue.GetString() : null; - var targetPort = targetElement.TryGetProperty("port", out var targetPortValue) ? targetPortValue.GetString() : null; - var sourceActivity = _activities.TryGetValue(sourceId, out var s) ? s : null!; - var targetActivity = targetId != null ? _activities.TryGetValue(targetId, out var t) ? t : null! : null!; - var source = new Endpoint(sourceActivity, sourcePort); - var target = new Endpoint(targetActivity, targetPort); - var verticesElement = doc.RootElement.TryGetProperty("vertices", out var verticesValue) ? verticesValue : default; - var vertices = Array.Empty(); + var root = doc.RootElement; - if (verticesElement.ValueKind == JsonValueKind.Array) - vertices = verticesElement.Deserialize(options)!; - - return new(source, target) + // case‐insensitive get + JsonElement Get(string name) { - Vertices = vertices - }; + if (root.TryGetProperty(name, out var e)) + return e; + var alt = char.ToUpperInvariant(name[0]) + name.Substring(1); + if (root.TryGetProperty(alt, out e)) + return e; + throw new JsonException($"Missing property '{name}' or '{alt}'"); + } + + var sourceElement = Get("source"); + var targetElement = Get("target"); + + // now inside sourceElement and targetElement, their children + // are again PascalCased (“Activity”, “Port”), so do the same thing: + + string GetId(JsonElement container, string propName) + { + if (container.TryGetProperty(propName, out var p)) + return p.GetString()!; + var alt = char.ToUpperInvariant(propName[0]) + propName.Substring(1); + return container.GetProperty(alt).GetString()!; + } + + string? GetPort(JsonElement container, string propName) + { + if (container.TryGetProperty(propName, out var p)) + return p.GetString(); + var alt = char.ToUpperInvariant(propName[0]) + propName.Substring(1); + return container.TryGetProperty(alt, out p) ? p.GetString() : null; + } + + var sourceId = GetId(sourceElement, "activity"); + var targetId = GetPort(targetElement, "activity"); // note: this could be null + var sourcePort = GetPort(sourceElement, "port"); + var targetPort = GetPort(targetElement, "port"); + + var sourceAct = _activities.TryGetValue(sourceId, out var s) ? s : throw new JsonException($"Unknown activity ID '{sourceId}'"); + var targetAct = + targetId != null + ? _activities.TryGetValue(targetId, out var t) + ? t + : throw new JsonException($"Unknown activity ID '{targetId}'") + : null; + + var source = new Endpoint(sourceAct, sourcePort); + var target = new Endpoint(targetAct!, targetPort); + + // vertices is already correct: + var vertices = Array.Empty(); + if (doc.RootElement.TryGetProperty("vertices", out var vertsEl) && vertsEl.ValueKind == JsonValueKind.Array) + vertices = vertsEl.Deserialize(options)!; + + return new Connection(source, target) { Vertices = vertices }; } /// @@ -53,22 +90,14 @@ public class ConnectionJsonConverter : JsonConverter { if (value.Source.Activity == null! || value.Target.Activity == null!) return; - + var model = new { - Source = new - { - Activity = value.Source.Activity.Id, - Port = value.Source.Port - }, - Target = new - { - Activity = value.Target.Activity.Id, - Port = value.Target.Port - }, - Vertices = value.Vertices + Source = new { Activity = value.Source.Activity.Id, Port = value.Source.Port }, + Target = new { Activity = value.Target.Activity.Id, Port = value.Target.Port }, + Vertices = value.Vertices, }; JsonSerializer.Serialize(writer, model, options); } -} \ No newline at end of file +} 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 c2bded0d4..4e0053ae4 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Serialization/FlowchartJsonConverter.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Serialization/FlowchartJsonConverter.cs @@ -1,4 +1,4 @@ -using System.Text.Json; +using System.Text.Json; using System.Text.Json.Serialization; using Elsa.Expressions.Contracts; using Elsa.Extensions; @@ -16,7 +16,7 @@ public class FlowchartJsonConverter(IIdentityGenerator identityGenerator, IWellK private const string AllActivitiesKey = "allActivities"; private const string AllConnectionsKey = "allConnections"; private const string NotFoundConnectionsKey = "notFoundConnections"; - + /// public override Activities.Flowchart Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { @@ -109,7 +109,12 @@ public class FlowchartJsonConverter(IIdentityGenerator identityGenerator, IWellK { var customPropertiesElement = rootElement.TryGetProperty("customProperties", out var customPropertiesEl) ? customPropertiesEl : default; - var notFoundConnectionsElement = customPropertiesElement.ValueKind != JsonValueKind.Undefined ? customPropertiesElement.TryGetProperty(NotFoundConnectionsKey, out var notFoundConnectionsEl) ? notFoundConnectionsEl : default : default; + var notFoundConnectionsElement = + customPropertiesElement.ValueKind != JsonValueKind.Undefined + ? customPropertiesElement.TryGetProperty(NotFoundConnectionsKey, out var notFoundConnectionsEl) + ? notFoundConnectionsEl + : default + : default; var notFoundConnections = notFoundConnectionsElement.ValueKind != JsonValueKind.Undefined ? DeserializeConnections(notFoundConnectionsElement, activities, connectionSerializerOptions) : new List(); // Add connections of NotFoundActivity to the list if they aren't already in it. @@ -141,7 +146,8 @@ public class FlowchartJsonConverter(IIdentityGenerator identityGenerator, IWellK var target = foundActivities.FirstOrDefault(x => x.Id == missingTarget.Activity?.Id); // ReSharper restore ConditionalAccessQualifierIsNonNullableAccordingToAPIContract - if (source == null || target == null) continue; + if (source == null || target == null) + continue; var connection = new Connection(new Endpoint(source, missingSource.Port), new Endpoint(target, missingTarget.Port)); connectionsThatCanBeRestored.Add(connection); @@ -152,29 +158,59 @@ public class FlowchartJsonConverter(IIdentityGenerator identityGenerator, IWellK private static ICollection DeserializeConnections(JsonElement connectionsElement, IDictionary activityDictionary, JsonSerializerOptions options) { - if (connectionsElement.ValueKind == JsonValueKind.Undefined) + // 1) Nothing → empty + if (connectionsElement.ValueKind == JsonValueKind.Undefined || connectionsElement.ValueKind == JsonValueKind.Null) return new List(); - // To not break existing workflow definitions, we need to support the old connection format. - var useOldConnectionConverter = connectionsElement.EnumerateArray().Any(x => x.TryGetProperty("sourcePort", out var sourcePort) && sourcePort.ValueKind == JsonValueKind.String); - - var connectionSerializerOptions = new JsonSerializerOptions(options); - - if (useOldConnectionConverter) + // 2) OData‐style wrapper: { "$values": [ … ] } + if (connectionsElement.ValueKind == JsonValueKind.Object && connectionsElement.TryGetProperty("$values", out var valuesEl) && valuesEl.ValueKind == JsonValueKind.Array) { - connectionSerializerOptions.Converters.Add(new ObsoleteConnectionJsonConverter(activityDictionary)); - - var obsoleteConnections = connectionsElement.ValueKind != JsonValueKind.Undefined - ? connectionsElement.Deserialize>(connectionSerializerOptions)?.Where(x => x.Source != null! && x.Target != null!).ToList() ?? [] - : []; - - return obsoleteConnections.Select(x => new Connection(new Endpoint(x.Source, x.SourcePort), new Endpoint(x.Target, x.TargetPort))).ToList(); + connectionsElement = valuesEl; + } + // 3) Single‐object (old style): wrap into a 1‑element array if it has a "source" property + else if (connectionsElement.ValueKind == JsonValueKind.Object && connectionsElement.TryGetProperty("source", out _)) + { + using var tmp = JsonDocument.Parse($"[{connectionsElement.GetRawText()}]"); + connectionsElement = tmp.RootElement; } - connectionSerializerOptions.Converters.Add(new ConnectionJsonConverter(activityDictionary)); + // 4) If it’s still not an array, bail + if (connectionsElement.ValueKind != JsonValueKind.Array) + return new List(); - return connectionsElement.ValueKind != JsonValueKind.Undefined - ? connectionsElement.Deserialize>(connectionSerializerOptions)?.Where(x => x.Source != null! && x.Target != null!).ToList() ?? [] - : []; + // Shortcut: detect the classic flat‐connection JSON and parse manually + var arr = connectionsElement.EnumerateArray().ToArray(); + if (arr.Length > 0 && arr[0].TryGetProperty("source", out var srcProp) && srcProp.ValueKind == JsonValueKind.String && arr[0].TryGetProperty("target", out var tgtProp) && tgtProp.ValueKind == JsonValueKind.String) + { + var list = new List(); + + foreach (var el in arr) + { + var srcId = el.GetProperty("source").GetString()!; + var tgtId = el.GetProperty("target").GetString()!; + var srcPort = el.TryGetProperty("sourcePort", out var sp) && sp.ValueKind == JsonValueKind.String ? sp.GetString() : null; + var tgtPort = el.TryGetProperty("targetPort", out var tp) && tp.ValueKind == JsonValueKind.String ? tp.GetString() : null; + + var srcAct = activityDictionary[srcId]; + var tgtAct = activityDictionary[tgtId]; + list.Add(new Connection(new Endpoint(srcAct, srcPort), new Endpoint(tgtAct, tgtPort))); + } + + return list; + } + + // Otherwise, it's an array of nested‐object connections → delegate to your converters + var serializer = new JsonSerializerOptions(options); + + // Legacy check: look for "sourcePort" on the first item to choose the old converter + if (arr.Length > 0 && arr[0].TryGetProperty("sourcePort", out _)) + serializer.Converters.Add(new ObsoleteConnectionJsonConverter(activityDictionary)); + else + serializer.Converters.Add(new ConnectionJsonConverter(activityDictionary)); + + var raw = connectionsElement.Deserialize>(serializer) ?? new List(); + + // drop any half‑baked entries + return raw.Where(c => c.Source?.Activity != null && c.Target?.Activity != null).ToList(); } -} \ No newline at end of file +} diff --git a/src/modules/Elsa.Workflows.Core/Services/ActivityFactory.cs b/src/modules/Elsa.Workflows.Core/Services/ActivityFactory.cs index c370a4e0b..081550a0d 100644 --- a/src/modules/Elsa.Workflows.Core/Services/ActivityFactory.cs +++ b/src/modules/Elsa.Workflows.Core/Services/ActivityFactory.cs @@ -1,4 +1,5 @@ -using System.Text.Json; +using System.Text.Json; +using System.Text.Json.Nodes; using Elsa.Expressions.Helpers; using Elsa.Extensions; using Elsa.Workflows.Memory; @@ -10,28 +11,120 @@ namespace Elsa.Workflows; /// public class ActivityFactory : IActivityFactory { - /// public IActivity Create(Type type, ActivityConstructorContext context) { - // Backwards compatibility for older JSON schemas. - var canStartWorkflow = GetBoolean(context.Element, "canStartWorkflow"); - var runAsynchronously = GetBoolean(context.Element, "runAsynchronously"); - var activityElement = context.Element; - var activityDescriptor = context.ActivityDescriptor; - var activity = (IActivity)context.Element.Deserialize(type, context.SerializerOptions)!; - var composite = activity as IComposite; + // 1) Grab the raw text + var raw = context.Element.GetRawText(); - composite?.Setup(); + // 2) Parse into a JsonNode so we can mutate + var node = JsonNode.Parse(raw)!; - ReadSyntheticInputs(activityDescriptor, activity, activityElement, context.SerializerOptions); - ReadSyntheticOutputs(activityDescriptor, activity, activityElement); - + // 3) Strip out any "_type" metadata wrappers + StripTypeMetadata(node); + + // 4) Get a cleaned JSON string + var cleanedJson = node.ToJsonString(new JsonSerializerOptions(JsonSerializerDefaults.Web)); + + // 5) Parse it back to a JsonDocument so we can get a clean JsonElement + using var doc = JsonDocument.Parse(cleanedJson); + var cleanedElement = doc.RootElement.Clone(); + + // 6) Deserialize into your IActivity + var activity = (IActivity)JsonSerializer.Deserialize(cleanedJson, type, context.SerializerOptions)!; + + // 7) Pull out your boolean flags from the cleaned element + var canStartWorkflow = GetBoolean(cleanedElement, "canStartWorkflow"); + var runAsynchronously = GetBoolean(cleanedElement, "runAsynchronously"); + + // 8) If composite, setup + if (activity is IComposite composite) + composite.Setup(); + + // 9) Your existing synthetic inputs/outputs routines, using the cleanedElement + ReadSyntheticInputs(context.ActivityDescriptor, activity, cleanedElement, context.SerializerOptions); + ReadSyntheticOutputs(context.ActivityDescriptor, activity, cleanedElement); + + // 10) Finally re‑apply those flags activity.SetCanStartWorkflow(canStartWorkflow); activity.SetRunAsynchronously(runAsynchronously); return activity; } - + + /// + /// Recursively remove any "_type" properties and unwrap any + /// { "_type": "...", "items": [ ... ] } or "values": [ ... ] wrappers + /// + private void StripTypeMetadata(JsonNode node) + { + switch (node) + { + case JsonObject obj: + // First recurse into each child + foreach (var key in obj.Select(kvp => kvp.Key).ToList()) + { + var child = obj[key]; + if (child is JsonObject wrapper && wrapper.ContainsKey("_type")) + { + // If it has an "items" array, replace the whole property with that array + if (wrapper["items"] is JsonArray items) + { + obj[key] = items; + StripTypeMetadata(items); + continue; + } + + // Or a "values" array + if (wrapper["values"] is JsonArray values) + { + obj[key] = values; + StripTypeMetadata(values); + continue; + } + } + + // Otherwise just recurse normally + if (child != null) + StripTypeMetadata(child); + } + + // And remove any stray _type on this object + obj.Remove("_type"); + break; + + case JsonArray arr: + for (var i = 0; i < arr.Count; i++) + { + var element = arr[i]; + if (element is JsonObject w && w.ContainsKey("_type")) + { + if (w["items"] is JsonArray items) + { + arr[i] = items; + StripTypeMetadata(items); + continue; + } + + if (w["values"] is JsonArray values) + { + arr[i] = values; + StripTypeMetadata(values); + continue; + } + } + + if (element != null) + StripTypeMetadata(element); + } + + break; + + // primitives—nothing to do + default: + break; + } + } + private void ReadSyntheticInputs(ActivityDescriptor activityDescriptor, IActivity activity, JsonElement activityRoot, JsonSerializerOptions options) { foreach (var inputDescriptor in activityDescriptor.Inputs.Where(x => x.IsSynthetic)) @@ -41,9 +134,9 @@ public class ActivityFactory : IActivityFactory var nakedType = inputDescriptor.Type; var wrappedType = typeof(Input<>).MakeGenericType(nakedType); - if (!activityRoot.TryGetProperty(propertyName, out var propertyElement) || propertyElement.ValueKind == JsonValueKind.Null || propertyElement.ValueKind == JsonValueKind.Undefined) + if (!activityRoot.TryGetProperty(propertyName, out var propertyElement) || propertyElement.ValueKind == JsonValueKind.Null || propertyElement.ValueKind == JsonValueKind.Undefined) continue; - + var isWrapped = propertyElement.ValueKind == JsonValueKind.Object && propertyElement.GetProperty("typeName").ValueKind != JsonValueKind.Undefined; if (isWrapped) @@ -77,10 +170,7 @@ public class ActivityFactory : IActivityFactory if (!memoryReferenceElement.TryGetProperty("id", out var memoryReferenceIdElement)) continue; - var variable = new Variable - { - Id = memoryReferenceIdElement.GetString()! - }; + var variable = new Variable { Id = memoryReferenceIdElement.GetString()! }; variable.Name = variable.Id; var output = Activator.CreateInstance(wrappedType, variable)!; @@ -107,4 +197,4 @@ public class ActivityFactory : IActivityFactory return false; } -} \ No newline at end of file +}