Merge pull request #6474 from elsa-workflows/blueberry-serialization-patch
Patch ObjectConverter
This commit is contained in:
commit
7805f8b02f
|
|
@ -21,5 +21,8 @@
|
|||
<ItemGroup>
|
||||
<PackageReference Include="System.Text.Json" VersionOverride="$(SystemTextJsonVersion)" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\modules\Elsa.Expressions\Elsa.Expressions.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\common\Elsa.Features\Elsa.Features.csproj" />
|
||||
<ProjectReference Include="..\Elsa.Common\Elsa.Common.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -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.")
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
Loading…
Reference in a new issue