Merge pull request #6474 from elsa-workflows/blueberry-serialization-patch

Patch ObjectConverter
This commit is contained in:
Sipke Schoorstra 2025-03-10 08:54:10 +01:00 committed by GitHub
commit 7805f8b02f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 77 additions and 39 deletions

View file

@ -21,5 +21,8 @@
<ItemGroup>
<PackageReference Include="System.Text.Json" VersionOverride="$(SystemTextJsonVersion)" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\modules\Elsa.Expressions\Elsa.Expressions.csproj" />
</ItemGroup>
</Project>

View file

@ -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)
{

View file

@ -15,6 +15,7 @@
<ItemGroup>
<ProjectReference Include="..\..\common\Elsa.Features\Elsa.Features.csproj" />
<ProjectReference Include="..\Elsa.Common\Elsa.Common.csproj" />
</ItemGroup>
</Project>

View file

@ -19,7 +19,7 @@ namespace Elsa.Expressions.Helpers;
/// <summary>
/// Provides options to the conversion method.
/// </summary>
public record ObjectConverterOptions(JsonSerializerOptions? SerializerOptions = default, IWellKnownTypeRegistry? WellKnownTypeRegistry = default);
public record ObjectConverterOptions(JsonSerializerOptions? SerializerOptions = null, IWellKnownTypeRegistry? WellKnownTypeRegistry = null, bool DeserializeJsonObjectToObject = false);
/// <summary>
/// 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.
/// </summary>
public static T? ConvertTo<T>(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<ExpandoObject>(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<string, object>).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<string>, then let's assume the string is a comma-separated list of strings.
if (typeof(IEnumerable<string>).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.")
},

View file

@ -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;
/// <summary>
/// A storage driver that stores objects in the workflow state itself.
/// </summary>
[Display(Name = "Workflow Instance")]
[UsedImplicitly]
public class WorkflowInstanceStorageDriver : IStorageDriver
public class WorkflowInstanceStorageDriver(IPayloadSerializer payloadSerializer) : IStorageDriver
{
/// <summary>
/// The key used to store the variables in the workflow state.
/// </summary>
public const string VariablesDictionaryStateKey = "Variables";
/// <inheritdoc />
public double Priority => 1;
public double Priority => 5;
/// <inheritdoc />
public IEnumerable<string> Tags => [];
/// <inheritdoc />
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);
}
/// <inheritdoc />