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); } ///