From 83ae0f351e89147b4021f8c3fb6be57c6fcaa895 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Wed, 16 Apr 2025 08:28:16 +0200 Subject: [PATCH] Add support for connection vertices in flowcharts (#6585) Introduced a `Position` model to represent coordinates. Updated connection models, serializers, and related logic to include vertices, enabling richer flowchart connections. Simplified null initialization and improved collection handling across components. --- .../Extensions/JsonObjectExtensions.cs | 6 +- .../Shared/Models/ActivityDesignerMetadata.cs | 2 +- .../Shared/Models/Connection.cs | 9 +- .../Flowchart/Activities/Flowchart.cs | 388 +++++++++--------- .../Activities/Flowchart/Models/Connection.cs | 27 +- .../Activities/Flowchart/Models/Position.cs | 10 + .../Serialization/ConnectionJsonConverter.cs | 25 +- .../Serialization/FlowchartJsonConverter.cs | 14 +- 8 files changed, 255 insertions(+), 226 deletions(-) create mode 100644 src/modules/Elsa.Workflows.Core/Activities/Flowchart/Models/Position.cs diff --git a/src/clients/Elsa.Api.Client/Extensions/JsonObjectExtensions.cs b/src/clients/Elsa.Api.Client/Extensions/JsonObjectExtensions.cs index b65fcf394..b37f0f742 100644 --- a/src/clients/Elsa.Api.Client/Extensions/JsonObjectExtensions.cs +++ b/src/clients/Elsa.Api.Client/Extensions/JsonObjectExtensions.cs @@ -24,7 +24,7 @@ public static class JsonObjectExtensions /// A representing the specified value. public static JsonNode SerializeToNode(this object value, JsonSerializerOptions? options = null) { - options ??= new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; + options ??= new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; return JsonSerializer.SerializeToNode(value, options)!; } @@ -37,7 +37,7 @@ public static class JsonObjectExtensions /// A representing the specified value. public static JsonArray SerializeToArray(this object value, JsonSerializerOptions? options = null) { - options ??= new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; + options ??= new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; return JsonSerializer.SerializeToNode(value, options)!.AsArray(); } @@ -50,7 +50,7 @@ public static class JsonObjectExtensions /// A representing the specified value. public static JsonArray SerializeToArray(this IEnumerable value, JsonSerializerOptions? options = null) { - options ??= new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; + options ??= new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; return JsonSerializer.SerializeToNode(value, options)!.AsArray(); } diff --git a/src/clients/Elsa.Api.Client/Shared/Models/ActivityDesignerMetadata.cs b/src/clients/Elsa.Api.Client/Shared/Models/ActivityDesignerMetadata.cs index 24e3f6d62..09a401099 100644 --- a/src/clients/Elsa.Api.Client/Shared/Models/ActivityDesignerMetadata.cs +++ b/src/clients/Elsa.Api.Client/Shared/Models/ActivityDesignerMetadata.cs @@ -23,7 +23,7 @@ public class ActivityDesignerMetadata /// The y coordinate of the activity. /// The width of the activity. /// The height of the activity. - public ActivityDesignerMetadata(double x, double y, double width, double height) : this(new Position(x, y), new Size(width, height)) + public ActivityDesignerMetadata(double x, double y, double width, double height) : this(new(x, y), new(width, height)) { } diff --git a/src/clients/Elsa.Api.Client/Shared/Models/Connection.cs b/src/clients/Elsa.Api.Client/Shared/Models/Connection.cs index 6794fb948..632786351 100644 --- a/src/clients/Elsa.Api.Client/Shared/Models/Connection.cs +++ b/src/clients/Elsa.Api.Client/Shared/Models/Connection.cs @@ -8,10 +8,15 @@ public class Connection /// /// Gets or sets the source endpoint. /// - public Endpoint Source { get; set; } = default!; + public Endpoint Source { get; set; } = null!; /// /// Gets or sets the target endpoint. /// - public Endpoint Target { get; set; } = default!; + public Endpoint Target { get; set; } = null!; + + /// + /// Gets or sets the vertices of the connection. + /// + public ICollection Vertices { get; set; } = []; } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.cs b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.cs index 5fbe321b1..03e333b32 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.cs @@ -1,134 +1,134 @@ -using System.ComponentModel; -using System.Runtime.CompilerServices; -using Elsa.Extensions; -using Elsa.Workflows.Activities.Flowchart.Contracts; -using Elsa.Workflows.Activities.Flowchart.Models; -using Elsa.Workflows.Attributes; -using Elsa.Workflows.Options; -using Elsa.Workflows.Signals; - -namespace Elsa.Workflows.Activities.Flowchart.Activities; - -/// -/// A flowchart consists of a collection of activities and connections between them. -/// -[Activity("Elsa", "Flow", "A flowchart is a collection of activities and connections between them.")] -[Browsable(false)] -public class Flowchart : Container -{ +using System.ComponentModel; +using System.Runtime.CompilerServices; +using Elsa.Extensions; +using Elsa.Workflows.Activities.Flowchart.Contracts; +using Elsa.Workflows.Activities.Flowchart.Models; +using Elsa.Workflows.Attributes; +using Elsa.Workflows.Options; +using Elsa.Workflows.Signals; + +namespace Elsa.Workflows.Activities.Flowchart.Activities; + +/// +/// A flowchart consists of a collection of activities and connections between them. +/// +[Activity("Elsa", "Flow", "A flowchart is a collection of activities and connections between them.")] +[Browsable(false)] +public class Flowchart : Container +{ private const string ScopeProperty = "FlowScope"; private const string GraphTransientProperty = "FlowGraph"; private const string BackwardConnectionActivityInput = "BackwardConnection"; - - /// - public Flowchart([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line) - { - OnSignalReceived(OnScheduleOutcomesAsync); - OnSignalReceived(OnScheduleChildActivityAsync); - OnSignalReceived(OnActivityCanceledAsync); - } - - /// - /// The activity to execute when the flowchart starts. - /// - [Port] [Browsable(false)] public IActivity? Start { get; set; } - - /// - /// A list of connections between activities. - /// - public ICollection Connections { get; set; } = new List(); - - /// - protected override async ValueTask ScheduleChildrenAsync(ActivityExecutionContext context) - { - var startActivity = GetStartActivity(context); - - if (startActivity == null) - { - // Nothing else to execute. - await context.CompleteActivityAsync(); - return; - } - - // Schedule the start activity. - await context.ScheduleActivityAsync(startActivity, OnChildCompletedAsync); - } - - private IActivity? GetStartActivity(ActivityExecutionContext context) - { - // If there's a trigger that triggered this workflow, use that. - var triggerActivityId = context.WorkflowExecutionContext.TriggerActivityId; - var triggerActivity = triggerActivityId != null ? Activities.FirstOrDefault(x => x.Id == triggerActivityId) : default; - - if (triggerActivity != null) - return triggerActivity; - - // If an explicit Start activity was provided, use that. - if (Start != null) - return Start; - - // If there is a Start activity on the flowchart, use that. - var startActivity = Activities.FirstOrDefault(x => x is Start); - - if (startActivity != null) - return startActivity; - - // If there's an activity marked as "Can Start Workflow", use that. - var canStartWorkflowActivity = Activities.FirstOrDefault(x => x.GetCanStartWorkflow()); - - if (canStartWorkflowActivity != null) - return canStartWorkflowActivity; - - // If there is a single activity that has no inbound connections, use that. - var root = GetRootActivity(); - - if (root != null) - return root; - - // If no start activity found, return the first activity. - return Activities.FirstOrDefault(); - } - /// - /// Checks if there is any pending work for the flowchart. - /// - private bool HasPendingWork(ActivityExecutionContext context) - { - var workflowExecutionContext = context.WorkflowExecutionContext; - var activityIds = Activities.Select(x => x.Id).ToList(); - var children = context.Children; - var hasRunningActivityInstances = children.Where(x => activityIds.Contains(x.Activity.Id)).Any(x => x.Status == ActivityStatus.Running); - - var hasPendingWork = workflowExecutionContext.Scheduler.List().Any(workItem => - { - var ownerInstanceId = workItem.Owner?.Id; - - if (ownerInstanceId == null) - return false; - - if (ownerInstanceId == context.Id) - return true; - - var ownerContext = context.WorkflowExecutionContext.ActivityExecutionContexts.First(x => x.Id == ownerInstanceId); - var ancestors = ownerContext.GetAncestors().ToList(); - - return ancestors.Any(x => x == context); - }); - - return hasRunningActivityInstances || hasPendingWork; - } - - private IActivity? GetRootActivity() - { - // Get the first activity that has no inbound connections. - var query = - from activity in Activities - let inboundConnections = Connections.Any(x => x.Target.Activity == activity) - where !inboundConnections - select activity; - - var rootActivity = query.FirstOrDefault(); - return rootActivity; + /// + public Flowchart([CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : base(source, line) + { + OnSignalReceived(OnScheduleOutcomesAsync); + OnSignalReceived(OnScheduleChildActivityAsync); + OnSignalReceived(OnActivityCanceledAsync); + } + + /// + /// The activity to execute when the flowchart starts. + /// + [Port] [Browsable(false)] public IActivity? Start { get; set; } + + /// + /// A list of connections between activities. + /// + public ICollection Connections { get; set; } = new List(); + + /// + protected override async ValueTask ScheduleChildrenAsync(ActivityExecutionContext context) + { + var startActivity = GetStartActivity(context); + + if (startActivity == null) + { + // Nothing else to execute. + await context.CompleteActivityAsync(); + return; + } + + // Schedule the start activity. + await context.ScheduleActivityAsync(startActivity, OnChildCompletedAsync); + } + + private IActivity? GetStartActivity(ActivityExecutionContext context) + { + // If there's a trigger that triggered this workflow, use that. + var triggerActivityId = context.WorkflowExecutionContext.TriggerActivityId; + var triggerActivity = triggerActivityId != null ? Activities.FirstOrDefault(x => x.Id == triggerActivityId) : null; + + if (triggerActivity != null) + return triggerActivity; + + // If an explicit Start activity was provided, use that. + if (Start != null) + return Start; + + // If there is a Start activity on the flowchart, use that. + var startActivity = Activities.FirstOrDefault(x => x is Start); + + if (startActivity != null) + return startActivity; + + // If there's an activity marked as "Can Start Workflow", use that. + var canStartWorkflowActivity = Activities.FirstOrDefault(x => x.GetCanStartWorkflow()); + + if (canStartWorkflowActivity != null) + return canStartWorkflowActivity; + + // If there is a single activity that has no inbound connections, use that. + var root = GetRootActivity(); + + if (root != null) + return root; + + // If no start activity found, return the first activity. + return Activities.FirstOrDefault(); + } + + /// + /// Checks if there is any pending work for the flowchart. + /// + private bool HasPendingWork(ActivityExecutionContext context) + { + var workflowExecutionContext = context.WorkflowExecutionContext; + var activityIds = Activities.Select(x => x.Id).ToList(); + var children = context.Children; + var hasRunningActivityInstances = children.Where(x => activityIds.Contains(x.Activity.Id)).Any(x => x.Status == ActivityStatus.Running); + + var hasPendingWork = workflowExecutionContext.Scheduler.List().Any(workItem => + { + var ownerInstanceId = workItem.Owner?.Id; + + if (ownerInstanceId == null) + return false; + + if (ownerInstanceId == context.Id) + return true; + + var ownerContext = context.WorkflowExecutionContext.ActivityExecutionContexts.First(x => x.Id == ownerInstanceId); + var ancestors = ownerContext.GetAncestors().ToList(); + + return ancestors.Any(x => x == context); + }); + + return hasRunningActivityInstances || hasPendingWork; + } + + private IActivity? GetRootActivity() + { + // Get the first activity that has no inbound connections. + var query = + from activity in Activities + let inboundConnections = Connections.Any(x => x.Target.Activity == activity) + where !inboundConnections + select activity; + + var rootActivity = query.FirstOrDefault(); + return rootActivity; } private FlowGraph GetFlowGraph(ActivityExecutionContext context) @@ -142,16 +142,16 @@ public class Flowchart : Container return context.GetProperty(ScopeProperty, () => new FlowScope()); } - private async ValueTask OnChildCompletedAsync(ActivityCompletedContext context) + private async ValueTask OnChildCompletedAsync(ActivityCompletedContext context) { var flowchartContext = context.TargetContext; var completedActivityContext = context.ChildContext; var completedActivity = completedActivityContext.Activity; var result = context.Result; - if (flowchartContext.Activity != this) - { - throw new Exception("Target context activity must be this flowchart"); + if (flowchartContext.Activity != this) + { + throw new Exception("Target context activity must be this flowchart"); } // If the completed activity's status is anything but "Completed", do not schedule its outbound activities. @@ -174,13 +174,13 @@ public class Flowchart : Container var flowGraph = GetFlowGraph(flowchartContext); var flowScope = GetFlowScope(flowchartContext); var completedActivityExcecutedByBackwardConnection = completedActivityContext.ActivityInput.GetValueOrDefault(BackwardConnectionActivityInput); - bool hasScheduledActivity = await ScheduleOutboundActivitiesAsync(flowGraph, flowScope, flowchartContext, completedActivity, outcomes, completedActivityExcecutedByBackwardConnection); + bool hasScheduledActivity = await ScheduleOutboundActivitiesAsync(flowGraph, flowScope, flowchartContext, completedActivity, outcomes, completedActivityExcecutedByBackwardConnection); // If there are not any outbound connections, complete the flowchart activity if there is no other pending work if (!hasScheduledActivity) { await CompleteIfNoPendingWorkAsync(flowchartContext); - } + } } /// @@ -355,65 +355,65 @@ public class Flowchart : Container await activityExecutionContext.CancelActivityAsync(); } } - - private async Task CompleteIfNoPendingWorkAsync(ActivityExecutionContext context) - { - var hasPendingWork = HasPendingWork(context); - - if (!hasPendingWork) - { - var hasFaultedActivities = context.Children.Any(x => x.Status == ActivityStatus.Faulted); - - if (!hasFaultedActivities) - { - await context.CompleteActivityAsync(); - } - } - } - - private async ValueTask OnScheduleOutcomesAsync(ScheduleActivityOutcomes signal, SignalContext context) - { - var flowchartContext = context.ReceiverActivityExecutionContext; - var schedulingActivityContext = context.SenderActivityExecutionContext; - var schedulingActivity = schedulingActivityContext.Activity; - var outcomes = signal.Outcomes; - var outboundConnections = Connections.Where(connection => connection.Source.Activity == schedulingActivity && outcomes.Contains(connection.Source.Port!)).ToList(); - var outboundActivities = outboundConnections.Select(x => x.Target.Activity).ToList(); - - if (outboundActivities.Any()) - { - // Schedule each child. - foreach (var activity in outboundActivities) await flowchartContext.ScheduleActivityAsync(activity, OnChildCompletedAsync); - } - } - - private async ValueTask OnScheduleChildActivityAsync(ScheduleChildActivity signal, SignalContext context) - { - var flowchartContext = context.ReceiverActivityExecutionContext; - var activity = signal.Activity; - var activityExecutionContext = signal.ActivityExecutionContext; - - if (activityExecutionContext != null) - { - await flowchartContext.ScheduleActivityAsync(activityExecutionContext.Activity, new ScheduleWorkOptions - { - ExistingActivityExecutionContext = activityExecutionContext, - CompletionCallback = OnChildCompletedAsync, - Input = signal.Input - }); - } - else - { - await flowchartContext.ScheduleActivityAsync(activity, new ScheduleWorkOptions - { - CompletionCallback = OnChildCompletedAsync, - Input = signal.Input - }); - } - } - - private async ValueTask OnActivityCanceledAsync(CancelSignal signal, SignalContext context) - { - await CompleteIfNoPendingWorkAsync(context.ReceiverActivityExecutionContext); - } + + private async Task CompleteIfNoPendingWorkAsync(ActivityExecutionContext context) + { + var hasPendingWork = HasPendingWork(context); + + if (!hasPendingWork) + { + var hasFaultedActivities = context.Children.Any(x => x.Status == ActivityStatus.Faulted); + + if (!hasFaultedActivities) + { + await context.CompleteActivityAsync(); + } + } + } + + private async ValueTask OnScheduleOutcomesAsync(ScheduleActivityOutcomes signal, SignalContext context) + { + var flowchartContext = context.ReceiverActivityExecutionContext; + var schedulingActivityContext = context.SenderActivityExecutionContext; + var schedulingActivity = schedulingActivityContext.Activity; + var outcomes = signal.Outcomes; + var outboundConnections = Connections.Where(connection => connection.Source.Activity == schedulingActivity && outcomes.Contains(connection.Source.Port!)).ToList(); + var outboundActivities = outboundConnections.Select(x => x.Target.Activity).ToList(); + + if (outboundActivities.Any()) + { + // Schedule each child. + foreach (var activity in outboundActivities) await flowchartContext.ScheduleActivityAsync(activity, OnChildCompletedAsync); + } + } + + private async ValueTask OnScheduleChildActivityAsync(ScheduleChildActivity signal, SignalContext context) + { + var flowchartContext = context.ReceiverActivityExecutionContext; + var activity = signal.Activity; + var activityExecutionContext = signal.ActivityExecutionContext; + + if (activityExecutionContext != null) + { + await flowchartContext.ScheduleActivityAsync(activityExecutionContext.Activity, new ScheduleWorkOptions + { + ExistingActivityExecutionContext = activityExecutionContext, + CompletionCallback = OnChildCompletedAsync, + Input = signal.Input + }); + } + else + { + await flowchartContext.ScheduleActivityAsync(activity, new ScheduleWorkOptions + { + CompletionCallback = OnChildCompletedAsync, + Input = signal.Input + }); + } + } + + private async ValueTask OnActivityCanceledAsync(CancelSignal signal, SignalContext context) + { + await CompleteIfNoPendingWorkAsync(context.ReceiverActivityExecutionContext); + } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Models/Connection.cs b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Models/Connection.cs index 698e3bf3d..8b5c1e281 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Models/Connection.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Models/Connection.cs @@ -33,25 +33,30 @@ public class Connection : IEquatable /// The target endpoint. public Connection(IActivity source, IActivity target) { - Source = new Endpoint(source); - Target = new Endpoint(target); + Source = new(source); + Target = new(target); } /// /// The source endpoint. /// - public Endpoint Source { get; set; } = default!; + public Endpoint Source { get; set; } = null!; /// /// The target endpoint. /// - public Endpoint Target { get; set; } = default!; - - public override string ToString() => - $"{Source.Activity.Id}{(string.IsNullOrEmpty(Source.Port) ? "" : $":{Source.Port}")}->" + - $"{Target.Activity.Id}{(string.IsNullOrEmpty(Target.Port) ? "" : $":{Target.Port}")}"; - - // Implement equality logic + public Endpoint Target { get; set; } = null!; + + /// + /// A collection of points representing the vertices of the connection. + /// + public ICollection Vertices { get; set; } = []; + + public override string ToString() => + $"{Source.Activity.Id}{(string.IsNullOrEmpty(Source.Port) ? "" : $":{Source.Port}")}->" + + $"{Target.Activity.Id}{(string.IsNullOrEmpty(Target.Port) ? "" : $":{Target.Port}")}"; + + // Implement equality logic public bool Equals(Connection? other) { if (other == null) return false; @@ -72,7 +77,7 @@ public class Connection : IEquatable { return e1.Activity.Equals(e2.Activity) && e1.Port == e2.Port; } - + private static int GetEndpointHashCode(Endpoint endpoint) { return HashCode.Combine(endpoint.Activity.GetHashCode(), endpoint.Port?.GetHashCode() ?? 0); diff --git a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Models/Position.cs b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Models/Position.cs new file mode 100644 index 000000000..215e0cf2d --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Models/Position.cs @@ -0,0 +1,10 @@ +using JetBrains.Annotations; + +namespace Elsa.Workflows.Activities.Flowchart.Models; + +[UsedImplicitly] +public class Position +{ + public double X { get; set; } + public double Y { get; set; } +} \ No newline at end of file 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 bdd00ee17..6c4656b1c 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Serialization/ConnectionJsonConverter.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Serialization/ConnectionJsonConverter.cs @@ -29,15 +29,23 @@ public class ConnectionJsonConverter : JsonConverter 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() : default; - var sourcePort = sourceElement.TryGetProperty("port", out var sourcePortValue) ? sourcePortValue.GetString() : default; - var targetPort = targetElement.TryGetProperty("port", out var targetPortValue) ? targetPortValue.GetString() : default; - - var sourceActivity = _activities.TryGetValue(sourceId, out var s) ? s : default!; - var targetActivity = targetId != null ? _activities.TryGetValue(targetId, out var t) ? t : default! : default!; + 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); - return new Connection(source, target); + var verticesElement = doc.RootElement.TryGetProperty("vertices", out var verticesValue) ? verticesValue : default; + var vertices = Array.Empty(); + + if (verticesElement.ValueKind == JsonValueKind.Array) + vertices = verticesElement.Deserialize(options)!; + + return new(source, target) + { + Vertices = vertices + }; } /// @@ -57,7 +65,8 @@ public class ConnectionJsonConverter : JsonConverter { Activity = value.Target.Activity.Id, Port = value.Target.Port - } + }, + Vertices = value.Vertices }; JsonSerializer.Serialize(writer, model, options); 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 827630a42..c2bded0d4 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Serialization/FlowchartJsonConverter.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Serialization/FlowchartJsonConverter.cs @@ -24,9 +24,9 @@ public class FlowchartJsonConverter(IIdentityGenerator identityGenerator, IWellK throw new JsonException("Failed to parse JsonDocument"); var id = doc.RootElement.TryGetProperty("id", out var idAttribute) ? idAttribute.GetString()! : identityGenerator.GenerateId(); - var nodeId = doc.RootElement.TryGetProperty("nodeId", out var nodeIdAttribute) ? nodeIdAttribute.GetString() : default; - var name = doc.RootElement.TryGetProperty("name", out var nameElement) ? nameElement.GetString() : default; - var type = doc.RootElement.TryGetProperty("type", out var typeElement) ? typeElement.GetString() : default; + var nodeId = doc.RootElement.TryGetProperty("nodeId", out var nodeIdAttribute) ? nodeIdAttribute.GetString() : null; + var name = doc.RootElement.TryGetProperty("name", out var nameElement) ? nameElement.GetString() : null; + var type = doc.RootElement.TryGetProperty("type", out var typeElement) ? typeElement.GetString() : null; var version = doc.RootElement.TryGetProperty("version", out var versionElement) ? versionElement.GetInt32() : 1; var connectionsElement = doc.RootElement.TryGetProperty("connections", out var connectionsEl) ? connectionsEl : default; @@ -165,8 +165,8 @@ public class FlowchartJsonConverter(IIdentityGenerator identityGenerator, IWellK connectionSerializerOptions.Converters.Add(new ObsoleteConnectionJsonConverter(activityDictionary)); var obsoleteConnections = connectionsElement.ValueKind != JsonValueKind.Undefined - ? connectionsElement.Deserialize>(connectionSerializerOptions)?.Where(x => x.Source != null! && x.Target != null!).ToList() ?? new List() - : new List(); + ? 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(); } @@ -174,7 +174,7 @@ public class FlowchartJsonConverter(IIdentityGenerator identityGenerator, IWellK connectionSerializerOptions.Converters.Add(new ConnectionJsonConverter(activityDictionary)); return connectionsElement.ValueKind != JsonValueKind.Undefined - ? connectionsElement.Deserialize>(connectionSerializerOptions)?.Where(x => x.Source != null! && x.Target != null!).ToList() ?? new List() - : new List(); + ? connectionsElement.Deserialize>(connectionSerializerOptions)?.Where(x => x.Source != null! && x.Target != null!).ToList() ?? [] + : []; } } \ No newline at end of file