From 3bad8a5b1b383568c98151c02670ec9b7e71b854 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Mon, 12 Feb 2024 18:47:25 +0100 Subject: [PATCH 1/6] Improve Elsa workflow expression serialization (#4930) * Improve Elsa workflow expression serialization Added serialization support for expressions in Elsa workflows, enabling serialization and deserialization to maintain consistent types across sessions. Updated relevant test cases for validation. * Remove PR workflow from GitHub actions The PR workflow has been removed from GitHub actions. --- .github/workflows/pr.yml | 43 ------------ Elsa.sln | 1 - .../Helpers/ObjectConverter.cs | 1 - .../Elsa.Expressions/LiteralExpression.cs | 2 + .../Models/ExpressionDescriptor.cs | 5 ++ .../Models/ExpressionSerializationContext.cs | 5 ++ .../Expressions/VariableExpressionHandler.cs | 1 + .../Converters/InputJsonConverter.cs | 28 +++----- .../DefaultExpressionDescriptorProvider.cs | 65 +++++++++++++++++-- .../VariableExpressions/Activities.cs | 41 ++++++++++++ .../VariableExpressions/Tests.cs | 16 ++++- .../VariableExpressions/Workflows.cs | 21 ++++-- 12 files changed, 149 insertions(+), 80 deletions(-) delete mode 100644 .github/workflows/pr.yml create mode 100644 src/modules/Elsa.Expressions/Models/ExpressionSerializationContext.cs create mode 100644 test/integration/Elsa.Workflows.IntegrationTests/Serialization/VariableExpressions/Activities.cs diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml deleted file mode 100644 index dd4b9ee41..000000000 --- a/.github/workflows/pr.yml +++ /dev/null @@ -1,43 +0,0 @@ -# ------------------------------------------------------------------------------ -# -# -# This code was generated. -# -# - To turn off auto-generation set: -# -# [CustomGitHubActions (AutoGenerate = false)] -# -# - To trigger manual generation invoke: -# -# nuke --generate-configuration GitHubActions_pr --host GitHubActions -# -# -# ------------------------------------------------------------------------------ - -name: pr - -on: - pull_request: - branches: - - main - paths: - - '**/*' - - '!docs/**/*' - - '!package.json' - - '!package-lock.json' - - '!readme.md' - -jobs: - ubuntu-latest: - name: ubuntu-latest - runs-on: ubuntu-latest - steps: - - if: ${{ runner.os == 'Windows' }} - name: 'Use GNU tar' - shell: cmd - run: | - echo "Adding GNU tar to PATH" - echo C:\Program Files\Git\usr\bin>>"%GITHUB_PATH%" - - uses: actions/checkout@v3 - - name: 'Run: Compile, Test, Pack' - run: ./build.cmd Compile Test Pack diff --git a/Elsa.sln b/Elsa.sln index e1a6d9796..b0c0f47e8 100644 --- a/Elsa.sln +++ b/Elsa.sln @@ -292,7 +292,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "pipelines", "pipelines", "{ .github\workflows\elsa-server.yml = .github\workflows\elsa-server.yml .github\workflows\elsa-studio.yml = .github\workflows\elsa-studio.yml .github\workflows\packages.yml = .github\workflows\packages.yml - .github\workflows\pr.yml = .github\workflows\pr.yml EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "_build", "build\_build.csproj", "{99F2B1DA-2F69-4D70-A2A3-AC985AD91EC4}" diff --git a/src/modules/Elsa.Expressions/Helpers/ObjectConverter.cs b/src/modules/Elsa.Expressions/Helpers/ObjectConverter.cs index 888060fd1..677f09ac0 100644 --- a/src/modules/Elsa.Expressions/Helpers/ObjectConverter.cs +++ b/src/modules/Elsa.Expressions/Helpers/ObjectConverter.cs @@ -1,6 +1,5 @@ using System.Collections; using System.ComponentModel; -using System.Diagnostics.CodeAnalysis; using System.Dynamic; using System.Globalization; using System.Text.Json; diff --git a/src/modules/Elsa.Expressions/LiteralExpression.cs b/src/modules/Elsa.Expressions/LiteralExpression.cs index b663b62be..7ddbf58fe 100644 --- a/src/modules/Elsa.Expressions/LiteralExpression.cs +++ b/src/modules/Elsa.Expressions/LiteralExpression.cs @@ -1,10 +1,12 @@ using Elsa.Expressions.Contracts; using Elsa.Expressions.Helpers; using Elsa.Expressions.Models; +using JetBrains.Annotations; namespace Elsa.Expressions; /// +[UsedImplicitly] public class LiteralExpressionHandler : IExpressionHandler { private readonly IWellKnownTypeRegistry _wellKnownTypeRegistry; diff --git a/src/modules/Elsa.Expressions/Models/ExpressionDescriptor.cs b/src/modules/Elsa.Expressions/Models/ExpressionDescriptor.cs index 7314c78a5..8f4220660 100644 --- a/src/modules/Elsa.Expressions/Models/ExpressionDescriptor.cs +++ b/src/modules/Elsa.Expressions/Models/ExpressionDescriptor.cs @@ -41,4 +41,9 @@ public class ExpressionDescriptor /// Gets or sets the memory block reference factory. /// public Func MemoryBlockReferenceFactory { get; set; } = () => new MemoryBlockReference(); + + /// + /// Gets or sets the expression deserialization function. + /// + public Func Deserialize { get; set; } = default!; } \ No newline at end of file diff --git a/src/modules/Elsa.Expressions/Models/ExpressionSerializationContext.cs b/src/modules/Elsa.Expressions/Models/ExpressionSerializationContext.cs new file mode 100644 index 000000000..f366531b4 --- /dev/null +++ b/src/modules/Elsa.Expressions/Models/ExpressionSerializationContext.cs @@ -0,0 +1,5 @@ +using System.Text.Json; + +namespace Elsa.Expressions.Models; + +public record ExpressionSerializationContext(JsonElement JsonElement, JsonSerializerOptions Options, Type MemoryBlockType); \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Expressions/VariableExpressionHandler.cs b/src/modules/Elsa.Workflows.Core/Expressions/VariableExpressionHandler.cs index 358f94d67..195572339 100644 --- a/src/modules/Elsa.Workflows.Core/Expressions/VariableExpressionHandler.cs +++ b/src/modules/Elsa.Workflows.Core/Expressions/VariableExpressionHandler.cs @@ -1,3 +1,4 @@ +using System.Text.Json; using Elsa.Expressions.Contracts; using Elsa.Expressions.Models; using Elsa.Workflows.Memory; diff --git a/src/modules/Elsa.Workflows.Core/Serialization/Converters/InputJsonConverter.cs b/src/modules/Elsa.Workflows.Core/Serialization/Converters/InputJsonConverter.cs index f7d194fca..d259b0deb 100644 --- a/src/modules/Elsa.Workflows.Core/Serialization/Converters/InputJsonConverter.cs +++ b/src/modules/Elsa.Workflows.Core/Serialization/Converters/InputJsonConverter.cs @@ -39,27 +39,17 @@ public class InputJsonConverter : JsonConverter> var expressionElement = doc.RootElement.TryGetProperty("expression", out var expressionElementValue) ? expressionElementValue : default; var expressionTypeNameElement = expressionElement.ValueKind != JsonValueKind.Undefined ? expressionElement.TryGetProperty("type", out var expressionTypeNameElementValue) ? expressionTypeNameElementValue : default : default; - var expressionTypeName = expressionTypeNameElement.ValueKind != JsonValueKind.Undefined ? expressionTypeNameElement.GetString() ?? "Literal" : "Literal"; - var expressionDescriptor = _expressionDescriptorRegistry.Find(expressionTypeName); - var memoryBlockReference = expressionDescriptor?.MemoryBlockReferenceFactory(); - var memoryBlockReferenceType = memoryBlockReference?.GetType(); - var expressionValueElement = expressionElement.TryGetProperty("value", out var expressionElementValueValue) ? expressionElementValueValue : default; - - var expressionValue = expressionValueElement.ValueKind switch - { - JsonValueKind.String => expressionValueElement.GetString(), - JsonValueKind.False => false, - JsonValueKind.True => true, - JsonValueKind.Number => expressionValueElement.GetDouble(), - JsonValueKind.Undefined => default, - _ => memoryBlockReferenceType != null ? expressionValueElement.Deserialize(memoryBlockReferenceType, options)! : default - }; - - var expression = new Expression(expressionTypeName, expressionValue); + var expressionTypeName = expressionTypeNameElement.ValueKind != JsonValueKind.Undefined ? expressionTypeNameElement.GetString() ?? "Literal" : default; + var expressionDescriptor = expressionTypeName != null ? _expressionDescriptorRegistry.Find(expressionTypeName) : default; + var memoryBlockReference = expressionDescriptor?.MemoryBlockReferenceFactory?.Invoke(); if (memoryBlockReference == null) return default!; + var memoryBlockType = memoryBlockReference.GetType(); + var context = new ExpressionSerializationContext(expressionElement, options, memoryBlockType); + var expression = expressionDescriptor!.Deserialize(context); + return (Input)Activator.CreateInstance(typeof(Input), expression, memoryBlockReference)!; } @@ -73,10 +63,10 @@ public class InputJsonConverter : JsonConverter> var expression = value.Expression; var expressionType = expression?.Type; var expressionDescriptor = expressionType != null ? _expressionDescriptorRegistry.Find(expressionType) : default; - + if (expressionDescriptor == null) throw new JsonException($"Could not find an expression descriptor for expression type '{expressionType}'."); - + var targetType = value.Type; var expressionValue = expressionDescriptor.IsSerializable ? expression : null; diff --git a/src/modules/Elsa.Workflows.Management/Providers/DefaultExpressionDescriptorProvider.cs b/src/modules/Elsa.Workflows.Management/Providers/DefaultExpressionDescriptorProvider.cs index d27da2b21..dd434f19f 100644 --- a/src/modules/Elsa.Workflows.Management/Providers/DefaultExpressionDescriptorProvider.cs +++ b/src/modules/Elsa.Workflows.Management/Providers/DefaultExpressionDescriptorProvider.cs @@ -1,3 +1,4 @@ +using System.Text.Json; using Elsa.Expressions; using Elsa.Expressions.Contracts; using Elsa.Expressions.Models; @@ -21,35 +22,85 @@ public class DefaultExpressionDescriptorProvider : IExpressionDescriptorProvider yield return CreateVariableDescriptor(); } - private ExpressionDescriptor CreateLiteralDescriptor() => CreateDescriptor("Literal", "Literal", isBrowsable: false); + private ExpressionDescriptor CreateLiteralDescriptor() + { + return CreateDescriptor( + "Literal", + "Literal", + isBrowsable: false, + memoryBlockReferenceFactory: () => new Literal(), + deserialize: (context) => + { + var elementValue = context.JsonElement.TryGetProperty("value", out var v) ? v : default; + + var value = (object?)(elementValue.ValueKind switch + { + JsonValueKind.String => elementValue.GetString(), + JsonValueKind.Number => elementValue.GetDecimal(), + JsonValueKind.True => true, + JsonValueKind.False => false, + _ => v.GetString() + }); + + return new Expression("Literal", value); + }); + } + private ExpressionDescriptor CreateObjectDescriptor() => CreateDescriptor("Object", "Object", monacoLanguage: "json", isBrowsable: false); [Obsolete("Use Object instead.")] private ExpressionDescriptor CreateJsonDescriptor() => CreateDescriptor("Json", "Json", monacoLanguage: "json", isBrowsable: false); private ExpressionDescriptor CreateDelegateDescriptor() => CreateDescriptor("Delegate", "Delegate", false, false); - private ExpressionDescriptor CreateVariableDescriptor() => CreateDescriptor("Variable", "Variable", isBrowsable: false, memoryBlockReferenceFactory: () => new Variable()); + + private ExpressionDescriptor CreateVariableDescriptor() + { + return CreateDescriptor( + "Variable", + "Variable", + isBrowsable: false, + memoryBlockReferenceFactory: () => new Variable(), + deserialize: context => + { + var expressionValueElement = context.JsonElement.TryGetProperty("value", out var expressionElementValueValue) ? expressionElementValueValue : default; + var expressionValue = expressionValueElement.Deserialize(context.MemoryBlockType, context.Options); + return new Expression("Variable", expressionValue); + } + ); + } private static ExpressionDescriptor CreateDescriptor( - string type, + string expressionType, string displayName, bool isSerializable = true, bool isBrowsable = true, string? monacoLanguage = null, - Func? memoryBlockReferenceFactory = default) where THandler : IExpressionHandler + Func? memoryBlockReferenceFactory = default, + Func? deserialize = default) + where THandler : IExpressionHandler { var descriptor = new ExpressionDescriptor { - Type = type, + Type = expressionType, DisplayName = displayName, IsSerializable = isSerializable, IsBrowsable = isBrowsable, HandlerFactory = sp => ActivatorUtilities.GetServiceOrCreateInstance(sp), - MemoryBlockReferenceFactory = memoryBlockReferenceFactory ?? (() => new MemoryBlockReference()) + MemoryBlockReferenceFactory = memoryBlockReferenceFactory ?? (() => new MemoryBlockReference()), + Deserialize = deserialize ?? + (context => + { + return context.JsonElement.ValueKind == JsonValueKind.Object + ? context.JsonElement.Deserialize((JsonSerializerOptions?)context.Options)! + : new Expression(expressionType, null!); + }) }; if (monacoLanguage != null) - descriptor.Properties = new { MonacoLanguage = monacoLanguage }.ToDictionary(); + descriptor.Properties = new + { + MonacoLanguage = monacoLanguage + }.ToDictionary(); return descriptor; } diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Serialization/VariableExpressions/Activities.cs b/test/integration/Elsa.Workflows.IntegrationTests/Serialization/VariableExpressions/Activities.cs new file mode 100644 index 000000000..6f76afac7 --- /dev/null +++ b/test/integration/Elsa.Workflows.IntegrationTests/Serialization/VariableExpressions/Activities.cs @@ -0,0 +1,41 @@ +using System.Text.Json.Serialization; +using Elsa.Expressions.Models; +using Elsa.Extensions; +using Elsa.Workflows.Memory; +using Elsa.Workflows.Models; + +namespace Elsa.Workflows.IntegrationTests.Serialization.VariableExpressions; + +/// +public class NumberActivity : CodeActivity +{ + /// + [JsonConstructor] + public NumberActivity() + { + } + + /// + public NumberActivity(Variable variable) + { + Number = new(variable); + } + + /// + public NumberActivity(Literal literal) + { + Number = new(literal); + } + + /// + /// Gets or sets the number. + /// + public Input Number { get; set; } = default!; + + /// + protected override void Execute(ActivityExecutionContext context) + { + var number = Number.Get(context); + Console.WriteLine(number.ToString()); + } +} \ No newline at end of file diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Serialization/VariableExpressions/Tests.cs b/test/integration/Elsa.Workflows.IntegrationTests/Serialization/VariableExpressions/Tests.cs index 01cc37560..71068a82e 100644 --- a/test/integration/Elsa.Workflows.IntegrationTests/Serialization/VariableExpressions/Tests.cs +++ b/test/integration/Elsa.Workflows.IntegrationTests/Serialization/VariableExpressions/Tests.cs @@ -18,23 +18,35 @@ public class Tests { private readonly IWorkflowSerializer _workflowSerializer; private readonly IWorkflowBuilder _workflowBuilder; + private readonly IWorkflowRunner _workflowRunner; + /// + /// Initializes a new instance of the class. + /// public Tests(ITestOutputHelper testOutputHelper) { var serviceProvider = new TestApplicationBuilder(testOutputHelper).Build(); _workflowSerializer = serviceProvider.GetRequiredService(); IWorkflowBuilderFactory workflowBuilderFactory = serviceProvider.GetRequiredService(); _workflowBuilder = workflowBuilderFactory.CreateBuilder(); + _workflowRunner = serviceProvider.GetRequiredService(); } + /// + /// Variable types remain intact after serialization. + /// [Fact(DisplayName = "Variable types remain intact after serialization")] public async Task Test1() { var workflow = await _workflowBuilder.BuildWorkflowAsync(); var serialized = _workflowSerializer.Serialize(workflow); var deserializedWorkflow = _workflowSerializer.Deserialize(serialized); - var rehydratedWriteLine = (WriteLine)((Flowchart)deserializedWorkflow.Root).Activities.ElementAt(0); + var rehydratedWriteLine1 = (WriteLine)((Sequence)deserializedWorkflow.Root).Activities.ElementAt(0); + var rehydratedNumberActivity1 = (NumberActivity)((Sequence)deserializedWorkflow.Root).Activities.ElementAt(2); - Assert.IsType>(rehydratedWriteLine.Text.Expression!.Value); + Assert.IsType>(rehydratedWriteLine1.Text.Expression!.Value); + Assert.IsType>(rehydratedNumberActivity1.Number.Expression!.Value); + + await _workflowRunner.RunAsync(workflow); } } \ No newline at end of file diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Serialization/VariableExpressions/Workflows.cs b/test/integration/Elsa.Workflows.IntegrationTests/Serialization/VariableExpressions/Workflows.cs index 3d13a59fa..2de1b523c 100644 --- a/test/integration/Elsa.Workflows.IntegrationTests/Serialization/VariableExpressions/Workflows.cs +++ b/test/integration/Elsa.Workflows.IntegrationTests/Serialization/VariableExpressions/Workflows.cs @@ -1,6 +1,5 @@ -using Elsa.Workflows; +using Elsa.Expressions.Models; using Elsa.Workflows.Activities; -using Elsa.Workflows.Activities.Flowchart.Activities; using Elsa.Workflows.Contracts; namespace Elsa.Workflows.IntegrationTests.Serialization.VariableExpressions; @@ -9,16 +8,24 @@ class SampleWorkflow : WorkflowBase { protected override void Build(IWorkflowBuilder workflow) { - var variable1 = workflow.WithVariable("Some Value"); + var variable1 = workflow.WithVariable("Some variable"); + var variable2 = workflow.WithVariable(42); + var literal1 = new Literal("Some literal"); + var literal2 = new Literal(84); var writeLine1 = new WriteLine(variable1); + var writeLine2 = new WriteLine(literal1); + var numberActivity1 = new NumberActivity(variable2); + var numberActivity2 = new NumberActivity(literal2); - workflow.Root = new Flowchart + workflow.Root = new Sequence { Activities = { - writeLine1 - }, - Start = writeLine1 + writeLine1, + writeLine2, + numberActivity1, + numberActivity2 + } }; } } \ No newline at end of file From d62c495b650959e097c89968013b1d004ed709ad Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Mon, 12 Feb 2024 18:57:02 +0100 Subject: [PATCH 2/6] Change default value retrieval to use ToString method The change was made in the DefaultExpressionDescriptorProvider.cs under Elsa.Workflows.Management. Instead of relying on the GetString method, the updated code now uses the ToString method for default value retrieval. This change improves how we process and retrieve default values in unspecified cases. --- .../Providers/DefaultExpressionDescriptorProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/Elsa.Workflows.Management/Providers/DefaultExpressionDescriptorProvider.cs b/src/modules/Elsa.Workflows.Management/Providers/DefaultExpressionDescriptorProvider.cs index dd434f19f..df80d1ce9 100644 --- a/src/modules/Elsa.Workflows.Management/Providers/DefaultExpressionDescriptorProvider.cs +++ b/src/modules/Elsa.Workflows.Management/Providers/DefaultExpressionDescriptorProvider.cs @@ -39,7 +39,7 @@ public class DefaultExpressionDescriptorProvider : IExpressionDescriptorProvider JsonValueKind.Number => elementValue.GetDecimal(), JsonValueKind.True => true, JsonValueKind.False => false, - _ => v.GetString() + _ => v.ToString() }); return new Expression("Literal", value); From 4e78aab80061d921b29bef51f37cd3582d11ae2e Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Mon, 12 Feb 2024 19:11:18 +0100 Subject: [PATCH 3/6] Remove version number from Elsa.Environments project file The version number was removed from the Elsa.Environments.csproj file. This was unnecessary because the version number is now handled elsewhere, reducing redundancy and maintenance. --- src/modules/Elsa.Environments/Elsa.Environments.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/src/modules/Elsa.Environments/Elsa.Environments.csproj b/src/modules/Elsa.Environments/Elsa.Environments.csproj index c41b5df4c..a9ae2faed 100644 --- a/src/modules/Elsa.Environments/Elsa.Environments.csproj +++ b/src/modules/Elsa.Environments/Elsa.Environments.csproj @@ -1,7 +1,6 @@ - 3.0.0 Provides API endpoints for client applications to enumerate available environments. From a6783256f48672eaf80ca87826806a19901cc20b Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Mon, 12 Feb 2024 19:26:00 +0100 Subject: [PATCH 4/6] Update expression serialization and deserialization Refined the expression serialization context and its deserialization method for more extensive usage. Also, simplified the handling of value retrieval in the DefaultExpressionDescriptorProvider. This allows greater control over serialization processes and makes the code more concise. --- .../Models/ExpressionDescriptor.cs | 19 +++++++++++++++++-- .../Models/ExpressionSerializationContext.cs | 5 ++++- .../Converters/InputJsonConverter.cs | 2 +- .../DefaultExpressionDescriptorProvider.cs | 18 +++++++----------- 4 files changed, 29 insertions(+), 15 deletions(-) diff --git a/src/modules/Elsa.Expressions/Models/ExpressionDescriptor.cs b/src/modules/Elsa.Expressions/Models/ExpressionDescriptor.cs index 8f4220660..b63ac9d7a 100644 --- a/src/modules/Elsa.Expressions/Models/ExpressionDescriptor.cs +++ b/src/modules/Elsa.Expressions/Models/ExpressionDescriptor.cs @@ -1,3 +1,4 @@ +using System.Text.Json; using Elsa.Expressions.Contracts; namespace Elsa.Expressions.Models; @@ -7,6 +8,20 @@ namespace Elsa.Expressions.Models; /// public class ExpressionDescriptor { + /// + /// Initializes a new instance of the class. + /// + public ExpressionDescriptor() + { + // Default deserialization function. + Deserialize = context => + { + return context.JsonElement.ValueKind == JsonValueKind.Object + ? context.JsonElement.Deserialize((JsonSerializerOptions?)context.Options)! + : new Expression(context.ExpressionType, null!); + }; + } + /// /// Gets or sets the syntax name. /// @@ -41,9 +56,9 @@ public class ExpressionDescriptor /// Gets or sets the memory block reference factory. /// public Func MemoryBlockReferenceFactory { get; set; } = () => new MemoryBlockReference(); - + /// /// Gets or sets the expression deserialization function. /// - public Func Deserialize { get; set; } = default!; + public Func Deserialize { get; set; } = default!; } \ No newline at end of file diff --git a/src/modules/Elsa.Expressions/Models/ExpressionSerializationContext.cs b/src/modules/Elsa.Expressions/Models/ExpressionSerializationContext.cs index f366531b4..d5da5c2c8 100644 --- a/src/modules/Elsa.Expressions/Models/ExpressionSerializationContext.cs +++ b/src/modules/Elsa.Expressions/Models/ExpressionSerializationContext.cs @@ -2,4 +2,7 @@ using System.Text.Json; namespace Elsa.Expressions.Models; -public record ExpressionSerializationContext(JsonElement JsonElement, JsonSerializerOptions Options, Type MemoryBlockType); \ No newline at end of file +/// +/// Defines the context for expression serialization. +/// +public record ExpressionSerializationContext(string ExpressionType, JsonElement JsonElement, JsonSerializerOptions Options, Type MemoryBlockType); \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Serialization/Converters/InputJsonConverter.cs b/src/modules/Elsa.Workflows.Core/Serialization/Converters/InputJsonConverter.cs index d259b0deb..f311ec8c5 100644 --- a/src/modules/Elsa.Workflows.Core/Serialization/Converters/InputJsonConverter.cs +++ b/src/modules/Elsa.Workflows.Core/Serialization/Converters/InputJsonConverter.cs @@ -47,7 +47,7 @@ public class InputJsonConverter : JsonConverter> return default!; var memoryBlockType = memoryBlockReference.GetType(); - var context = new ExpressionSerializationContext(expressionElement, options, memoryBlockType); + var context = new ExpressionSerializationContext(expressionTypeName!, expressionElement, options, memoryBlockType); var expression = expressionDescriptor!.Deserialize(context); return (Input)Activator.CreateInstance(typeof(Input), expression, memoryBlockReference)!; diff --git a/src/modules/Elsa.Workflows.Management/Providers/DefaultExpressionDescriptorProvider.cs b/src/modules/Elsa.Workflows.Management/Providers/DefaultExpressionDescriptorProvider.cs index df80d1ce9..86076cee7 100644 --- a/src/modules/Elsa.Workflows.Management/Providers/DefaultExpressionDescriptorProvider.cs +++ b/src/modules/Elsa.Workflows.Management/Providers/DefaultExpressionDescriptorProvider.cs @@ -62,9 +62,9 @@ public class DefaultExpressionDescriptorProvider : IExpressionDescriptorProvider memoryBlockReferenceFactory: () => new Variable(), deserialize: context => { - var expressionValueElement = context.JsonElement.TryGetProperty("value", out var expressionElementValueValue) ? expressionElementValueValue : default; - var expressionValue = expressionValueElement.Deserialize(context.MemoryBlockType, context.Options); - return new Expression("Variable", expressionValue); + var valueElement = context.JsonElement.TryGetProperty("value", out var v) ? v : default; + var value = valueElement.Deserialize(context.MemoryBlockType, context.Options); + return new Expression("Variable", value); } ); } @@ -86,16 +86,12 @@ public class DefaultExpressionDescriptorProvider : IExpressionDescriptorProvider IsSerializable = isSerializable, IsBrowsable = isBrowsable, HandlerFactory = sp => ActivatorUtilities.GetServiceOrCreateInstance(sp), - MemoryBlockReferenceFactory = memoryBlockReferenceFactory ?? (() => new MemoryBlockReference()), - Deserialize = deserialize ?? - (context => - { - return context.JsonElement.ValueKind == JsonValueKind.Object - ? context.JsonElement.Deserialize((JsonSerializerOptions?)context.Options)! - : new Expression(expressionType, null!); - }) + MemoryBlockReferenceFactory = memoryBlockReferenceFactory ?? (() => new MemoryBlockReference()) }; + if (deserialize != null) + descriptor.Deserialize = deserialize; + if (monacoLanguage != null) descriptor.Properties = new { From 4bb1c846a9243ef0fd23e018296033b7c9d3b8c0 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Mon, 12 Feb 2024 20:49:44 +0100 Subject: [PATCH 5/6] Refactor GetOutput method in ActivityExtensions The GetOutput method was extracted from the execution contexts to ensure more precise and isolated functionality. It was simplified and redefined in three contexts which are: the ActivityExecutionContext, the WorkflowExecutionContext, and the ExpressionExecutionContext. This will enhance maintainability and readability of the code. --- .../Extensions/ActivityExtensions.cs | 27 +++++++------------ 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/src/modules/Elsa.Workflows.Core/Extensions/ActivityExtensions.cs b/src/modules/Elsa.Workflows.Core/Extensions/ActivityExtensions.cs index f5e629ce2..9b230e96d 100644 --- a/src/modules/Elsa.Workflows.Core/Extensions/ActivityExtensions.cs +++ b/src/modules/Elsa.Workflows.Core/Extensions/ActivityExtensions.cs @@ -58,21 +58,6 @@ public static class ActivityExtensions return query.Select(x => x!).ToList(); } - - /// - /// Gets the output with the specified name. - /// - /// The activity to get the output from. - /// The workflow execution context. - /// Name of the output. - /// The output value. - public static object? GetOutput(this IActivity activity, WorkflowExecutionContext context, string? outputName = default) - { - var workflowExecutionContext = context; - var outputRegister = workflowExecutionContext.GetActivityOutputRegister(); - var output = outputRegister.FindOutputByActivityId(activity.Id, outputName); - return output; - } /// /// Gets the output with the specified name. @@ -83,7 +68,10 @@ public static class ActivityExtensions /// The output value. public static object? GetOutput(this IActivity activity, ActivityExecutionContext context, string? outputName = default) { - return activity.GetOutput(context.WorkflowExecutionContext, outputName); + var workflowExecutionContext = context.WorkflowExecutionContext; + var outputRegister = workflowExecutionContext.GetActivityOutputRegister(); + var output = outputRegister.FindOutputByActivityInstanceId(context.Id, outputName); + return output; } /// @@ -95,7 +83,12 @@ public static class ActivityExtensions /// The output value. public static object? GetOutput(this IActivity activity, ExpressionExecutionContext context, string? outputName = default) { - return activity.GetOutput(context.GetWorkflowExecutionContext(), outputName); + var activityExecutionContext = context.GetActivityExecutionContext(); + + if (activityExecutionContext == null) + return null; + + return activity.GetOutput(activityExecutionContext, outputName); } /// From c0e8eaf45cd3960221424c58459f8b6580354155 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Mon, 12 Feb 2024 21:00:00 +0100 Subject: [PATCH 6/6] Optimize output retrieval by activityId and activityInstanceId Modified `ActivityOutputRegister.cs` to utilize `LastOrDefault` instead of `FirstOrDefault` for retrieving the latest output by activityId or activityInstanceId. Now, in `ActivityExtensions.cs`, it differentiates between the output retrieval method based on whether the current activity context is identical to the requested one. --- .../Elsa.Workflows.Core/Extensions/ActivityExtensions.cs | 9 +++++++-- .../Elsa.Workflows.Core/Models/ActivityOutputRegister.cs | 4 ++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/modules/Elsa.Workflows.Core/Extensions/ActivityExtensions.cs b/src/modules/Elsa.Workflows.Core/Extensions/ActivityExtensions.cs index 9b230e96d..da916646c 100644 --- a/src/modules/Elsa.Workflows.Core/Extensions/ActivityExtensions.cs +++ b/src/modules/Elsa.Workflows.Core/Extensions/ActivityExtensions.cs @@ -70,8 +70,13 @@ public static class ActivityExtensions { var workflowExecutionContext = context.WorkflowExecutionContext; var outputRegister = workflowExecutionContext.GetActivityOutputRegister(); - var output = outputRegister.FindOutputByActivityInstanceId(context.Id, outputName); - return output; + + // If the provided activity execution context is the same as the current activity's execution context, we return the exact output value of the current activity execution context. + if(context.Activity.NodeId == activity.NodeId) + return outputRegister.FindOutputByActivityInstanceId(context.Id, outputName); + + // If the provided activity execution context is different from the current activity's execution context, we look for the last output value of the activity. + return outputRegister.FindOutputByActivityId(activity.Id, outputName); } /// diff --git a/src/modules/Elsa.Workflows.Core/Models/ActivityOutputRegister.cs b/src/modules/Elsa.Workflows.Core/Models/ActivityOutputRegister.cs index 6122ea42b..8b120af83 100644 --- a/src/modules/Elsa.Workflows.Core/Models/ActivityOutputRegister.cs +++ b/src/modules/Elsa.Workflows.Core/Models/ActivityOutputRegister.cs @@ -61,7 +61,7 @@ public class ActivityOutputRegister /// The output value. public object? FindOutputByActivityId(string activityId, string? outputName = default) { - var record = _records.FirstOrDefault(x => x.ActivityId == activityId && x.OutputName == (outputName ?? DefaultOutputName)); + var record = _records.LastOrDefault(x => x.ActivityId == activityId && x.OutputName == (outputName ?? DefaultOutputName)); return record?.Value; } @@ -73,7 +73,7 @@ public class ActivityOutputRegister /// The output value. public object? FindOutputByActivityInstanceId(string activityInstanceId, string? outputName = default) { - var record = _records.FirstOrDefault(x => x.ActivityInstanceId == activityInstanceId && x.OutputName == (outputName ?? DefaultOutputName)); + var record = _records.LastOrDefault(x => x.ActivityInstanceId == activityInstanceId && x.OutputName == (outputName ?? DefaultOutputName)); return record?.Value; } } \ No newline at end of file