< Summary

Information
Class: Elsa.Expressions.Helpers.ObjectConverter
Assembly: Elsa.Expressions
File(s): /home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Expressions/Helpers/ObjectConverter.cs
Line coverage
45%
Covered lines: 81
Uncovered lines: 96
Coverable lines: 177
Total lines: 373
Line coverage: 45.7%
Branch coverage
45%
Covered branches: 86
Total branches: 190
Branch coverage: 45.2%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
TryConvertTo(...)100%11100%
TryConvertTo(...)100%1160%
ConvertTo(...)100%22100%
get_DefaultSerializerOptions()100%22100%
get_InternalSerializerOptions()100%22100%
ConvertTo(...)51.28%400915645.9%
ReturnOrThrow()0%620%
IsDateType(...)100%11100%
ConvertAnyDateType(...)0%702260%

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.Common.Models;
 13using Elsa.Expressions.Contracts;
 14using Elsa.Expressions.Exceptions;
 15using Elsa.Expressions.Extensions;
 16using Elsa.Expressions.Models;
 17using Elsa.Extensions;
 18
 19namespace Elsa.Expressions.Helpers;
 20
 21/// <summary>
 22/// Provides options to the conversion method.
 23/// </summary>
 24public record ObjectConverterOptions(
 25    JsonSerializerOptions? SerializerOptions = null,
 26    IWellKnownTypeRegistry? WellKnownTypeRegistry = null,
 27    bool DeserializeJsonObjectToObject = false,
 28    bool? StrictMode = null);
 29
 30/// <summary>
 31/// A helper that attempts many strategies to try and convert the source value into the destination type.
 32/// </summary>
 33public static class ObjectConverter
 34{
 35    public static bool StrictMode = false; // Set to true to opt into strict mode.
 36
 37    /// <summary>
 38    /// Attempts to convert the source value into the destination type.
 39    /// </summary>
 1825340    public static Result TryConvertTo<T>(this object? value, ObjectConverterOptions? converterOptions = null) => value.T
 41
 42    /// <summary>
 43    /// Attempts to convert the source value into the destination type.
 44    /// </summary>
 45    [RequiresUnreferencedCode("The JsonSerializer type is not trim-compatible.")]
 46    public static Result TryConvertTo(this object? value, Type targetType, ObjectConverterOptions? converterOptions = nu
 47    {
 48        try
 49        {
 2068350            var convertedValue = value.ConvertTo(targetType, converterOptions);
 2068351            return new(true, convertedValue, null);
 52        }
 053        catch (Exception e)
 54        {
 055            return new(false, null, e);
 56        }
 2068357    }
 58
 59    /// <summary>
 60    /// Attempts to convert the source value into the destination type.
 61    /// </summary>
 280562    public static T? ConvertTo<T>(this object? value, ObjectConverterOptions? converterOptions = null) => value != null 
 63
 64    private static JsonSerializerOptions? _defaultSerializerOptions;
 65    private static JsonSerializerOptions? _internalSerializerOptions;
 66
 134867    private static JsonSerializerOptions DefaultSerializerOptions => _defaultSerializerOptions ??= new()
 134868    {
 134869        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
 134870        PropertyNameCaseInsensitive = true,
 134871        ReferenceHandler = ReferenceHandler.Preserve,
 134872        Converters =
 134873        {
 134874            new JsonStringEnumConverter()
 134875        },
 134876        Encoder = JavaScriptEncoder.Create(UnicodeRanges.All)
 134877    };
 78
 117279    private static JsonSerializerOptions InternalSerializerOptions => _internalSerializerOptions ??= new()
 117280    {
 117281        Encoder = JavaScriptEncoder.Create(UnicodeRanges.All)
 117282    };
 83
 84    /// <summary>
 85    /// Attempts to convert the source value into the destination type.
 86    /// </summary>
 87    [RequiresUnreferencedCode("The JsonSerializer type is not trim-compatible.")]
 88    public static object? ConvertTo(this object? value, Type targetType, ObjectConverterOptions? converterOptions = null
 89    {
 2613090        var strictMode = converterOptions?.StrictMode ?? StrictMode;
 91
 2613092        if (value == null)
 205493            return null;
 94
 2407695        var sourceType = value.GetType();
 96
 2407697        if (targetType.IsAssignableFrom(sourceType))
 2247698            return value;
 99
 1600100        var serializerOptions = converterOptions?.SerializerOptions ?? DefaultSerializerOptions;
 1600101        var underlyingTargetType = Nullable.GetUnderlyingType(targetType) ?? targetType;
 1600102        var underlyingSourceType = Nullable.GetUnderlyingType(sourceType) ?? sourceType;
 103
 1600104        if (value is JsonElement { ValueKind: JsonValueKind.Number } jsonNumber && underlyingTargetType == typeof(string
 0105            return jsonNumber.ToString().ConvertTo(underlyingTargetType);
 106
 1600107        if (value is JsonElement jsonElement)
 108        {
 34109            if (jsonElement.ValueKind == JsonValueKind.String && underlyingTargetType != typeof(string))
 11110                return jsonElement.GetString().ConvertTo(underlyingTargetType);
 111
 23112            return jsonElement.Deserialize(targetType, serializerOptions);
 113        }
 114
 1566115        if (value is JsonNode jsonNode)
 116        {
 24117            if (jsonNode is not JsonArray jsonArray)
 118            {
 18119                var valueKind = jsonNode.GetValueKind();
 18120                if (valueKind == JsonValueKind.Null)
 0121                    return null;
 122
 18123                if (valueKind == JsonValueKind.Undefined)
 0124                    return null;
 125
 18126                return underlyingTargetType switch
 18127                {
 18128                    { } t when t == typeof(bool) && valueKind == JsonValueKind.False => false,
 18129                    { } t when t == typeof(bool) && valueKind == JsonValueKind.True => true,
 35130                    { } t when t.IsNumericType() && valueKind == JsonValueKind.Number => ConvertTo(jsonNode.ToString(), 
 2131                    { } t when t == typeof(string) && valueKind == JsonValueKind.String => jsonNode.ToString(),
 0132                    { } t when t == typeof(string) => jsonNode.ToString(),
 0133                    { } t when t == typeof(ExpandoObject) && jsonNode.GetValueKind() == JsonValueKind.Object => JsonSeri
 0134                    { } t when t != typeof(object) || converterOptions?.DeserializeJsonObjectToObject == true => jsonNod
 0135                    _ => jsonNode
 18136                };
 137            }
 138
 139            // Convert to target type if target type is an array or a generic collection.
 6140            if (targetType.IsArray || targetType.IsCollectionType())
 141            {
 142                // The element type of the source array is JsonObject. If the element type of the target array is Object
 143                // Deserializing normally would return an array of JsonElement instead of JsonObject, but we want to kee
 6144                var targetElementType = targetType.IsArray ? targetType.GetElementType()! : targetType.GenericTypeArgume
 145
 6146                if (targetElementType != typeof(object))
 6147                    return jsonArray.Deserialize(targetType, serializerOptions);
 148            }
 149        }
 150
 1542151        if (underlyingSourceType == typeof(string) && !underlyingTargetType.IsPrimitive && underlyingTargetType != typeo
 152        {
 677153            var stringValue = (string)value;
 154
 677155            if (underlyingTargetType == typeof(byte[]))
 156            {
 157                // Byte arrays are serialized to base64, so in this case, we convert the string back to the requested ta
 0158                return Convert.FromBase64String(stringValue);
 159            }
 160
 161            try
 162            {
 677163                var firstChar = stringValue.TrimStart().FirstOrDefault();
 164
 677165                if (firstChar is '{' or '[')
 370166                    return JsonSerializer.Deserialize(stringValue, underlyingTargetType, serializerOptions);
 307167            }
 0168            catch (Exception e)
 169            {
 0170                throw new TypeConversionException($"Failed to deserialize {stringValue} to {underlyingTargetType}", valu
 171            }
 172        }
 173
 1172174        if (targetType == typeof(object))
 0175            return value;
 176
 1172177        if (underlyingTargetType.IsInstanceOfType(value))
 0178            return value;
 179
 1172180        if (underlyingSourceType == underlyingTargetType)
 0181            return value;
 182
 1172183        if (IsDateType(underlyingSourceType) && IsDateType(underlyingTargetType))
 0184            return ConvertAnyDateType(value, underlyingTargetType);
 185
 1172186        var internalSerializerOptions = InternalSerializerOptions;
 187
 1172188        if (typeof(IDictionary<string, object>).IsAssignableFrom(underlyingSourceType) && (underlyingTargetType.IsClass 
 189        {
 16190            if (typeof(ExpandoObject) == underlyingTargetType)
 191            {
 0192                var expandoJson = JsonSerializer.Serialize(value, internalSerializerOptions);
 0193                return ConvertTo(expandoJson, underlyingTargetType, converterOptions);
 194            }
 195
 16196            if (typeof(IDictionary<string, object>).IsAssignableFrom(underlyingTargetType))
 16197                return new Dictionary<string, object>((IDictionary<string, object>)value);
 198
 0199            var sourceDictionary = (IDictionary<string, object>)value;
 0200            var json = JsonSerializer.Serialize(sourceDictionary, internalSerializerOptions);
 0201            return ConvertTo(json, underlyingTargetType, converterOptions);
 202        }
 203
 1156204        if (typeof(IEnumerable<object>).IsAssignableFrom(underlyingSourceType))
 0205            if (underlyingTargetType == typeof(string))
 0206                return JsonSerializer.Serialize(value, internalSerializerOptions);
 207
 1156208        var targetTypeConverter = TypeDescriptor.GetConverter(underlyingTargetType);
 209
 1156210        if (targetTypeConverter.CanConvertFrom(underlyingSourceType))
 211        {
 1091212            var isValid = targetTypeConverter.IsValid(value);
 213
 1091214            if (isValid)
 1091215                return targetTypeConverter.ConvertFrom(null, CultureInfo.InvariantCulture, value);
 216        }
 217
 65218        var sourceTypeConverter = TypeDescriptor.GetConverter(underlyingSourceType);
 219
 65220        if (sourceTypeConverter.CanConvertTo(underlyingTargetType))
 221        {
 222            // TypeConverter.IsValid is not supported for ConvertTo, so we have to try and ignore any exceptions.
 223            try
 224            {
 65225                return sourceTypeConverter.ConvertTo(value, underlyingTargetType);
 226            }
 0227            catch
 228            {
 229                // Ignore and try other conversion strategies.
 0230            }
 231        }
 232
 0233        if (underlyingTargetType.IsEnum)
 234        {
 0235            if (underlyingSourceType == typeof(string))
 0236                return Enum.Parse(underlyingTargetType, (string)value);
 237
 0238            if (underlyingSourceType == typeof(int))
 0239                return Enum.ToObject(underlyingTargetType, value);
 240
 0241            if (underlyingSourceType == typeof(double))
 0242                return Enum.ToObject(underlyingTargetType, Convert.ChangeType(value, typeof(int), CultureInfo.InvariantC
 243
 0244            if (underlyingSourceType == typeof(long))
 0245                return Enum.ToObject(underlyingTargetType, Convert.ChangeType(value, typeof(int), CultureInfo.InvariantC
 246        }
 247
 0248        if (value is string s)
 249        {
 0250            if (string.IsNullOrWhiteSpace(s))
 0251                return null;
 252
 0253            if (underlyingTargetType == typeof(Type))
 0254                return converterOptions?.WellKnownTypeRegistry != null ? converterOptions.WellKnownTypeRegistry.GetTypeO
 255
 256            // At this point, if the input is a string and the target type is IEnumerable<string>, assume the string is 
 0257            if (typeof(IEnumerable<string>).IsAssignableFrom(underlyingTargetType))
 0258                return new[]
 0259                {
 0260                    s
 0261                };
 262        }
 263
 0264        if (value is IEnumerable enumerable)
 265        {
 0266            if (underlyingTargetType is { IsGenericType: true })
 267            {
 0268                var desiredCollectionItemType = targetType.GenericTypeArguments[0];
 0269                var desiredCollectionType = typeof(ICollection<>).MakeGenericType(desiredCollectionItemType);
 270
 0271                if (underlyingTargetType.IsAssignableFrom(desiredCollectionType) || desiredCollectionType.IsAssignableFr
 272                {
 0273                    var collectionType = typeof(List<>).MakeGenericType(desiredCollectionItemType);
 0274                    var collection = (IList)Activator.CreateInstance(collectionType)!;
 275
 0276                    foreach (var item in enumerable)
 277                    {
 0278                        var convertedItem = ConvertTo(item, desiredCollectionItemType);
 0279                        collection.Add(convertedItem);
 280                    }
 281
 0282                    return collection;
 283                }
 284            }
 285
 0286            if (underlyingTargetType.IsArray)
 287            {
 0288                var executedEnumerable = enumerable.Cast<object>().ToList();
 0289                var underlyingTargetElementType = underlyingTargetType.GetElementType()!;
 0290                var array = Array.CreateInstance(underlyingTargetElementType, executedEnumerable.Count);
 0291                var index = 0;
 0292                foreach (var item in executedEnumerable)
 293                {
 0294                    var convertedItem = ConvertTo(item, underlyingTargetElementType);
 0295                    array.SetValue(convertedItem, index);
 0296                    index++;
 297                }
 298
 0299                return array;
 300            }
 301        }
 302
 303        try
 304        {
 0305            return Convert.ChangeType(value, underlyingTargetType, CultureInfo.InvariantCulture);
 306        }
 307        catch (FormatException e)
 308        {
 0309            return ReturnOrThrow(e);
 310        }
 311        catch (InvalidCastException e)
 312        {
 0313            return ReturnOrThrow(e);
 314        }
 315
 316        object? ReturnOrThrow(Exception e)
 317        {
 0318            if (!strictMode)
 0319                return targetType.GetDefaultValue(); // Backward compatibility: return default value if strict mode is o
 320
 0321            throw new TypeConversionException($"Failed to convert an object of type {sourceType} to {underlyingTargetTyp
 322        }
 435323    }
 324
 325    /// <summary>
 326    /// Returns true if the specified type is a date-like type, false otherwise.
 327    /// </summary>
 328    private static bool IsDateType(Type type)
 329    {
 1172330        var dateTypes = new[]
 1172331        {
 1172332            typeof(DateTime), typeof(DateTimeOffset), typeof(DateOnly)
 1172333        };
 334
 1172335        return dateTypes.Contains(type);
 336    }
 337
 338    /// <summary>
 339    /// Converts any date type to the specified target type.
 340    /// </summary>
 341    /// <param name="value">Any of <see cref="DateTime"/>, <see cref="DateTimeOffset"/> or <see cref="DateOnly"/>.</para
 342    /// <param name="targetType">Any of <see cref="DateTime"/>, <see cref="DateTimeOffset"/> or <see cref="DateOnly"/>.<
 343    /// <returns>The converted value.</returns>
 344    /// <exception cref="ArgumentException">Thrown if <paramref name="value"/> is not of type <see cref="DateTime"/>, <s
 345    private static object ConvertAnyDateType(object value, Type targetType)
 346    {
 0347        return targetType switch
 0348        {
 0349            { } t when t == typeof(DateTime) => value switch
 0350            {
 0351                DateTime dateTime => dateTime,
 0352                DateTimeOffset dateTimeOffset => dateTimeOffset.DateTime,
 0353                DateOnly date => new(date.Year, date.Month, date.Day),
 0354                _ => throw new ArgumentException("Invalid value type.")
 0355            },
 0356            { } t when t == typeof(DateTimeOffset) => value switch
 0357            {
 0358                DateTime dateTime => new(dateTime),
 0359                DateTimeOffset dateTimeOffset => dateTimeOffset,
 0360                DateOnly date => new(date.Year, date.Month, date.Day, 0, 0, 0, TimeSpan.Zero),
 0361                _ => throw new ArgumentException("Invalid value type.")
 0362            },
 0363            { } t when t == typeof(DateOnly) => value switch
 0364            {
 0365                DateTime dateTime => new(dateTime.Year, dateTime.Month, dateTime.Day),
 0366                DateTimeOffset dateTimeOffset => new(dateTimeOffset.Year, dateTimeOffset.Month, dateTimeOffset.Day),
 0367                DateOnly date => date,
 0368                _ => throw new ArgumentException("Invalid value type.")
 0369            },
 0370            _ => throw new ArgumentException("Invalid target type.")
 0371        };
 372    }
 373}