Convert to unknown generic type: ChangeType<T>

Problem

When you create a generic method (Foo) or generic class (Bar), it often happens that you need to change (or convert) a type to T. In these situations you need a ChangeType function that changes "any" type to T. Unfortunately this is not a standard method. The Cast() method of Linq looks like a solution but it is very limited. This wil not work for a lot of types:


public static T ChangeTypeLinqVariant<T>(this object value)
{
  return (new [] {value}).Cast<T>().Single();
}

Examples

Obvious examples where you need a ChangeType method are:

  • Returns T but the parameter of the method is an object, string, XML or data from a DataReader.
  • Method returns T1 but the parameters of the method is T2: T1 Foo(T2 value))

An obvious example is:


private Car GetCar()
{
    return new Car
    {
        Id = GetItemFromXml<Guid>(@"Indentification/id"),
        Brand = GetItemFromXml<string>(@"BrandInfo/name"), 
        ConstructionYear = GetItemFromXml<int>(@"BuilDetails/year"), 
    };
}

private T GetItemFromXml<T>(string xmlPath)
{
    var itemValueAsString = GetAValueFromXml(xmlPath);
    return itemValueAsString.ChangeType<T>();
}

private string GetAValueFromXml(string xmlPath)
{
    // Open a file and get a string value based on the xmlPath
}

A less obvious example is:


private void AddItem<TK, TV>(Dictionary<TK, TV>dictionary, TK key, TV value)
{
  if (key is string) // Then we want the key to be lower case
  {
    // This wil not compile because key is a TK, not a string
    key = key.ToString().ToLower();
    // This will do the job
    key = key.ToString().ToLower().ChangeType<TK>();
  }
  if (dictionary.ContainsKey(key)) return;
  dictionary.Add(key, value);
}

Solution

A start it the good direction was a post on StackOverflow. But that solution didn't work for types like enum's, strings, Guids, sientific notation numbers etc. So I added soms new code to it and now it works for most of the common scenarios.

Usings:


using System;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;

The code that does the job:


public static class ChangeTypeExtensions
{
    private static readonly CultureInfo DefaultCultureInfo = CultureInfo.InvariantCulture;
    private const bool DefaultReturnDefaultValueWhenPossible = true;

    /// <summary>
    /// Convert anything to T, possibly a null
    /// </summary>
    [return: MaybeNull]
    public static T ChangeType<T>(this object? value, bool returnDefaultValueWhenPossible = DefaultReturnDefaultValueWhenPossible)
    {
        return ChangeType<T>(value, DefaultCultureInfo, returnDefaultValueWhenPossible);
    }

    /// <summary>
    /// Convert anything to T, possibly a null
    /// </summary>
    [return: MaybeNull]
    public static T ChangeType<T>(this object? value, CultureInfo cultureInfo, bool returnDefaultValueWhenPossible = DefaultReturnDefaultValueWhenPossible)
    {
        var toType = typeof(T);

        if (value == null && toType.IsClass) return default;

        var stringValue = value as string;

        if (stringValue == null)
        {
            if (toType == typeof(string)) // It is not a string but it will be converted to a string
            {
                return ChangeType<T>(Convert.ToString(value, cultureInfo), cultureInfo, returnDefaultValueWhenPossible);
            }
        }
        else // It is a string
        {
            if (toType == typeof(string)) return (T)value; // It is a string and it will stay a string

            stringValue = stringValue.Trim();

            if (toType.IsEnum)
            {
                return (T)Enum.Parse(typeof(T), stringValue);
            }

            if (stringValue.Length == 0 && toType != typeof(string)) // Empty string is a weird case, will convert to a default type
            {
                if (returnDefaultValueWhenPossible && (toType.IsClass || IsNullableStruct(toType)))
                {
                    return default;
                }
                throw new FormatException("Empty string is not in a correct format for a struct.");
            }

            if (toType == typeof(Guid)) // It is a string and it will be converted to a guid
            {
                return ChangeType<T>(new Guid(stringValue), cultureInfo, returnDefaultValueWhenPossible); // Convert to guid can not be done with a default converter, constructor met with a parameter and default value are special cases
            }

            if (IsTypeOfFloatingPoint<T>() && IsValueInScientificNotation(stringValue))
            {
                var convertedFloatingPointValue = ConvertScientificNotationToType<T>(stringValue);
                return ChangeType<T>(convertedFloatingPointValue, cultureInfo, returnDefaultValueWhenPossible);
            }
        }

        if (IsNullableStruct(toType))
        {
            toType = Nullable.GetUnderlyingType(toType)!;

            if (value == null)
            {
                if (toType.IsEnum)
                {
                    return default;
                }
                if (returnDefaultValueWhenPossible)
                {
                    return default;
                }
                throw new InvalidCastException("Nullable struct type with null value can not be converted when value is null.");
            }

            if (toType.IsEnum)
            {
                if (stringValue != null)
                {
                    return (T)Enum.Parse(toType, stringValue);
                }
            }
        }

        if (value == null)
        {
            throw new ArgumentException("Null can not be converted to non nullable struct.", nameof(value));
        }

        var fromType = value.GetType();

        // ReSharper disable once SuspiciousTypeConversion.Global, Generic Type T can be IConvertible
        var canConvert = fromType is IConvertible || ConvertsWithIConvertible(toType) && !toType.IsEnum;

        if (canConvert)
        {
            return (T)Convert.ChangeType(value, toType, cultureInfo);
        }

        if (toType.IsEnum)
        {
            // cast from nullable enum
            return (T)Enum.ToObject(toType, value);
        }

        return (T)value;
    }

