< Summary

Information
Class: Elsa.Api.Client.Extensions.ObjectConverter
Assembly: Elsa.Api.Client
File(s): /home/runner/work/elsa-core/elsa-core/src/clients/Elsa.Api.Client/Extensions/ObjectConverter.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 106
Coverable lines: 106
Total lines: 229
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 108
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
ConvertTo(...)0%620%
ConvertTo(...)0%8556920%
IsDateType(...)100%210%
ConvertAnyDateType(...)0%210140%

File(s)

/home/runner/work/elsa-core/elsa-core/src/clients/Elsa.Api.Client/Extensions/ObjectConverter.cs

#LineLine coverage
 1using System.Collections;
 2using System.ComponentModel;
 3using System.Dynamic;
 4using System.Globalization;
 5using System.Text.Json;
 6using System.Text.Json.Nodes;
 7using System.Text.Json.Serialization;
 8
 9namespace Elsa.Api.Client.Extensions;
 10
 11/// <summary>
 12/// Provides options to the conversion method.
 13/// </summary>
 14public record ObjectConverterOptions(JsonSerializerOptions? SerializerOptions = default)
 15{
 16    /// <inheritdoc />
 17    public ObjectConverterOptions(Action<JsonSerializerOptions> configureSerializerOptions) : this()
 18    {
 19        SerializerOptions = new JsonSerializerOptions();
 20        configureSerializerOptions(SerializerOptions);
 21    }
 22}
 23
 24/// <summary>
 25/// A helper that attempts many strategies to try and convert the source value into the destination type.
 26/// </summary>
 27public static class ObjectConverter
 28{
 29    /// <summary>
 30    /// Attempts to convert the source value into the destination type.
 31    /// </summary>
 032    public static T? ConvertTo<T>(this object? value, ObjectConverterOptions? serializerOptions = null) => value != null
 33
 34    /// <summary>
 35    /// Attempts to convert the source value into the destination type.
 36    /// </summary>
 37    public static object? ConvertTo(this object? value, Type targetType, ObjectConverterOptions? converterOptions = null
 38    {
 039        if (value == null)
 040            return default!;
 41
 042        var sourceType = value.GetType();
 43
 044        if (sourceType == targetType)
 045            return value;
 46
 047        var options = converterOptions?.SerializerOptions != null ? new JsonSerializerOptions(converterOptions.Serialize
 048        options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
 049        options.ReferenceHandler = ReferenceHandler.Preserve;
 050        options.PropertyNameCaseInsensitive = true;
 051        options.Converters.Add(new JsonStringEnumConverter());
 52
 053        var underlyingTargetType = Nullable.GetUnderlyingType(targetType) ?? targetType;
 054        var underlyingSourceType = Nullable.GetUnderlyingType(sourceType) ?? sourceType;
 55
 056        if (value is JsonElement { ValueKind: JsonValueKind.Number } jsonNumber && underlyingTargetType == typeof(string
 057            return jsonNumber.ToString().ConvertTo(underlyingTargetType);
 58
 059        if (value is JsonElement jsonElement)
 60        {
 061            if (jsonElement.ValueKind == JsonValueKind.String && underlyingTargetType != typeof(string))
 062                return jsonElement.GetString().ConvertTo(underlyingTargetType);
 63
 064            return jsonElement.Deserialize(targetType, options);
 65        }
 66
 067        if (value is JsonNode jsonNode)
 068            return jsonNode.Deserialize(targetType, options);
 69
 070        if (underlyingSourceType == typeof(string) && !underlyingTargetType.IsPrimitive && underlyingTargetType != typeo
 71        {
 072            var stringValue = (string)value;
 73
 74            try
 75            {
 076                var firstChar = stringValue.TrimStart().FirstOrDefault();
 77
 078                if (firstChar is '{' or '[')
 079                    return JsonSerializer.Deserialize(stringValue, underlyingTargetType, options);
 080            }
 081            catch (Exception)
 82            {
 083                throw new Exception($"Failed to deserialize {stringValue} to {underlyingTargetType}");
 84            }
 85        }
 86
 087        if (targetType == typeof(object))
 088            return value;
 89
 090        if (underlyingTargetType.IsInstanceOfType(value))
 091            return value;
 92
 093        if (underlyingSourceType == underlyingTargetType)
 094            return value;
 95
 096        if (IsDateType(underlyingSourceType) && IsDateType(underlyingTargetType))
 097            return ConvertAnyDateType(value, underlyingTargetType);
 98
 099        if (typeof(IDictionary<string, object>).IsAssignableFrom(underlyingSourceType) && underlyingTargetType.IsClass)
 100        {
 0101            if (typeof(ExpandoObject) == underlyingTargetType)
 102            {
 0103                var expandoJson = JsonSerializer.Serialize(value);
 0104                return ConvertTo(expandoJson, underlyingTargetType, converterOptions);
 105            }
 106
 0107            if (typeof(IDictionary<string, object>).IsAssignableFrom(underlyingTargetType))
 0108                return new Dictionary<string, object>((IDictionary<string, object>)value);
 109
 0110            if (typeof(ExpandoObject) == underlyingSourceType)
 111            {
 112                // Parse ExpandoObject into target type.
 0113                var expandoObject = (IDictionary<string, object>)value;
 0114                var json = JsonSerializer.Serialize(expandoObject);
 0115                return ConvertTo(json, underlyingTargetType, converterOptions);
 116            }
 117        }
 118
 0119        var targetTypeConverter = TypeDescriptor.GetConverter(underlyingTargetType);
 120
 0121        if (targetTypeConverter.CanConvertFrom(underlyingSourceType))
 0122            return targetTypeConverter.IsValid(value)
 0123                ? targetTypeConverter.ConvertFrom(null!, CultureInfo.InvariantCulture, value)
 0124                : targetType.GetDefaultValue();
 125
 0126        var sourceTypeConverter = TypeDescriptor.GetConverter(underlyingSourceType);
 127
 0128        if (sourceTypeConverter.CanConvertTo(underlyingTargetType))
 0129            return sourceTypeConverter.ConvertTo(value, underlyingTargetType);
 130
 0131        if (underlyingTargetType.IsEnum)
 132        {
 0133            if (underlyingSourceType == typeof(string))
 0134                return Enum.Parse(underlyingTargetType, (string)value);
 135
 0136            if (underlyingSourceType == typeof(int))
 0137                return Enum.ToObject(underlyingTargetType, value);
 138
 0139            if (underlyingSourceType == typeof(double))
 0140                return Enum.ToObject(underlyingTargetType, Convert.ChangeType(value, typeof(int), CultureInfo.InvariantC
 141        }
 142
 0143        if (value is string s)
 144        {
 0145            if (string.IsNullOrWhiteSpace(s))
 0146                return null;
 147
 0148            if (underlyingTargetType == typeof(Type))
 0149                return Type.GetType(s);
 150
 151            // Perhaps it's a bit of a leap, but if the input is a string and the target type is IEnumerable<string>, th
 0152            if (typeof(IEnumerable<string>).IsAssignableFrom(underlyingTargetType))
 0153                return new[] { s };
 154        }
 155
 0156        if (value is IEnumerable enumerable)
 157        {
 0158            if (underlyingTargetType is { IsGenericType: true })
 159            {
 0160                var desiredCollectionItemType = targetType.GenericTypeArguments[0];
 0161                var desiredCollectionType = typeof(ICollection<>).MakeGenericType(desiredCollectionItemType);
 162
 0163                if (underlyingTargetType.IsAssignableFrom(desiredCollectionType) || desiredCollectionType.IsAssignableFr
 164                {
 0165                    var collectionType = typeof(List<>).MakeGenericType(desiredCollectionItemType);
 0166                    var collection = (IList)Activator.CreateInstance(collectionType)!;
 167
 0168                    foreach (var item in enumerable)
 169                    {
 0170                        var convertedItem = ConvertTo(item, desiredCollectionItemType);
 0171                        collection.Add(convertedItem);
 172                    }
 173
 0174                    return collection;
 175                }
 176            }
 177        }
 178
 179        try
 180        {
 0181            return Convert.ChangeType(value, underlyingTargetType, CultureInfo.InvariantCulture);
 182        }
 0183        catch (InvalidCastException)
 184        {
 0185            throw new Exception($"Failed to convert an object of type {sourceType} to {underlyingTargetType}");
 186        }
 0187    }
 188
 189    /// <summary>
 190    /// Returns true if the specified type is date-like type, false otherwise.
 191    /// </summary>
 192    private static bool IsDateType(Type type)
 193    {
 0194        var dateTypes = new[]
 0195        {
 0196            typeof(DateTime),
 0197            typeof(DateTimeOffset)
 0198        };
 199
 0200        return dateTypes.Contains(type);
 201    }
 202
 203    /// <summary>
 204    /// Converts any date type to the specified target type.
 205    /// </summary>
 206    /// <param name="value">Any of <see cref="DateTime"/> or <see cref="DateTimeOffset"/>.</param>
 207    /// <param name="targetType">Any of <see cref="DateTime"/> or <see cref="DateTimeOffset"/>.</param>
 208    /// <returns>The converted value.</returns>
 209    /// <exception cref="ArgumentException">Thrown if <paramref name="value"/> is not of type <see cref="DateTime"/> or 
 210    private static object ConvertAnyDateType(object value, Type targetType)
 211    {
 0212        return targetType switch
 0213        {
 0214            { } t when t == typeof(DateTime) => value switch
 0215            {
 0216                DateTime dateTime => dateTime,
 0217                DateTimeOffset dateTimeOffset => dateTimeOffset.DateTime,
 0218                _ => throw new ArgumentException("Invalid value type.")
 0219            },
 0220            { } t when t == typeof(DateTimeOffset) => value switch
 0221            {
 0222                DateTime dateTime => new DateTimeOffset(dateTime),
 0223                DateTimeOffset dateTimeOffset => dateTimeOffset,
 0224                _ => throw new ArgumentException("Invalid value type.")
 0225            },
 0226            _ => throw new ArgumentException("Invalid target type.")
 0227        };
 228    }
 229}