< Summary

Information
Class: Elsa.Expressions.Helpers.ObjectConverterOptions
Assembly: Elsa.Expressions
File(s): /home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Expressions/Helpers/ObjectConverter.cs
Line coverage
80%
Covered lines: 4
Uncovered lines: 1
Coverable lines: 5
Total lines: 372
Line coverage: 80%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
get_SerializerOptions()100%11100%
get_WellKnownTypeRegistry()100%210%
get_DeserializeJsonObjectToObject()100%11100%
get_StrictMode()100%11100%

File(s)

/home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Expressions/Helpers/ObjectConverter.cs

#LineLine coverage
 1using System.Collections;
 2using System.ComponentModel;
 3using System.Diagnostics.CodeAnalysis;
 4using System.Diagnostics.SymbolStore;
 5using System.Dynamic;
 6using System.Globalization;
 7using System.Text.Encodings.Web;
 8using System.Text.Json;
 9using System.Text.Json.Nodes;
 10using System.Text.Json.Serialization;
 11using System.Text.Unicode;
 12using Elsa.Expressions.Contracts;
 13using Elsa.Expressions.Exceptions;
 14using Elsa.Expressions.Extensions;
 15using Elsa.Expressions.Models;
 16using Elsa.Extensions;
 17
 18namespace Elsa.Expressions.Helpers;
 19
 20/// <summary>
 21/// Provides options to the conversion method.
 22/// </summary>
 182423public record ObjectConverterOptions(
 49624    JsonSerializerOptions? SerializerOptions = null,
 025    IWellKnownTypeRegistry? WellKnownTypeRegistry = null,
 1526    bool DeserializeJsonObjectToObject = false,
 363027    bool? StrictMode = null);
 28
 29/// <summary>
 30/// A helper that attempts many strategies to try and convert the source value into the destination type.
 31/// </summary>
 32public static class ObjectConverter
 33{
 34    public static bool StrictMode = false; // Set to true to opt into strict mode.
 35
 36    /// <summary>
 37    /// Attempts to convert the source value into the destination type.
 38    /// </summary>
 39    public static Result TryConvertTo<T>(this object? value, ObjectConverterOptions? converterOptions = null) => value.T
 40
 41    /// <summary>
 42    /// Attempts to convert the source value into the destination type.
 43    /// </summary>
 44    [RequiresUnreferencedCode("The JsonSerializer type is not trim-compatible.")]
 45    public static Result TryConvertTo(this object? value, Type targetType, ObjectConverterOptions? converterOptions = nu
 46    {
 47        try
 48        {
 49            var convertedValue = value.ConvertTo(targetType, converterOptions);
 50            return new(true, convertedValue, null);
 51        }
 52        catch (Exception e)
 53        {
 54            return new(false, null, e);
 55        }
 56    }
 57
 58    /// <summary>
 59    /// Attempts to convert the source value into the destination type.
 60    /// </summary>
 61    public static T? ConvertTo<T>(this object? value, ObjectConverterOptions? converterOptions = null) => value != null 
 62
 63    private static JsonSerializerOptions? _defaultSerializerOptions;
 64    private static JsonSerializerOptions? _internalSerializerOptions;
 65
 66    private static JsonSerializerOptions DefaultSerializerOptions => _defaultSerializerOptions ??= new()
 67    {
 68        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
 69        PropertyNameCaseInsensitive = true,
 70        ReferenceHandler = ReferenceHandler.Preserve,
 71        Converters =
 72        {
 73            new JsonStringEnumConverter()
 74        },
 75        Encoder = JavaScriptEncoder.Create(UnicodeRanges.All)
 76    };
 77
 78    private static JsonSerializerOptions InternalSerializerOptions => _internalSerializerOptions ??= new()
 79    {
 80        Encoder = JavaScriptEncoder.Create(UnicodeRanges.All)
 81    };
 82
 83    /// <summary>
 84    /// Attempts to convert the source value into the destination type.
 85    /// </summary>
 86    [RequiresUnreferencedCode("The JsonSerializer type is not trim-compatible.")]
 87    public static object? ConvertTo(this object? value, Type targetType, ObjectConverterOptions? converterOptions = null
 88    {
 89        var strictMode = converterOptions?.StrictMode ?? StrictMode;
 90
 91        if (value == null)
 92            return null;
 93
 94        var sourceType = value.GetType();
 95
 96        if (targetType.IsAssignableFrom(sourceType))
 97            return value;
 98
 99        var serializerOptions = converterOptions?.SerializerOptions ?? DefaultSerializerOptions;
 100        var underlyingTargetType = Nullable.GetUnderlyingType(targetType) ?? targetType;
 101        var underlyingSourceType = Nullable.GetUnderlyingType(sourceType) ?? sourceType;
 102
 103        if (value is JsonElement { ValueKind: JsonValueKind.Number } jsonNumber && underlyingTargetType == typeof(string
 104            return jsonNumber.ToString().ConvertTo(underlyingTargetType);
 105
 106        if (value is JsonElement jsonElement)
 107        {
 108            if (jsonElement.ValueKind == JsonValueKind.String && underlyingTargetType != typeof(string))
 109                return jsonElement.GetString().ConvertTo(underlyingTargetType);
 110
 111            return jsonElement.Deserialize(targetType, serializerOptions);
 112        }
 113
 114        if (value is JsonNode jsonNode)
 115        {
 116            if (jsonNode is not JsonArray jsonArray)
 117            {
 118                var valueKind = jsonNode.GetValueKind();
 119                if (valueKind == JsonValueKind.Null)
 120                    return null;
 121
 122                if (valueKind == JsonValueKind.Undefined)
 123                    return null;
 124
 125                return underlyingTargetType switch
 126                {
 127                    { } t when t == typeof(bool) && valueKind == JsonValueKind.False => false,
 128                    { } t when t == typeof(bool) && valueKind == JsonValueKind.True => true,
 129                    { } t when t.IsNumericType() && valueKind == JsonValueKind.Number => ConvertTo(jsonNode.ToString(), 
 130                    { } t when t == typeof(string) && valueKind == JsonValueKind.String => jsonNode.ToString(),
 131                    { } t when t == typeof(string) => jsonNode.ToString(),
 132                    { } t when t == typeof(ExpandoObject) && jsonNode.GetValueKind() == JsonValueKind.Object => JsonSeri
 133                    { } t when t != typeof(object) || converterOptions?.DeserializeJsonObjectToObject == true => jsonNod
 134                    _ => jsonNode
 135                };
 136            }
 137
 138            // Convert to target type if target type is an array or a generic collection.
 139            if (targetType.IsArray || targetType.IsCollectionType())
 140            {
 141                // The element type of the source array is JsonObject. If the element type of the target array is Object
 142                // Deserializing normally would return an array of JsonElement instead of JsonObject, but we want to kee
 143                var targetElementType = targetType.IsArray ? targetType.GetElementType()! : targetType.GenericTypeArgume
 144
 145                if (targetElementType != typeof(object))
 146                    return jsonArray.Deserialize(targetType, serializerOptions);
 147            }
 148        }
 149
 150        if (underlyingSourceType == typeof(string) && !underlyingTargetType.IsPrimitive && underlyingTargetType != typeo
 151        {
 152            var stringValue = (string)value;
 153
 154            if (underlyingTargetType == typeof(byte[]))
 155            {
 156                // Byte arrays are serialized to base64, so in this case, we convert the string back to the requested ta
 157                return Convert.FromBase64String(stringValue);
 158            }
 159
 160            try
 161            {
 162                var firstChar = stringValue.TrimStart().FirstOrDefault();
 163
 164                if (firstChar is '{' or '[')
 165                    return JsonSerializer.Deserialize(stringValue, underlyingTargetType, serializerOptions);
 166            }
 167            catch (Exception e)
 168            {
 169                throw new TypeConversionException($"Failed to deserialize {stringValue} to {underlyingTargetType}", valu
 170            }
 171        }
 172
 173        if (targetType == typeof(object))
 174            return value;
 175
 176        if (underlyingTargetType.IsInstanceOfType(value))
 177            return value;
 178
 179        if (underlyingSourceType == underlyingTargetType)
 180            return value;
 181
 182        if (IsDateType(underlyingSourceType) && IsDateType(underlyingTargetType))
 183            return ConvertAnyDateType(value, underlyingTargetType);
 184
 185        var internalSerializerOptions = InternalSerializerOptions;
 186
 187        if (typeof(IDictionary<string, object>).IsAssignableFrom(underlyingSourceType) && (underlyingTargetType.IsClass 
 188        {
 189            if (typeof(ExpandoObject) == underlyingTargetType)
 190            {
 191                var expandoJson = JsonSerializer.Serialize(value, internalSerializerOptions);
 192                return ConvertTo(expandoJson, underlyingTargetType, converterOptions);
 193            }
 194
 195            if (typeof(IDictionary<string, object>).IsAssignableFrom(underlyingTargetType))
 196                return new Dictionary<string, object>((IDictionary<string, object>)value);
 197
 198            var sourceDictionary = (IDictionary<string, object>)value;
 199            var json = JsonSerializer.Serialize(sourceDictionary, internalSerializerOptions);
 200            return ConvertTo(json, underlyingTargetType, converterOptions);
 201        }
 202
 203        if (typeof(IEnumerable<object>).IsAssignableFrom(underlyingSourceType))
 204            if (underlyingTargetType == typeof(string))
 205                return JsonSerializer.Serialize(value, internalSerializerOptions);
 206
 207        var targetTypeConverter = TypeDescriptor.GetConverter(underlyingTargetType);
 208
 209        if (targetTypeConverter.CanConvertFrom(underlyingSourceType))
 210        {
 211            var isValid = targetTypeConverter.IsValid(value);
 212
 213            if (isValid)
 214                return targetTypeConverter.ConvertFrom(null, CultureInfo.InvariantCulture, value);
 215        }
 216
 217        var sourceTypeConverter = TypeDescriptor.GetConverter(underlyingSourceType);
 218
 219        if (sourceTypeConverter.CanConvertTo(underlyingTargetType))
 220        {
 221            // TypeConverter.IsValid is not supported for ConvertTo, so we have to try and ignore any exceptions.
 222            try
 223            {
 224                return sourceTypeConverter.ConvertTo(value, underlyingTargetType);
 225            }
 226            catch
 227            {
 228                // Ignore and try other conversion strategies.
 229            }
 230        }
 231
 232        if (underlyingTargetType.IsEnum)
 233        {
 234            if (underlyingSourceType == typeof(string))
 235                return Enum.Parse(underlyingTargetType, (string)value);
 236
 237            if (underlyingSourceType == typeof(int))
 238                return Enum.ToObject(underlyingTargetType, value);
 239
 240            if (underlyingSourceType == typeof(double))
 241                return Enum.ToObject(underlyingTargetType, Convert.ChangeType(value, typeof(int), CultureInfo.InvariantC
 242
 243            if (underlyingSourceType == typeof(long))
 244                return Enum.ToObject(underlyingTargetType, Convert.ChangeType(value, typeof(int), CultureInfo.InvariantC
 245        }
 246
 247        if (value is string s)
 248        {
 249            if (string.IsNullOrWhiteSpace(s))
 250                return null;
 251
 252            if (underlyingTargetType == typeof(Type))
 253                return converterOptions?.WellKnownTypeRegistry != null ? converterOptions.WellKnownTypeRegistry.GetTypeO
 254
 255            // At this point, if the input is a string and the target type is IEnumerable<string>, assume the string is 
 256            if (typeof(IEnumerable<string>).IsAssignableFrom(underlyingTargetType))
 257                return new[]
 258                {
 259                    s
 260                };
 261        }
 262
 263        if (value is IEnumerable enumerable)
 264        {
 265            if (underlyingTargetType is { IsGenericType: true })
 266            {
 267                var desiredCollectionItemType = targetType.GenericTypeArguments[0];
 268                var desiredCollectionType = typeof(ICollection<>).MakeGenericType(desiredCollectionItemType);
 269
 270                if (underlyingTargetType.IsAssignableFrom(desiredCollectionType) || desiredCollectionType.IsAssignableFr
 271                {
 272                    var collectionType = typeof(List<>).MakeGenericType(desiredCollectionItemType);
 273                    var collection = (IList)Activator.CreateInstance(collectionType)!;
 274
 275                    foreach (var item in enumerable)
 276                    {
 277                        var convertedItem = ConvertTo(item, desiredCollectionItemType);
 278                        collection.Add(convertedItem);
 279                    }
 280
 281                    return collection;
 282                }
 283            }
 284
 285            if (underlyingTargetType.IsArray)
 286            {
 287                var executedEnumerable = enumerable.Cast<object>().ToList();
 288                var underlyingTargetElementType = underlyingTargetType.GetElementType()!;
 289                var array = Array.CreateInstance(underlyingTargetElementType, executedEnumerable.Count);
 290                var index = 0;
 291                foreach (var item in executedEnumerable)
 292                {
 293                    var convertedItem = ConvertTo(item, underlyingTargetElementType);
 294                    array.SetValue(convertedItem, index);
 295                    index++;
 296                }
 297
 298                return array;
 299            }
 300        }
 301
 302        try
 303        {
 304            return Convert.ChangeType(value, underlyingTargetType, CultureInfo.InvariantCulture);
 305        }
 306        catch (FormatException e)
 307        {
 308            return ReturnOrThrow(e);
 309        }
 310        catch (InvalidCastException e)
 311        {
 312            return ReturnOrThrow(e);
 313        }
 314
 315        object? ReturnOrThrow(Exception e)
 316        {
 317            if (!strictMode)
 318                return targetType.GetDefaultValue(); // Backward compatibility: return default value if strict mode is o
 319
 320            throw new TypeConversionException($"Failed to convert an object of type {sourceType} to {underlyingTargetTyp
 321        }
 322    }
 323
 324    /// <summary>
 325    /// Returns true if the specified type is a date-like type, false otherwise.
 326    /// </summary>
 327    private static bool IsDateType(Type type)
 328    {
 329        var dateTypes = new[]
 330        {
 331            typeof(DateTime), typeof(DateTimeOffset), typeof(DateOnly)
 332        };
 333
 334        return dateTypes.Contains(type);
 335    }
 336
 337    /// <summary>
 338    /// Converts any date type to the specified target type.
 339    /// </summary>
 340    /// <param name="value">Any of <see cref="DateTime"/>, <see cref="DateTimeOffset"/> or <see cref="DateOnly"/>.</para
 341    /// <param name="targetType">Any of <see cref="DateTime"/>, <see cref="DateTimeOffset"/> or <see cref="DateOnly"/>.<
 342    /// <returns>The converted value.</returns>
 343    /// <exception cref="ArgumentException">Thrown if <paramref name="value"/> is not of type <see cref="DateTime"/>, <s
 344    private static object ConvertAnyDateType(object value, Type targetType)
 345    {
 346        return targetType switch
 347        {
 348            { } t when t == typeof(DateTime) => value switch
 349            {
 350                DateTime dateTime => dateTime,
 351                DateTimeOffset dateTimeOffset => dateTimeOffset.DateTime,
 352                DateOnly date => new(date.Year, date.Month, date.Day),
 353                _ => throw new ArgumentException("Invalid value type.")
 354            },
 355            { } t when t == typeof(DateTimeOffset) => value switch
 356            {
 357                DateTime dateTime => new(dateTime),
 358                DateTimeOffset dateTimeOffset => dateTimeOffset,
 359                DateOnly date => new(date.Year, date.Month, date.Day, 0, 0, 0, TimeSpan.Zero),
 360                _ => throw new ArgumentException("Invalid value type.")
 361            },
 362            { } t when t == typeof(DateOnly) => value switch
 363            {
 364                DateTime dateTime => new(dateTime.Year, dateTime.Month, dateTime.Day),
 365                DateTimeOffset dateTimeOffset => new(dateTimeOffset.Year, dateTimeOffset.Month, dateTimeOffset.Day),
 366                DateOnly date => date,
 367                _ => throw new ArgumentException("Invalid value type.")
 368            },
 369            _ => throw new ArgumentException("Invalid target type.")
 370        };
 371    }
 372}