< 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: 373
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.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>
 323424public record ObjectConverterOptions(
 56625    JsonSerializerOptions? SerializerOptions = null,
 026    IWellKnownTypeRegistry? WellKnownTypeRegistry = null,
 1527    bool DeserializeJsonObjectToObject = false,
 638628    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>
 40    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        {
 50            var convertedValue = value.ConvertTo(targetType, converterOptions);
 51            return new(true, convertedValue, null);
 52        }
 53        catch (Exception e)
 54        {
 55            return new(false, null, e);
 56        }
 57    }
 58
 59    /// <summary>
 60    /// Attempts to convert the source value into the destination type.
 61    /// </summary>
 62    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
 67    private static JsonSerializerOptions DefaultSerializerOptions => _defaultSerializerOptions ??= new()
 68    {
 69        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
 70        PropertyNameCaseInsensitive = true,
 71        ReferenceHandler = ReferenceHandler.Preserve,
 72        Converters =
 73        {
 74            new JsonStringEnumConverter()
 75        },
 76        Encoder = JavaScriptEncoder.Create(UnicodeRanges.All)
 77    };
 78
 79    private static JsonSerializerOptions InternalSerializerOptions => _internalSerializerOptions ??= new()
 80    {
 81        Encoder = JavaScriptEncoder.Create(UnicodeRanges.All)
 82    };
 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    {
 90        var strictMode = converterOptions?.StrictMode ?? StrictMode;
 91
 92        if (value == null)
 93            return null;
 94
 95        var sourceType = value.GetType();
 96
 97        if (targetType.IsAssignableFrom(sourceType))
 98            return value;
 99
 100        var serializerOptions = converterOptions?.SerializerOptions ?? DefaultSerializerOptions;
 101        var underlyingTargetType = Nullable.GetUnderlyingType(targetType) ?? targetType;
 102        var underlyingSourceType = Nullable.GetUnderlyingType(sourceType) ?? sourceType;
 103
 104        if (value is JsonElement { ValueKind: JsonValueKind.Number } jsonNumber && underlyingTargetType == typeof(string
 105            return jsonNumber.ToString().ConvertTo(underlyingTargetType);
 106
 107        if (value is JsonElement jsonElement)
 108        {
 109            if (jsonElement.ValueKind == JsonValueKind.String && underlyingTargetType != typeof(string))
 110                return jsonElement.GetString().ConvertTo(underlyingTargetType);
 111
 112            return jsonElement.Deserialize(targetType, serializerOptions);
 113        }
 114
 115        if (value is JsonNode jsonNode)
 116        {
 117            if (jsonNode is not JsonArray jsonArray)
 118            {
 119                var valueKind = jsonNode.GetValueKind();
 120                if (valueKind == JsonValueKind.Null)
 121                    return null;
 122
 123                if (valueKind == JsonValueKind.Undefined)
 124                    return null;
 125
 126                return underlyingTargetType switch
 127                {
 128                    { } t when t == typeof(bool) && valueKind == JsonValueKind.False => false,
 129                    { } t when t == typeof(bool) && valueKind == JsonValueKind.True => true,
 130                    { } t when t.IsNumericType() && valueKind == JsonValueKind.Number => ConvertTo(jsonNode.ToString(), 
 131                    { } t when t == typeof(string) && valueKind == JsonValueKind.String => jsonNode.ToString(),
 132                    { } t when t == typeof(string) => jsonNode.ToString(),
 133                    { } t when t == typeof(ExpandoObject) && jsonNode.GetValueKind() == JsonValueKind.Object => JsonSeri
 134                    { } t when t != typeof(object) || converterOptions?.DeserializeJsonObjectToObject == true => jsonNod
 135                    _ => jsonNode
 136                };
 137            }
 138
 139            // Convert to target type if target type is an array or a generic collection.
 140            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
 144                var targetElementType = targetType.IsArray ? targetType.GetElementType()! : targetType.GenericTypeArgume
 145
 146                if (targetElementType != typeof(object))
 147                    return jsonArray.Deserialize(targetType, serializerOptions);
 148            }
 149        }
 150
 151        if (underlyingSourceType == typeof(string) && !underlyingTargetType.IsPrimitive && underlyingTargetType != typeo
 152        {
 153            var stringValue = (string)value;
 154
 155            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
 158                return Convert.FromBase64String(stringValue);
 159            }
 160
 161            try
 162            {
 163                var firstChar = stringValue.TrimStart().FirstOrDefault();
 164
 165                if (firstChar is '{' or '[')
 166                    return JsonSerializer.Deserialize(stringValue, underlyingTargetType, serializerOptions);
 167            }
 168            catch (Exception e)
 169            {
 170                throw new TypeConversionException($"Failed to deserialize {stringValue} to {underlyingTargetType}", valu
 171            }
 172        }
 173
 174        if (targetType == typeof(object))
 175            return value;
 176
 177        if (underlyingTargetType.IsInstanceOfType(value))
 178            return value;
 179
 180        if (underlyingSourceType == underlyingTargetType)
 181            return value;
 182
 183        if (IsDateType(underlyingSourceType) && IsDateType(underlyingTargetType))
 184            return ConvertAnyDateType(value, underlyingTargetType);
 185
 186        var internalSerializerOptions = InternalSerializerOptions;
 187
 188        if (typeof(IDictionary<string, object>).IsAssignableFrom(underlyingSourceType) && (underlyingTargetType.IsClass 
 189        {
 190            if (typeof(ExpandoObject) == underlyingTargetType)
 191            {
 192                var expandoJson = JsonSerializer.Serialize(value, internalSerializerOptions);
 193                return ConvertTo(expandoJson, underlyingTargetType, converterOptions);
 194            }
 195
 196            if (typeof(IDictionary<string, object>).IsAssignableFrom(underlyingTargetType))
 197                return new Dictionary<string, object>((IDictionary<string, object>)value);
 198
 199            var sourceDictionary = (IDictionary<string, object>)value;
 200            var json = JsonSerializer.Serialize(sourceDictionary, internalSerializerOptions);
 201            return ConvertTo(json, underlyingTargetType, converterOptions);
 202        }
 203
 204        if (typeof(IEnumerable<object>).IsAssignableFrom(underlyingSourceType))
 205            if (underlyingTargetType == typeof(string))
 206                return JsonSerializer.Serialize(value, internalSerializerOptions);
 207
 208        var targetTypeConverter = TypeDescriptor.GetConverter(underlyingTargetType);
 209
 210        if (targetTypeConverter.CanConvertFrom(underlyingSourceType))
 211        {
 212            var isValid = targetTypeConverter.IsValid(value);
 213
 214            if (isValid)
 215                return targetTypeConverter.ConvertFrom(null, CultureInfo.InvariantCulture, value);
 216        }
 217
 218        var sourceTypeConverter = TypeDescriptor.GetConverter(underlyingSourceType);
 219
 220        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            {
 225                return sourceTypeConverter.ConvertTo(value, underlyingTargetType);
 226            }
 227            catch
 228            {
 229                // Ignore and try other conversion strategies.
 230            }
 231        }
 232
 233        if (underlyingTargetType.IsEnum)
 234        {
 235            if (underlyingSourceType == typeof(string))
 236                return Enum.Parse(underlyingTargetType, (string)value);
 237
 238            if (underlyingSourceType == typeof(int))
 239                return Enum.ToObject(underlyingTargetType, value);
 240
 241            if (underlyingSourceType == typeof(double))
 242                return Enum.ToObject(underlyingTargetType, Convert.ChangeType(value, typeof(int), CultureInfo.InvariantC
 243
 244            if (underlyingSourceType == typeof(long))
 245                return Enum.ToObject(underlyingTargetType, Convert.ChangeType(value, typeof(int), CultureInfo.InvariantC
 246        }
 247
 248        if (value is string s)
 249        {
 250            if (string.IsNullOrWhiteSpace(s))
 251                return null;
 252
 253            if (underlyingTargetType == typeof(Type))
 254                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 
 257            if (typeof(IEnumerable<string>).IsAssignableFrom(underlyingTargetType))
 258                return new[]
 259                {
 260                    s
 261                };
 262        }
 263
 264        if (value is IEnumerable enumerable)
 265        {
 266            if (underlyingTargetType is { IsGenericType: true })
 267            {
 268                var desiredCollectionItemType = targetType.GenericTypeArguments[0];
 269                var desiredCollectionType = typeof(ICollection<>).MakeGenericType(desiredCollectionItemType);
 270
 271                if (underlyingTargetType.IsAssignableFrom(desiredCollectionType) || desiredCollectionType.IsAssignableFr
 272                {
 273                    var collectionType = typeof(List<>).MakeGenericType(desiredCollectionItemType);
 274                    var collection = (IList)Activator.CreateInstance(collectionType)!;
 275
 276                    foreach (var item in enumerable)
 277                    {
 278                        var convertedItem = ConvertTo(item, desiredCollectionItemType);
 279                        collection.Add(convertedItem);
 280                    }
 281
 282                    return collection;
 283                }
 284            }
 285
 286            if (underlyingTargetType.IsArray)
 287            {
 288                var executedEnumerable = enumerable.Cast<object>().ToList();
 289                var underlyingTargetElementType = underlyingTargetType.GetElementType()!;
 290                var array = Array.CreateInstance(underlyingTargetElementType, executedEnumerable.Count);
 291                var index = 0;
 292                foreach (var item in executedEnumerable)
 293                {
 294                    var convertedItem = ConvertTo(item, underlyingTargetElementType);
 295                    array.SetValue(convertedItem, index);
 296                    index++;
 297                }
 298
 299                return array;
 300            }
 301        }
 302
 303        try
 304        {
 305            return Convert.ChangeType(value, underlyingTargetType, CultureInfo.InvariantCulture);
 306        }
 307        catch (FormatException e)
 308        {
 309            return ReturnOrThrow(e);
 310        }
 311        catch (InvalidCastException e)
 312        {
 313            return ReturnOrThrow(e);
 314        }
 315
 316        object? ReturnOrThrow(Exception e)
 317        {
 318            if (!strictMode)
 319                return targetType.GetDefaultValue(); // Backward compatibility: return default value if strict mode is o
 320
 321            throw new TypeConversionException($"Failed to convert an object of type {sourceType} to {underlyingTargetTyp
 322        }
 323    }
 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    {
 330        var dateTypes = new[]
 331        {
 332            typeof(DateTime), typeof(DateTimeOffset), typeof(DateOnly)
 333        };
 334
 335        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    {
 347        return targetType switch
 348        {
 349            { } t when t == typeof(DateTime) => value switch
 350            {
 351                DateTime dateTime => dateTime,
 352                DateTimeOffset dateTimeOffset => dateTimeOffset.DateTime,
 353                DateOnly date => new(date.Year, date.Month, date.Day),
 354                _ => throw new ArgumentException("Invalid value type.")
 355            },
 356            { } t when t == typeof(DateTimeOffset) => value switch
 357            {
 358                DateTime dateTime => new(dateTime),
 359                DateTimeOffset dateTimeOffset => dateTimeOffset,
 360                DateOnly date => new(date.Year, date.Month, date.Day, 0, 0, 0, TimeSpan.Zero),
 361                _ => throw new ArgumentException("Invalid value type.")
 362            },
 363            { } t when t == typeof(DateOnly) => value switch
 364            {
 365                DateTime dateTime => new(dateTime.Year, dateTime.Month, dateTime.Day),
 366                DateTimeOffset dateTimeOffset => new(dateTimeOffset.Year, dateTimeOffset.Month, dateTimeOffset.Day),
 367                DateOnly date => date,
 368                _ => throw new ArgumentException("Invalid value type.")
 369            },
 370            _ => throw new ArgumentException("Invalid target type.")
 371        };
 372    }
 373}