diff --git a/src/clients/Elsa.Api.Client/Elsa.Api.Client.csproj b/src/clients/Elsa.Api.Client/Elsa.Api.Client.csproj
index 5358e76f3..664e83334 100644
--- a/src/clients/Elsa.Api.Client/Elsa.Api.Client.csproj
+++ b/src/clients/Elsa.Api.Client/Elsa.Api.Client.csproj
@@ -21,5 +21,8 @@
+
+
+
diff --git a/src/clients/Elsa.Api.Client/Extensions/ObjectConverter.cs b/src/clients/Elsa.Api.Client/Extensions/ObjectConverter.cs
index 8b302ab0b..75f7d9644 100644
--- a/src/clients/Elsa.Api.Client/Extensions/ObjectConverter.cs
+++ b/src/clients/Elsa.Api.Client/Extensions/ObjectConverter.cs
@@ -137,7 +137,7 @@ public static class ObjectConverter
return Enum.ToObject(underlyingTargetType, value);
if (underlyingSourceType == typeof(double))
- return Enum.ToObject(underlyingTargetType, Convert.ChangeType(value, typeof(int)));
+ return Enum.ToObject(underlyingTargetType, Convert.ChangeType(value, typeof(int), CultureInfo.InvariantCulture));
}
if (value is string s)
@@ -178,7 +178,7 @@ public static class ObjectConverter
try
{
- return Convert.ChangeType(value, underlyingTargetType);
+ return Convert.ChangeType(value, underlyingTargetType, CultureInfo.InvariantCulture);
}
catch (InvalidCastException)
{
diff --git a/src/modules/Elsa.Expressions/Elsa.Expressions.csproj b/src/modules/Elsa.Expressions/Elsa.Expressions.csproj
index d696bff63..1cb36facc 100644
--- a/src/modules/Elsa.Expressions/Elsa.Expressions.csproj
+++ b/src/modules/Elsa.Expressions/Elsa.Expressions.csproj
@@ -15,6 +15,7 @@
+
diff --git a/src/modules/Elsa.Expressions/Helpers/ObjectConverter.cs b/src/modules/Elsa.Expressions/Helpers/ObjectConverter.cs
index 26a1a01bf..899a84ddc 100644
--- a/src/modules/Elsa.Expressions/Helpers/ObjectConverter.cs
+++ b/src/modules/Elsa.Expressions/Helpers/ObjectConverter.cs
@@ -19,7 +19,7 @@ namespace Elsa.Expressions.Helpers;
///
/// Provides options to the conversion method.
///
-public record ObjectConverterOptions(JsonSerializerOptions? SerializerOptions = default, IWellKnownTypeRegistry? WellKnownTypeRegistry = default);
+public record ObjectConverterOptions(JsonSerializerOptions? SerializerOptions = null, IWellKnownTypeRegistry? WellKnownTypeRegistry = null, bool DeserializeJsonObjectToObject = false);
///
/// A helper that attempts many strategies to try and convert the source value into the destination type.
@@ -40,11 +40,11 @@ public static class ObjectConverter
try
{
var convertedValue = value.ConvertTo(targetType, converterOptions);
- return new Result(true, convertedValue, null);
+ return new(true, convertedValue, null);
}
catch (Exception e)
{
- return new Result(false, null, e);
+ return new(false, null, e);
}
}
@@ -52,20 +52,23 @@ public static class ObjectConverter
/// Attempts to convert the source value into the destination type.
///
public static T? ConvertTo(this object? value, ObjectConverterOptions? converterOptions = null) => value != null ? (T?)value.ConvertTo(typeof(T), converterOptions) : default;
-
+
private static JsonSerializerOptions? _defaultSerializerOptions;
private static JsonSerializerOptions? _internalSerializerOptions;
-
- private static JsonSerializerOptions DefaultSerializerOptions => _defaultSerializerOptions ??= new JsonSerializerOptions
+
+ private static JsonSerializerOptions DefaultSerializerOptions => _defaultSerializerOptions ??= new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
PropertyNameCaseInsensitive = true,
ReferenceHandler = ReferenceHandler.Preserve,
- Converters = { new JsonStringEnumConverter() },
+ Converters =
+ {
+ new JsonStringEnumConverter()
+ },
Encoder = JavaScriptEncoder.Create(UnicodeRanges.All)
};
-
- private static JsonSerializerOptions InternalSerializerOptions => _internalSerializerOptions ??= new JsonSerializerOptions
+
+ private static JsonSerializerOptions InternalSerializerOptions => _internalSerializerOptions ??= new()
{
Encoder = JavaScriptEncoder.Create(UnicodeRanges.All)
};
@@ -77,11 +80,11 @@ public static class ObjectConverter
public static object? ConvertTo(this object? value, Type targetType, ObjectConverterOptions? converterOptions = null)
{
if (value == null)
- return default!;
+ return null;
var sourceType = value.GetType();
- if (sourceType == targetType)
+ if (targetType.IsAssignableFrom(sourceType))
return value;
var serializerOptions = converterOptions?.SerializerOptions ?? DefaultSerializerOptions;
@@ -99,26 +102,41 @@ public static class ObjectConverter
return jsonElement.Deserialize(targetType, serializerOptions);
}
- if (value is JsonNode jsonObject)
+ if (value is JsonNode jsonNode)
{
- return underlyingTargetType switch
+ if (jsonNode is not JsonArray jsonArray)
{
- { } t when t == typeof(string) => jsonObject.ToString(),
- { } t when t != typeof(object) => jsonObject.Deserialize(targetType, serializerOptions),
- _ => jsonObject
- };
+ return underlyingTargetType switch
+ {
+ { } t when t == typeof(string) => jsonNode.ToString(),
+ { } t when t == typeof(ExpandoObject) && jsonNode.GetValueKind() == JsonValueKind.Object => JsonSerializer.Deserialize(jsonNode.ToJsonString()),
+ { } t when t != typeof(object) || converterOptions?.DeserializeJsonObjectToObject == true => jsonNode.Deserialize(targetType, serializerOptions),
+ _ => jsonNode
+ };
+ }
+
+ // Convert to target type if target type is an array or a generic collection.
+ if (targetType.IsArray || targetType.IsCollectionType())
+ {
+ // The element type of the source array is JsonObject. If the element type of the target array is Object then return the source array as an array of JsonObjects.
+ // Deserializing normally would return an array of JsonElement instead of JsonObject, but we want to keep JsonObject elements:
+ var targetElementType = targetType.IsArray ? targetType.GetElementType()! : targetType.GenericTypeArguments[0];
+
+ if (targetElementType != typeof(object))
+ return jsonArray.Deserialize(targetType, serializerOptions);
+ }
}
if (underlyingSourceType == typeof(string) && !underlyingTargetType.IsPrimitive && underlyingTargetType != typeof(object))
{
var stringValue = (string)value;
-
+
if (underlyingTargetType == typeof(byte[]))
{
// Byte arrays are serialized to base64, so in this case, we convert the string back to the requested target type of byte[].
return Convert.FromBase64String(stringValue);
}
-
+
try
{
var firstChar = stringValue.TrimStart().FirstOrDefault();
@@ -145,7 +163,7 @@ public static class ObjectConverter
return ConvertAnyDateType(value, underlyingTargetType);
var internalSerializerOptions = InternalSerializerOptions;
-
+
if (typeof(IDictionary).IsAssignableFrom(underlyingSourceType) && underlyingTargetType.IsClass)
{
if (typeof(ExpandoObject) == underlyingTargetType)
@@ -187,10 +205,10 @@ public static class ObjectConverter
return Enum.ToObject(underlyingTargetType, value);
if (underlyingSourceType == typeof(double))
- return Enum.ToObject(underlyingTargetType, Convert.ChangeType(value, typeof(int)));
-
+ return Enum.ToObject(underlyingTargetType, Convert.ChangeType(value, typeof(int), CultureInfo.InvariantCulture));
+
if (underlyingSourceType == typeof(long))
- return Enum.ToObject(underlyingTargetType, Convert.ChangeType(value, typeof(int)));
+ return Enum.ToObject(underlyingTargetType, Convert.ChangeType(value, typeof(int), CultureInfo.InvariantCulture));
}
if (value is string s)
@@ -203,7 +221,10 @@ public static class ObjectConverter
// Perhaps it's a bit of a leap, but if the input is a string and the target type is IEnumerable, then let's assume the string is a comma-separated list of strings.
if (typeof(IEnumerable).IsAssignableFrom(underlyingTargetType))
- return new[] { s };
+ return new[]
+ {
+ s
+ };
}
if (value is IEnumerable enumerable)
@@ -231,7 +252,7 @@ public static class ObjectConverter
try
{
- return Convert.ChangeType(value, underlyingTargetType);
+ return Convert.ChangeType(value, underlyingTargetType, CultureInfo.InvariantCulture);
}
catch (InvalidCastException e)
{
@@ -246,9 +267,7 @@ public static class ObjectConverter
{
var dateTypes = new[]
{
- typeof(DateTime),
- typeof(DateTimeOffset),
- typeof(DateOnly)
+ typeof(DateTime), typeof(DateTimeOffset), typeof(DateOnly)
};
return dateTypes.Contains(type);
@@ -269,20 +288,20 @@ public static class ObjectConverter
{
DateTime dateTime => dateTime,
DateTimeOffset dateTimeOffset => dateTimeOffset.DateTime,
- DateOnly date => new DateTime(date.Year, date.Month, date.Day),
+ DateOnly date => new(date.Year, date.Month, date.Day),
_ => throw new ArgumentException("Invalid value type.")
},
{ } t when t == typeof(DateTimeOffset) => value switch
{
- DateTime dateTime => new DateTimeOffset(dateTime),
+ DateTime dateTime => new(dateTime),
DateTimeOffset dateTimeOffset => dateTimeOffset,
- DateOnly date => new DateTimeOffset(date.Year, date.Month, date.Day, 0, 0, 0, TimeSpan.Zero),
+ DateOnly date => new(date.Year, date.Month, date.Day, 0, 0, 0, TimeSpan.Zero),
_ => throw new ArgumentException("Invalid value type.")
},
{ } t when t == typeof(DateOnly) => value switch
{
- DateTime dateTime => new DateOnly(dateTime.Year, dateTime.Month, dateTime.Day),
- DateTimeOffset dateTimeOffset => new DateOnly(dateTimeOffset.Year, dateTimeOffset.Month, dateTimeOffset.Day),
+ DateTime dateTime => new(dateTime.Year, dateTime.Month, dateTime.Day),
+ DateTimeOffset dateTimeOffset => new(dateTimeOffset.Year, dateTimeOffset.Month, dateTimeOffset.Day),
DateOnly date => date,
_ => throw new ArgumentException("Invalid value type.")
},
diff --git a/src/modules/Elsa.Workflows.Core/Services/WorkflowInstanceStorageDriver.cs b/src/modules/Elsa.Workflows.Core/Services/WorkflowInstanceStorageDriver.cs
index b6e7fd7ae..9d1e7add3 100644
--- a/src/modules/Elsa.Workflows.Core/Services/WorkflowInstanceStorageDriver.cs
+++ b/src/modules/Elsa.Workflows.Core/Services/WorkflowInstanceStorageDriver.cs
@@ -1,22 +1,29 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json;
using System.Text.Json.Nodes;
+using Elsa.Expressions.Helpers;
using Elsa.Extensions;
using Elsa.Workflows.Contracts;
using JetBrains.Annotations;
namespace Elsa.Workflows.Services;
+///
/// A storage driver that stores objects in the workflow state itself.
+///
[Display(Name = "Workflow Instance")]
[UsedImplicitly]
-public class WorkflowInstanceStorageDriver : IStorageDriver
+public class WorkflowInstanceStorageDriver(IPayloadSerializer payloadSerializer) : IStorageDriver
{
+ ///
/// The key used to store the variables in the workflow state.
+ ///
public const string VariablesDictionaryStateKey = "Variables";
-
+
///
- public double Priority => 1;
+ public double Priority => 5;
+ ///
+ public IEnumerable Tags => [];
///
public ValueTask WriteAsync(string id, object value, StorageDriverContext context)
@@ -34,7 +41,15 @@ public class WorkflowInstanceStorageDriver : IStorageDriver
{
var dictionary = GetVariablesDictionary(context);
var node = dictionary.GetValueOrDefault(id);
- return new(node);
+ var variable = context.Variable;
+ var variableType = variable.GetVariableType();
+ var options = new ObjectConverterOptions
+ {
+ DeserializeJsonObjectToObject = true,
+ SerializerOptions = payloadSerializer.GetOptions()
+ };
+ var parsedValue = node.ConvertTo(variableType, options);
+ return new (parsedValue);
}
///