using System.Collections;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.SymbolStore;
using System.Dynamic;
using System.Globalization;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using System.Text.Unicode;
using Elsa.Expressions.Contracts;
using Elsa.Expressions.Exceptions;
using Elsa.Expressions.Extensions;
using Elsa.Expressions.Models;
using Elsa.Extensions;
namespace Elsa.Expressions.Helpers;
///
/// Provides options to the conversion method.
///
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.
///
public static class ObjectConverter
{
public static bool StrictMode = false; // Set to true to opt into strict mode.
///
/// Attempts to convert the source value into the destination type.
///
public static Result TryConvertTo(this object? value, ObjectConverterOptions? converterOptions = null) => value.TryConvertTo(typeof(T), converterOptions);
///
/// Attempts to convert the source value into the destination type.
///
[RequiresUnreferencedCode("The JsonSerializer type is not trim-compatible.")]
public static Result TryConvertTo(this object? value, Type targetType, ObjectConverterOptions? converterOptions = null)
{
try
{
var convertedValue = value.ConvertTo(targetType, converterOptions);
return new(true, convertedValue, null);
}
catch (Exception e)
{
return new(false, null, e);
}
}
///
/// 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()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
PropertyNameCaseInsensitive = true,
ReferenceHandler = ReferenceHandler.Preserve,
Converters =
{
new JsonStringEnumConverter()
},
Encoder = JavaScriptEncoder.Create(UnicodeRanges.All)
};
private static JsonSerializerOptions InternalSerializerOptions => _internalSerializerOptions ??= new()
{
Encoder = JavaScriptEncoder.Create(UnicodeRanges.All)
};
///
/// Attempts to convert the source value into the destination type.
///
[RequiresUnreferencedCode("The JsonSerializer type is not trim-compatible.")]
public static object? ConvertTo(this object? value, Type targetType, ObjectConverterOptions? converterOptions = null)
{
if (value == null)
return null;
var sourceType = value.GetType();
if (targetType.IsAssignableFrom(sourceType))
return value;
var serializerOptions = converterOptions?.SerializerOptions ?? DefaultSerializerOptions;
var underlyingTargetType = Nullable.GetUnderlyingType(targetType) ?? targetType;
var underlyingSourceType = Nullable.GetUnderlyingType(sourceType) ?? sourceType;
if (value is JsonElement { ValueKind: JsonValueKind.Number } jsonNumber && underlyingTargetType == typeof(string))
return jsonNumber.ToString().ConvertTo(underlyingTargetType);
if (value is JsonElement jsonElement)
{
if (jsonElement.ValueKind == JsonValueKind.String && underlyingTargetType != typeof(string))
return jsonElement.GetString().ConvertTo(underlyingTargetType);
return jsonElement.Deserialize(targetType, serializerOptions);
}
if (value is JsonNode jsonNode)
{
if (jsonNode is not JsonArray jsonArray)
{
var valueKind = jsonNode.GetValueKind();
if (valueKind == JsonValueKind.Null)
return null;
if (valueKind == JsonValueKind.Undefined)
return null;
return underlyingTargetType switch
{
{ } t when t == typeof(bool) && valueKind == JsonValueKind.False => false,
{ } t when t == typeof(bool) && valueKind == JsonValueKind.True => true,
{ } t when t.IsNumericType() && valueKind == JsonValueKind.Number => ConvertTo(jsonNode.ToString(), t),
{ } t when t == typeof(string) && valueKind == JsonValueKind.String => jsonNode.ToString(),
{ } 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();
if (firstChar is '{' or '[')
return JsonSerializer.Deserialize(stringValue, underlyingTargetType, serializerOptions);
}
catch (Exception e)
{
throw new TypeConversionException($"Failed to deserialize {stringValue} to {underlyingTargetType}", value, underlyingTargetType, e);
}
}
if (targetType == typeof(object))
return value;
if (underlyingTargetType.IsInstanceOfType(value))
return value;
if (underlyingSourceType == underlyingTargetType)
return value;
if (IsDateType(underlyingSourceType) && IsDateType(underlyingTargetType))
return ConvertAnyDateType(value, underlyingTargetType);
var internalSerializerOptions = InternalSerializerOptions;
if (typeof(IDictionary).IsAssignableFrom(underlyingSourceType) && underlyingTargetType.IsClass)
{
if (typeof(ExpandoObject) == underlyingTargetType)
{
var expandoJson = JsonSerializer.Serialize(value, internalSerializerOptions);
return ConvertTo(expandoJson, underlyingTargetType, converterOptions);
}
if (typeof(IDictionary).IsAssignableFrom(underlyingTargetType))
return new Dictionary((IDictionary)value);
var sourceDictionary = (IDictionary)value;
var json = JsonSerializer.Serialize(sourceDictionary, internalSerializerOptions);
return ConvertTo(json, underlyingTargetType, converterOptions);
}
if (typeof(IEnumerable