diff --git a/src/modules/Elsa.Common/Extensions/JsonSerializerOptionsExtensions.cs b/src/modules/Elsa.Common/Extensions/JsonSerializerOptionsExtensions.cs
index 0bf9a9910..e573f3d4f 100644
--- a/src/modules/Elsa.Common/Extensions/JsonSerializerOptionsExtensions.cs
+++ b/src/modules/Elsa.Common/Extensions/JsonSerializerOptionsExtensions.cs
@@ -27,4 +27,14 @@ public static class JsonSerializerOptionsExtensions
{
return new(options);
}
+
+ ///
+ /// Clones the options and sets for execution-time value conversion.
+ ///
+ public static JsonSerializerOptions CloneForValueConversion(this JsonSerializerOptions options)
+ {
+ var clone = options.Clone();
+ clone.ReferenceHandler = ReferenceHandler.Preserve;
+ return clone;
+ }
}
\ No newline at end of file
diff --git a/src/modules/Elsa.Workflows.Core/Extensions/ExpressionExecutionContextExtensions.cs b/src/modules/Elsa.Workflows.Core/Extensions/ExpressionExecutionContextExtensions.cs
index e3c6860cb..744a3275a 100644
--- a/src/modules/Elsa.Workflows.Core/Extensions/ExpressionExecutionContextExtensions.cs
+++ b/src/modules/Elsa.Workflows.Core/Extensions/ExpressionExecutionContextExtensions.cs
@@ -394,17 +394,9 @@ public static class ExpressionExecutionContextExtensions
}
}
- private static JsonSerializerOptions? _serializerOptions;
-
private static JsonSerializerOptions GetSerializerOptions(ExpressionExecutionContext context)
{
- if (_serializerOptions != null)
- return _serializerOptions;
-
- var serializerOptions = context.GetRequiredService().GetOptions().Clone();
- serializerOptions.ReferenceHandler = ReferenceHandler.Preserve;
- _serializerOptions = serializerOptions;
- return serializerOptions;
+ return context.GetRequiredService().GetOptions().CloneForValueConversion();
}
extension(ExpressionExecutionContext context)
diff --git a/src/modules/Elsa.Workflows.Core/VariableStorageDrivers/WorkflowInstanceStorageDriver.cs b/src/modules/Elsa.Workflows.Core/VariableStorageDrivers/WorkflowInstanceStorageDriver.cs
index 7c1d4e963..af2bcf605 100644
--- a/src/modules/Elsa.Workflows.Core/VariableStorageDrivers/WorkflowInstanceStorageDriver.cs
+++ b/src/modules/Elsa.Workflows.Core/VariableStorageDrivers/WorkflowInstanceStorageDriver.cs
@@ -32,7 +32,7 @@ public class WorkflowInstanceStorageDriver(IPayloadSerializer payloadSerializer,
{
try
{
- var node = JsonSerializer.SerializeToNode(value);
+ var node = JsonSerializer.SerializeToNode(value, GetSerializationType(value, context), GetSerializerOptions());
dictionary[id] = node!;
}
catch (Exception ex) when (ex is JsonException or NotSupportedException or ObjectDisposedException)
@@ -51,10 +51,15 @@ public class WorkflowInstanceStorageDriver(IPayloadSerializer payloadSerializer,
var node = dictionary.GetValueOrDefault(id);
var variable = context.Variable;
var variableType = variable.GetVariableType();
+ var serializerOptions = GetSerializerOptions();
+
+ if (variableType == typeof(object))
+ return new(node?.Deserialize(typeof(object), serializerOptions));
+
var options = new ObjectConverterOptions
{
DeserializeJsonObjectToObject = true,
- SerializerOptions = payloadSerializer.GetOptions()
+ SerializerOptions = serializerOptions
};
var result = node.TryConvertTo(variableType, options);
if (result.IsSuccess)
@@ -76,6 +81,17 @@ public class WorkflowInstanceStorageDriver(IPayloadSerializer payloadSerializer,
return ValueTask.CompletedTask;
}
+ private JsonSerializerOptions GetSerializerOptions() => payloadSerializer.GetOptions().Clone();
+
+ private static Type GetSerializationType(object? value, StorageDriverContext context)
+ {
+ var declaredType = context.Variable.GetVariableType();
+ if (declaredType == typeof(object))
+ return typeof(object);
+
+ return value?.GetType() ?? declaredType;
+ }
+
private VariablesDictionary GetVariablesDictionary(StorageDriverContext context) => context.ExecutionContext.Properties.GetOrAdd(VariablesDictionaryStateKey, () => new VariablesDictionary());
private void SetVariablesDictionary(StorageDriverContext context, VariablesDictionary dictionary) => context.ExecutionContext.Properties[VariablesDictionaryStateKey] = dictionary;
diff --git a/test/unit/Elsa.Workflows.Core.UnitTests/Extensions/ExpressionExecutionContextExtensions/ExpressionExecutionContextExtensionsTests.cs b/test/unit/Elsa.Workflows.Core.UnitTests/Extensions/ExpressionExecutionContextExtensions/ExpressionExecutionContextExtensionsTests.cs
index 4afd76c17..62adca947 100644
--- a/test/unit/Elsa.Workflows.Core.UnitTests/Extensions/ExpressionExecutionContextExtensions/ExpressionExecutionContextExtensionsTests.cs
+++ b/test/unit/Elsa.Workflows.Core.UnitTests/Extensions/ExpressionExecutionContextExtensions/ExpressionExecutionContextExtensionsTests.cs
@@ -1,9 +1,14 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Elsa.Common;
using Elsa.Expressions.JavaScript.Activities;
using Elsa.Expressions.Models;
using Elsa.Extensions;
using Elsa.Testing.Shared;
using Elsa.Workflows.Activities;
using Elsa.Workflows.Memory;
+using Microsoft.Extensions.DependencyInjection;
+using NSubstitute;
namespace Elsa.Workflows.Core.UnitTests;
@@ -107,4 +112,88 @@ public class ExpressionExecutionContextExtensionsTests
var updatedVariable = context.GetVariable("test");
Assert.Equal(10, updatedVariable);
}
+
+ [Fact]
+ public async Task GetInput_UsesSecondHostConverters_AfterFirstHostAlreadyConverted()
+ {
+ // Arrange
+ var firstContext = await CreateContextWithSerializerAsync("first");
+ firstContext.WorkflowExecutionContext.Input["payload"] = """{"token":"x"}""";
+
+ // Act
+ var first = firstContext.ExpressionExecutionContext.GetInput("payload");
+
+ var secondContext = await CreateContextWithSerializerAsync("second");
+ secondContext.WorkflowExecutionContext.Input["payload"] = """{"token":"x"}""";
+ var second = secondContext.ExpressionExecutionContext.GetInput("payload");
+
+ // Assert
+ Assert.Equal("first", first?.Tag);
+ Assert.Equal("second", second?.Tag);
+ }
+
+ [Fact]
+ public async Task GetInput_ResolvesSerializerOptionsFromCurrentHostOnEachCall()
+ {
+ // Arrange
+ var serializer = Substitute.For();
+ serializer.GetOptions().Returns(CreateTaggedOptions("first"), CreateTaggedOptions("second"));
+
+ var context = await CreateContextAsync(serializer);
+ context.WorkflowExecutionContext.Input["payload"] = """{"token":"x"}""";
+
+ // Act
+ var first = context.ExpressionExecutionContext.GetInput("payload");
+ var second = context.ExpressionExecutionContext.GetInput("payload");
+
+ // Assert
+ Assert.Equal("first", first?.Tag);
+ Assert.Equal("second", second?.Tag);
+ }
+
+ private static Task CreateContextWithSerializerAsync(string converterTag) =>
+ CreateContextAsync(new StubJsonSerializer(CreateTaggedOptions(converterTag)));
+
+ private static Task CreateContextAsync(IJsonSerializer serializer)
+ {
+ var fixture = new ActivityTestFixture(new WriteLine("test"))
+ .ConfigureServices(services => services.AddSingleton(serializer));
+ return fixture.BuildAsync();
+ }
+
+ private static JsonSerializerOptions CreateTaggedOptions(string tag)
+ {
+ var options = new JsonSerializerOptions();
+ options.Converters.Add(new TaggedValueConverter(tag));
+ return options;
+ }
+
+ private sealed class TaggedValue
+ {
+ public string Tag { get; set; } = "";
+ }
+
+ private sealed class TaggedValueConverter(string tag) : JsonConverter
+ {
+ public override TaggedValue Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ using var _ = JsonDocument.ParseValue(ref reader);
+ return new TaggedValue { Tag = tag };
+ }
+
+ public override void Write(Utf8JsonWriter writer, TaggedValue value, JsonSerializerOptions options) =>
+ writer.WriteStringValue(value.Tag);
+ }
+
+ private sealed class StubJsonSerializer(JsonSerializerOptions options) : IJsonSerializer
+ {
+ public JsonSerializerOptions GetOptions() => options;
+ public void ApplyOptions(JsonSerializerOptions _) { }
+ public string Serialize(object value) => throw new NotSupportedException();
+ public string Serialize(object value, Type type) => throw new NotSupportedException();
+ public string Serialize(T value) => throw new NotSupportedException();
+ public object Deserialize(string json) => throw new NotSupportedException();
+ public object Deserialize(string json, Type type) => throw new NotSupportedException();
+ public T Deserialize(string json) => throw new NotSupportedException();
+ }
}
diff --git a/test/unit/Elsa.Workflows.Core.UnitTests/VariableStorageDrivers/WorkflowInstanceStorageDriverTests.cs b/test/unit/Elsa.Workflows.Core.UnitTests/VariableStorageDrivers/WorkflowInstanceStorageDriverTests.cs
index b3e767fa0..cf52c95e9 100644
--- a/test/unit/Elsa.Workflows.Core.UnitTests/VariableStorageDrivers/WorkflowInstanceStorageDriverTests.cs
+++ b/test/unit/Elsa.Workflows.Core.UnitTests/VariableStorageDrivers/WorkflowInstanceStorageDriverTests.cs
@@ -1,7 +1,10 @@
using System.Text.Json;
using System.Text.Json.Nodes;
+using System.Text.Json.Serialization;
+using Elsa.Common.Serialization;
using Elsa.Expressions.Helpers;
using Elsa.Workflows.Memory;
+using Elsa.Workflows.Serialization.Converters;
using Microsoft.Extensions.Logging.Abstractions;
using NSubstitute;
@@ -53,6 +56,81 @@ public class WorkflowInstanceStorageDriverTests
Assert.False(GetVariables(harness.Properties).ContainsKey(id));
}
+ [Fact]
+ public async Task WriteThenRead_WhenValueHasOrdinaryDollarRefProperty_RoundTrips()
+ {
+ // Arrange
+ var harness = CreateHarness(new Variable("payload", new()));
+ const string id = "payloadVariable";
+ var value = new RefPayload { Ref = "ordinary" };
+
+ // Act
+ await harness.Driver.WriteAsync(id, value, harness.Context);
+ var read = await harness.Driver.ReadAsync(id, harness.Context);
+
+ // Assert
+ var restored = Assert.IsType(read);
+ Assert.Equal("ordinary", restored.Ref);
+ }
+
+ [Fact]
+ public async Task WriteThenRead_WhenObjectVariableHoldsAliasedType_RestoresConcreteClrType()
+ {
+ // Arrange
+ var registry = SerializationTypeRegistry.CreateDefault();
+ registry.RegisterType(typeof(AliasedPerson), nameof(AliasedPerson));
+ var harness = CreateHarness(new Variable