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.
This commit is contained in:
parent
fa32e00bce
commit
3bad8a5b1b
43
.github/workflows/pr.yml
vendored
43
.github/workflows/pr.yml
vendored
|
|
@ -1,43 +0,0 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
# <auto-generated>
|
||||
#
|
||||
# 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
|
||||
#
|
||||
# </auto-generated>
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
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
|
||||
1
Elsa.sln
1
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}"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
using System.Collections;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Dynamic;
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
using Elsa.Expressions.Contracts;
|
||||
using Elsa.Expressions.Helpers;
|
||||
using Elsa.Expressions.Models;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Elsa.Expressions;
|
||||
|
||||
/// <inheritdoc />
|
||||
[UsedImplicitly]
|
||||
public class LiteralExpressionHandler : IExpressionHandler
|
||||
{
|
||||
private readonly IWellKnownTypeRegistry _wellKnownTypeRegistry;
|
||||
|
|
|
|||
|
|
@ -41,4 +41,9 @@ public class ExpressionDescriptor
|
|||
/// Gets or sets the memory block reference factory.
|
||||
/// </summary>
|
||||
public Func<MemoryBlockReference> MemoryBlockReferenceFactory { get; set; } = () => new MemoryBlockReference();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the expression deserialization function.
|
||||
/// </summary>
|
||||
public Func<ExpressionSerializationContext, Expression> Deserialize { get; set; } = default!;
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
using System.Text.Json;
|
||||
|
||||
namespace Elsa.Expressions.Models;
|
||||
|
||||
public record ExpressionSerializationContext(JsonElement JsonElement, JsonSerializerOptions Options, Type MemoryBlockType);
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
using System.Text.Json;
|
||||
using Elsa.Expressions.Contracts;
|
||||
using Elsa.Expressions.Models;
|
||||
using Elsa.Workflows.Memory;
|
||||
|
|
|
|||
|
|
@ -39,27 +39,17 @@ public class InputJsonConverter<T> : JsonConverter<Input<T>>
|
|||
|
||||
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<T>)Activator.CreateInstance(typeof(Input<T>), expression, memoryBlockReference)!;
|
||||
}
|
||||
|
||||
|
|
@ -73,10 +63,10 @@ public class InputJsonConverter<T> : JsonConverter<Input<T>>
|
|||
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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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<LiteralExpressionHandler>("Literal", "Literal", isBrowsable: false);
|
||||
private ExpressionDescriptor CreateLiteralDescriptor()
|
||||
{
|
||||
return CreateDescriptor<LiteralExpressionHandler>(
|
||||
"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<ObjectExpressionHandler>("Object", "Object", monacoLanguage: "json", isBrowsable: false);
|
||||
|
||||
[Obsolete("Use Object instead.")]
|
||||
private ExpressionDescriptor CreateJsonDescriptor() => CreateDescriptor<ObjectExpressionHandler>("Json", "Json", monacoLanguage: "json", isBrowsable: false);
|
||||
|
||||
private ExpressionDescriptor CreateDelegateDescriptor() => CreateDescriptor<DelegateExpressionHandler>("Delegate", "Delegate", false, false);
|
||||
private ExpressionDescriptor CreateVariableDescriptor() => CreateDescriptor<VariableExpressionHandler>("Variable", "Variable", isBrowsable: false, memoryBlockReferenceFactory: () => new Variable());
|
||||
|
||||
private ExpressionDescriptor CreateVariableDescriptor()
|
||||
{
|
||||
return CreateDescriptor<VariableExpressionHandler>(
|
||||
"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<THandler>(
|
||||
string type,
|
||||
string expressionType,
|
||||
string displayName,
|
||||
bool isSerializable = true,
|
||||
bool isBrowsable = true,
|
||||
string? monacoLanguage = null,
|
||||
Func<MemoryBlockReference>? memoryBlockReferenceFactory = default) where THandler : IExpressionHandler
|
||||
Func<MemoryBlockReference>? memoryBlockReferenceFactory = default,
|
||||
Func<ExpressionSerializationContext, Expression>? deserialize = default)
|
||||
where THandler : IExpressionHandler
|
||||
{
|
||||
var descriptor = new ExpressionDescriptor
|
||||
{
|
||||
Type = type,
|
||||
Type = expressionType,
|
||||
DisplayName = displayName,
|
||||
IsSerializable = isSerializable,
|
||||
IsBrowsable = isBrowsable,
|
||||
HandlerFactory = sp => ActivatorUtilities.GetServiceOrCreateInstance<THandler>(sp),
|
||||
MemoryBlockReferenceFactory = memoryBlockReferenceFactory ?? (() => new MemoryBlockReference())
|
||||
MemoryBlockReferenceFactory = memoryBlockReferenceFactory ?? (() => new MemoryBlockReference()),
|
||||
Deserialize = deserialize ??
|
||||
(context =>
|
||||
{
|
||||
return context.JsonElement.ValueKind == JsonValueKind.Object
|
||||
? context.JsonElement.Deserialize<Expression>((JsonSerializerOptions?)context.Options)!
|
||||
: new Expression(expressionType, null!);
|
||||
})
|
||||
};
|
||||
|
||||
if (monacoLanguage != null)
|
||||
descriptor.Properties = new { MonacoLanguage = monacoLanguage }.ToDictionary();
|
||||
descriptor.Properties = new
|
||||
{
|
||||
MonacoLanguage = monacoLanguage
|
||||
}.ToDictionary();
|
||||
|
||||
return descriptor;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
/// <inheritdoc />
|
||||
public class NumberActivity : CodeActivity
|
||||
{
|
||||
/// <inheritdoc />
|
||||
[JsonConstructor]
|
||||
public NumberActivity()
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public NumberActivity(Variable<int> variable)
|
||||
{
|
||||
Number = new(variable);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public NumberActivity(Literal<int> literal)
|
||||
{
|
||||
Number = new(literal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number.
|
||||
/// </summary>
|
||||
public Input<int> Number { get; set; } = default!;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Execute(ActivityExecutionContext context)
|
||||
{
|
||||
var number = Number.Get(context);
|
||||
Console.WriteLine(number.ToString());
|
||||
}
|
||||
}
|
||||
|
|
@ -18,23 +18,35 @@ public class Tests
|
|||
{
|
||||
private readonly IWorkflowSerializer _workflowSerializer;
|
||||
private readonly IWorkflowBuilder _workflowBuilder;
|
||||
private readonly IWorkflowRunner _workflowRunner;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Tests"/> class.
|
||||
/// </summary>
|
||||
public Tests(ITestOutputHelper testOutputHelper)
|
||||
{
|
||||
var serviceProvider = new TestApplicationBuilder(testOutputHelper).Build();
|
||||
_workflowSerializer = serviceProvider.GetRequiredService<IWorkflowSerializer>();
|
||||
IWorkflowBuilderFactory workflowBuilderFactory = serviceProvider.GetRequiredService<IWorkflowBuilderFactory>();
|
||||
_workflowBuilder = workflowBuilderFactory.CreateBuilder();
|
||||
_workflowRunner = serviceProvider.GetRequiredService<IWorkflowRunner>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Variable types remain intact after serialization.
|
||||
/// </summary>
|
||||
[Fact(DisplayName = "Variable types remain intact after serialization")]
|
||||
public async Task Test1()
|
||||
{
|
||||
var workflow = await _workflowBuilder.BuildWorkflowAsync<SampleWorkflow>();
|
||||
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<Variable<string>>(rehydratedWriteLine.Text.Expression!.Value);
|
||||
Assert.IsType<Variable<string>>(rehydratedWriteLine1.Text.Expression!.Value);
|
||||
Assert.IsType<Variable<int>>(rehydratedNumberActivity1.Number.Expression!.Value);
|
||||
|
||||
await _workflowRunner.RunAsync(workflow);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<string>("Some Value");
|
||||
var variable1 = workflow.WithVariable("Some variable");
|
||||
var variable2 = workflow.WithVariable(42);
|
||||
var literal1 = new Literal<string>("Some literal");
|
||||
var literal2 = new Literal<int>(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
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue