< 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: 372
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.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>
 23public record ObjectConverterOptions(
 24    JsonSerializerOptions? SerializerOptions = null,
 25    IWellKnownTypeRegistry? WellKnownTypeRegistry = null,
 26    bool DeserializeJsonObjectToObject = false,
 27    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>
 1600939    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        {
 1636349            var convertedValue = value.ConvertTo(targetType, converterOptions);
 1636350            return new(true, convertedValue, null);
 51        }
 052        catch (Exception e)
 53        {
 054            return new(false, null, e);
 55        }
 1636356    }
 57
 58    /// <summary>
 59    /// Attempts to convert the source value into the destination type.
 60    /// </summary>
 158061    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
 59166    private static JsonSerializerOptions DefaultSerializerOptions => _defaultSerializerOptions ??= new()
 59167    {
 59168        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
 59169        PropertyNameCaseInsensitive = true,
 59170        ReferenceHandler = ReferenceHandler.Preserve,
 59171        Converters =
 59172        {
 59173            new JsonStringEnumConverter()
 59174        },
 59175        Encoder = JavaScriptEncoder.Create(UnicodeRanges.All)
 59176    };
 77
 51478    private static JsonSerializerOptions InternalSerializerOptions => _internalSerializerOptions ??= new()
 51479    {
 51480        Encoder = JavaScriptEncoder.Create(UnicodeRanges.All)
 51481    };
 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    {
 1949789        var strictMode = converterOptions?.StrictMode ?? StrictMode;
 90
 1949791        if (value == null)
 41192            return null;
 93
 1908694        var sourceType = value.GetType();
 95
 1908696        if (targetType.IsAssignableFrom(sourceType))
 1824997            return value;
 98
 83799        var serializerOptions = converterOptions?.SerializerOptions ?? DefaultSerializerOptions;
 837100        var underlyingTargetType = Nullable.GetUnderlyingType(targetType) ?? targetType;
 837101        var underlyingSourceType = Nullable.GetUnderlyingType(sourceType) ?? sourceType;
 102
 837103        if (value is JsonElement { ValueKind: JsonValueKind.Number } jsonNumber && underlyingTargetType == typeof(string
 0104            return jsonNumber.ToString().ConvertTo(underlyingTargetType);
 105
 837106        if (value is JsonElement jsonElement)
 107        {
 34108            if (jsonElement.ValueKind == JsonValueKind.String && underlyingTargetType != typeof(string))
 11109                return jsonElement.GetString().ConvertTo(underlyingTargetType);
 110
 23111            return jsonElement.Deserialize(targetType, serializerOptions);
 112        }
 113
 803114        if (value is JsonNode jsonNode)
 115        {
 24116            if (jsonNode is not JsonArray jsonArray)
 117            {
 18118                var valueKind = jsonNode.GetValueKind();
 18119                if (valueKind == JsonValueKind.Null)
 0120                    return null;
 121
 18122                if (valueKind == JsonValueKind.Undefined)
 0123                    return null;
 124
 18125                return underlyingTargetType switch
 18126                {
 18127                    { } t when t == typeof(bool) && valueKind == JsonValueKind.False => false,
 18128                    { } t when t == typeof(bool) && valueKind == JsonValueKind.True => true,
 35129                    { } t when t.IsNumericType() && valueKind == JsonValueKind.Number => ConvertTo(jsonNode.ToString(), 
 2130                    { } t when t == typeof(string) && valueKind == JsonValueKind.String => jsonNode.ToString(),
 0131                    { } t when t == typeof(string) => jsonNode.ToString(),
 0132                    { } t when t == typeof(ExpandoObject) && jsonNode.GetValueKind() == JsonValueKind.Object => JsonSeri
 0133                    { } t when t != typeof(object) || converterOptions?.DeserializeJsonObjectToObject == true => jsonNod
 0134                    _ => jsonNode
 18135                };
 136            }
 137
 138            // Convert to target type if target type is an array or a generic collection.
 6139            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
 6143                var targetElementType = targetType.IsArray ? targetType.GetElementType()! : targetType.GenericTypeArgume
 144
 6145                if (targetElementType != typeof(object))
 6146                    return jsonArray.Deserialize(targetType, serializerOptions);
 147            }
 148        }
 149
 779150        if (underlyingSourceType == typeof(string) && !underlyingTargetType.IsPrimitive && underlyingTargetType != typeo
 151        {
 521152            var stringValue = (string)value;
 153
 521154            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
 0157                return Convert.FromBase64String(stringValue);
 158            }
 159
 160            try
 161            {
 521162                var firstChar = stringValue.TrimStart().FirstOrDefault();
 163
 521164                if (firstChar is '{' or '[')
 265165                    return JsonSerializer.Deserialize(stringValue, underlyingTargetType, serializerOptions);
 256166            }
 0167            catch (Exception e)
 168            {
 0169                throw new TypeConversionException($"Failed to deserialize {stringValue} to {underlyingTargetType}", valu
 170            }
 171        }
 172
 514173        if (targetType == typeof(object))
 0174            return value;
 175
 514176        if (underlyingTargetType.IsInstanceOfType(value))
 0177            return value;
 178
 514179        if (underlyingSourceType == underlyingTargetType)
 0180            return value;
 181
 514182        if (IsDateType(underlyingSourceType) && IsDateType(underlyingTargetType))
 0183            return ConvertAnyDateType(value, underlyingTargetType);
 184
 514185        var internalSerializerOptions = InternalSerializerOptions;
 186
 514187        if (typeof(IDictionary<string, object>).IsAssignableFrom(underlyingSourceType) && (underlyingTargetType.IsClass 
 188        {
 16189            if (typeof(ExpandoObject) == underlyingTargetType)
 190            {
 0191                var expandoJson = JsonSerializer.Serialize(value, internalSerializerOptions);
 0192                return ConvertTo(expandoJson, underlyingTargetType, converterOptions);
 193            }
 194
 16195            if (typeof(IDictionary<string, object>).IsAssignableFrom(underlyingTargetType))
 16196                return new Dictionary<string, object>((IDictionary<string, object>)value);
 197
 0198            var sourceDictionary = (IDictionary<string, object>)value;
 0199            var json = JsonSerializer.Serialize(sourceDictionary, internalSerializerOptions);
 0200            return ConvertTo(json, underlyingTargetType, converterOptions);
 201        }
 202
 498203        if (typeof(IEnumerable<object>).IsAssignableFrom(underlyingSourceType))
 0204            if (underlyingTargetType == typeof(string))
 0205                return JsonSerializer.Serialize(value, internalSerializerOptions);
 206
 498207        var targetTypeConverter = TypeDescriptor.GetConverter(underlyingTargetType);
 208
 498209        if (targetTypeConverter.CanConvertFrom(underlyingSourceType))
 210        {
 439211            var isValid = targetTypeConverter.IsValid(value);
 212
 439213            if (isValid)
 439214                return targetTypeConverter.ConvertFrom(null, CultureInfo.InvariantCulture, value);
 215        }
 216
 59217        var sourceTypeConverter = TypeDescriptor.GetConverter(underlyingSourceType);
 218
 59219        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            {
 59224                return sourceTypeConverter.ConvertTo(value, underlyingTargetType);
 225            }
 0226            catch
 227            {
 228                // Ignore and try other conversion strategies.
 0229            }
 230        }
 231
 0232        if (underlyingTargetType.IsEnum)
 233        {
 0234            if (underlyingSourceType == typeof(string))
 0235                return Enum.Parse(underlyingTargetType, (string)value);
 236
 0237            if (underlyingSourceType == typeof(int))
 0238                return Enum.ToObject(underlyingTargetType, value);
 239
 0240            if (underlyingSourceType == typeof(double))
 0241                return Enum.ToObject(underlyingTargetType, Convert.ChangeType(value, typeof(int), CultureInfo.InvariantC
 242
 0243            if (underlyingSourceType == typeof(long))
 0244                return Enum.ToObject(underlyingTargetType, Convert.ChangeType(value, typeof(int), CultureInfo.InvariantC
 245        }
 246
 0247        if (value is string s)
 248        {
 0249            if (string.IsNullOrWhiteSpace(s))
 0250                return null;
 251
 0252            if (underlyingTargetType == typeof(Type))
 0253                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 
 0256            if (typeof(IEnumerable<string>).IsAssignableFrom(underlyingTargetType))
 0257                return new[]
 0258                {
 0259                    s
 0260                };
 261        }
 262
 0263        if (value is IEnumerable enumerable)
 264        {
 0265            if (underlyingTargetType is { IsGenericType: true })
 266            {
 0267                var desiredCollectionItemType = targetType.GenericTypeArguments[0];
 0268                var desiredCollectionType = typeof(ICollection<>).MakeGenericType(desiredCollectionItemType);
 269
 0270                if (underlyingTargetType.IsAssignableFrom(desiredCollectionType) || desiredCollectionType.IsAssignableFr
 271                {
 0272                    var collectionType = typeof(List<>).MakeGenericType(desiredCollectionItemType);
 0273                    var collection = (IList)Activator.CreateInstance(collectionType)!;
 274
 0275                    foreach (var item in enumerable)
 276                    {
 0277                        var convertedItem = ConvertTo(item, desiredCollectionItemType);
 0278                        collection.Add(convertedItem);
 279                    }
 280
 0281                    return collection;
 282                }
 283            }
 284
 0285            if (underlyingTargetType.IsArray)
 286            {
 0287                var executedEnumerable = enumerable.Cast<object>().ToList();
 0288                var underlyingTargetElementType = underlyingTargetType.GetElementType()!;
 0289                var array = Array.CreateInstance(underlyingTargetElementType, executedEnumerable.Count);
 0290                var index = 0;
 0291                foreach (var item in executedEnumerable)
 292                {
 0293                    var convertedItem = ConvertTo(item, underlyingTargetElementType);
 0294                    array.SetValue(convertedItem, index);
 0295                    index++;
 296                }
 297
 0298                return array;
 299            }
 300        }
 301
 302        try
 303        {
 0304            return Convert.ChangeType(value, underlyingTargetType, CultureInfo.InvariantCulture);
 305        }
 306        catch (FormatException e)
 307        {
 0308            return ReturnOrThrow(e);
 309        }
 310        catch (InvalidCastException e)
 311        {
 0312            return ReturnOrThrow(e);
 313        }
 314
 315        object? ReturnOrThrow(Exception e)
 316        {
 0317            if (!strictMode)
 0318                return targetType.GetDefaultValue(); // Backward compatibility: return default value if strict mode is o
 319
 0320            throw new TypeConversionException($"Failed to convert an object of type {sourceType} to {underlyingTargetTyp
 321        }
 324322    }
 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    {
 514329        var dateTypes = new[]
 514330        {
 514331            typeof(DateTime), typeof(DateTimeOffset), typeof(DateOnly)
 514332        };
 333
 514334        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    {
 0346        return targetType switch
 0347        {
 0348            { } t when t == typeof(DateTime) => value switch
 0349            {
 0350                DateTime dateTime => dateTime,
 0351                DateTimeOffset dateTimeOffset => dateTimeOffset.DateTime,
 0352                DateOnly date => new(date.Year, date.Month, date.Day),
 0353                _ => throw new ArgumentException("Invalid value type.")
 0354            },
 0355            { } t when t == typeof(DateTimeOffset) => value switch
 0356            {
 0357                DateTime dateTime => new(dateTime),
 0358                DateTimeOffset dateTimeOffset => dateTimeOffset,
 0359                DateOnly date => new(date.Year, date.Month, date.Day, 0, 0, 0, TimeSpan.Zero),
 0360                _ => throw new ArgumentException("Invalid value type.")
 0361            },
 0362            { } t when t == typeof(DateOnly) => value switch
 0363            {
 0364                DateTime dateTime => new(dateTime.Year, dateTime.Month, dateTime.Day),
 0365                DateTimeOffset dateTimeOffset => new(dateTimeOffset.Year, dateTimeOffset.Month, dateTimeOffset.Day),
 0366                DateOnly date => date,
 0367                _ => throw new ArgumentException("Invalid value type.")
 0368            },
 0369            _ => throw new ArgumentException("Invalid target type.")
 0370        };
 371    }
 372}