diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml deleted file mode 100644 index dd4b9ee41e..0000000000 --- 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 e1a6d97967..b0c0f47e82 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 888060fd1b..677f09ac0a 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 b663b62be8..7ddbf58fea 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 7314c78a50..8f42206606 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 0000000000..f366531b46 --- /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 358f94d672..1955723398 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 f7d194fca1..d259b0deb6 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 override Input Read(ref Utf8JsonReader reader, Type typeToConvert, Jso 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 override void Write(Utf8JsonWriter writer, Input value, JsonSerializer 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 d27da2b210..dd434f19f0 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 IEnumerable GetDescriptors() 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 0000000000..6f76afac7a --- /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 01cc375600..71068a82e9 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 3d13a59fab..2de1b523c5 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