    private static bool IsNullableStruct(Type type)
    {
        return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>);
    }

    private static bool IsValueInScientificNotation(object value)
    {
        if (value == null) return false;

        string eNotationString = (value.ToString() ?? string.Empty).Trim();

        return Regex.IsMatch(eNotationString, "^[-+]?[0-9,.]+[E]{1}[-+]?[0-9]+$", RegexOptions.IgnoreCase);
    }

    private static bool IsTypeOfFloatingPoint<T>()
    {
        return typeof(T).IsIn(typeof(float), typeof(float?), typeof(double), typeof(double?), typeof(decimal), typeof(decimal?));
    }

    private static object ConvertScientificNotationToType<T>(object value)
    {
        string eNotation = (value?.ToString() ?? string.Empty).Trim();

        var numberStyle = NumberStyles.AllowDecimalPoint | NumberStyles.AllowExponent | NumberStyles.AllowLeadingSign;

        if (typeof(T).IsIn(typeof(double), typeof(double?)))
        {
            return double.Parse(eNotation, numberStyle, CultureInfo.InvariantCulture);
        }

        if (typeof(T).IsIn(typeof(float), typeof(float?)))
        {
            return float.Parse(eNotation, numberStyle, CultureInfo.InvariantCulture);
        }

        if (typeof(T).IsIn(typeof(decimal), typeof(decimal?)))
        {
            return decimal.Parse(eNotation, numberStyle, CultureInfo.InvariantCulture);
        }

        throw new InvalidOperationException($"{value} can not be converted to type {typeof(T).FullName}.");
    }

    private static bool ConvertsWithIConvertible(Type type)
    {
        return type.IsIn(typeof(bool), typeof(char), typeof(sbyte), typeof(byte), typeof(short), typeof(ushort),
                         typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(float), typeof(double),
                         typeof(decimal), typeof(DateTime), typeof(string));

    }

    public static object? ChangeType(this object value, Type theTypeToChangeTo, CultureInfo cultureInfo, bool returnDefaultValueWhenPossible)
    {
        MethodInfo? method = typeof(ChangeTypeExtensions).GetMethod(nameof(ChangeTypeWithUniqueName), BindingFlags.NonPublic | BindingFlags.Static);
        method = method!.MakeGenericMethod(theTypeToChangeTo);
        var result = method.Invoke(null, new[] { value, returnDefaultValueWhenPossible, cultureInfo });

        return result;
    }

    /// <summary>
    /// Convert anything to T, possibly a null
    /// </summary>
    private static object? ChangeTypeWithUniqueName<T>(this object value, CultureInfo cultureInfo, bool returnDefaultValueWhenPossible)
    {
        return value.ChangeType<T>(cultureInfo, returnDefaultValueWhenPossible);
    }
}

public static class OtherExtensions
{
    public static bool IsIn<T>(this T source, params T[] values)
    {
        if (source == null || values == null)
        {
            return false;
        }
        return values.Any(v => source.Equals(v));
    }
}

Leave a Comment

Comment

Comments