Skip to content

Commit

Permalink
Improve Elsa workflow expression serialization (#4930)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
sfmskywalker authored Feb 12, 2024
1 parent fa32e00 commit 3bad8a5
Show file tree
Hide file tree
Showing 12 changed files with 149 additions and 80 deletions.
43 changes: 0 additions & 43 deletions .github/workflows/pr.yml

This file was deleted.

1 change: 0 additions & 1 deletion Elsa.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand Down
1 change: 0 additions & 1 deletion src/modules/Elsa.Expressions/Helpers/ObjectConverter.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using System.Collections;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Dynamic;
using System.Globalization;
using System.Text.Json;
Expand Down
2 changes: 2 additions & 0 deletions src/modules/Elsa.Expressions/LiteralExpression.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
5 changes: 5 additions & 0 deletions src/modules/Elsa.Expressions/Models/ExpressionDescriptor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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!;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using System.Text.Json;

namespace Elsa.Expressions.Models;

public record ExpressionSerializationContext(JsonElement JsonElement, JsonSerializerOptions Options, Type MemoryBlockType);
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Text.Json;
using Elsa.Expressions.Contracts;
using Elsa.Expressions.Models;
using Elsa.Workflows.Memory;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,27 +39,17 @@ public override Input<T> 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<T>)Activator.CreateInstance(typeof(Input<T>), expression, memoryBlockReference)!;
}

Expand All @@ -73,10 +63,10 @@ public override void Write(Utf8JsonWriter writer, Input<T> 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;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Text.Json;
using Elsa.Expressions;
using Elsa.Expressions.Contracts;
using Elsa.Expressions.Models;
Expand All @@ -21,35 +22,85 @@ public IEnumerable<ExpressionDescriptor> GetDescriptors()
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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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
}
};
}
}

0 comments on commit 3bad8a5

Please sign in to comment.