Convert.ChangeType doesn't handle nullables

I had an assembly, originally written against .NET 1.1 and now running on .NET 2.0, that tried to convert a boxed DateTime object to a Nullable<DateTime> (DateTime?) object, using the Convert.ChangeType method. When this code runs, I get this exception:


[InvalidCastException: Invalid cast from 'System.DateTime' to ' System.Nullable`1[[System.DateTime, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]'.]
  System.Convert.DefaultToType(IConvertible value, Type targetType, IFormatProvider provider) +864
  System.DateTime.System.IConvertible.ToType(Type type, IFormatProvider provider) +36
  System.Convert.ChangeType(Object value, Type conversionType, IFormatProvider provider) +433
  System.Convert.ChangeType(Object value, Type conversionType) +38



Since a DateTime can be assigned to a nullable DateTime, it didn't make sense to me why it couldn't be converted to one, by calling Convert.ChangeType(Object, Type) with a Nullable as the second parameter. A Google search showed that Paul Wilson, Cameron Beccario, and others ran into this too during the beta timeframe.

The CLR team at Microsoft confirmed that this is a known problem:

Yes, Convert is not very extensible and was designed around a fixed set of types to address a specific functionality requirement for VB. System.ComponentModel has a more comprehensive data type conversion model, and I believe does deal with nullable.

The docs showed a lot of System.ComponentModel classes for conversion, e.g. DecimalConverter, but unfortunately I couldn't use those in this particular case, since it doesn't support the late-bound casting that code was doing. It was disappointing that this particular case wasn't handled, but it's probably a rare case, and it's understandable given some of the changes to nullables that came so late in the game.

My workaround in the short term was to not use the Nullable type for that particular method, until I could dig into System.ComponentModel more to find an alternative, or a patch is released for Convert.ChangeType to handle nullables.

Edit: I've now implemented a wrapper that handles nullables correctly.

10 Comments

  • Here's my ugly generic ChangeType method (from my ORMapper):



    static public object ChangeType(object value, Type type) {

    if (value == null &amp;&amp; type.IsGenericType) return Activator.CreateInstance(type);

    if (value == null) return null;

    if (type == value.GetType()) return value;

    if (type.IsEnum) {

    if (value is string)

    return Enum.Parse (type, value as string);

    else

    return Enum.ToObject(type, value);

    }

    if (!type.IsInterface &amp;&amp; type.IsGenericType) {

    Type innerType = type.GetGenericArguments()[0];

    object innerValue = QueryHelper.ChangeType(value, innerType);

    return Activator.CreateInstance(type, new object[] { innerValue });

    }

    if (value is string &amp;&amp; type == typeof(Guid)) return new Guid(value as string);

    if (value is string &amp;&amp; type == typeof(Version)) return new Version(value as string);

    if (!(value is IConvertible)) return value;

    return Convert.ChangeType(value, type);

    }



    It may have more than you need, and there are bound to be even more special cases like Guid and Version, but it works so far.

  • By the way, the call to QueryHelper.ChangeType is just a recursive call on the exact same method, so change the QueryHelper class name to your own class name.

  • Thanks was looking into this issue..

    How about this as an option?

    public static class Converter {
    public static T ChangeType(object value) {
    TypeConverter tc = TypeDescriptor.GetConverter(typeof(T));
    return (T)tc.ConvertFrom(value);
    }

    public static void RegisterTypeConverter() where TC : TypeConverter {
    TypeDescriptor.AddAttributes(typeof(T), new TypeConverterAttribute(typeof(TC)));
    }
    }

    This allows you to create new TypeDescriptors and add them at runtime, for any missing converters e.g. Version

    public class VersionConverter : TypeConverter
    {
    public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value) {
    string strvalue = value as string;
    if (strvalue != null) {
    return new Version(strvalue);
    }
    else {
    return new Version();
    }
    }
    }

    Test code...

    static void Main(string[] args) {

    Converter.RegisterTypeConverter();

    int? i = Converter.ChangeType("123");
    DateTime dt = Converter.ChangeType("20:33");
    char c = Converter.ChangeType("x");
    Guid g = Converter.ChangeType("{32F92EEB-A703-4eb7-A9F8-62E09F87D03F}");
    Version v = Converter.ChangeType("1.2.3.4");
    DateTime? k = Converter.ChangeType(null);
    }



  • Excellent solution Rich. Thanks.

  • Thanks Rich, that rocks!

  • Very elegant solution Rich, love it. Thanks.

  • I slightly modified your http://aspalliance.com/852 code. I canno tput code there due to server error.

    I modified your code slightly, to make it generic. Thanks!

    public static T ChangeType(object value) {
    Type conversionType = typeof(T);
    if (conversionType.IsGenericType &&
    conversionType.GetGenericTypeDefinition().Equals(typeof(Nullable))) {
    if (value == null) { return default(T); }

    conversionType = Nullable.GetUnderlyingType(conversionType); ;
    }

    return (T)Convert.ChangeType(value, conversionType);
    }

  • Great work. especially the generic method.

    Thankx.

  • public static T ChangeType(object value)
    {
    Type conversionType = typeof(T);
    if (conversionType.IsGenericType && conversionType.GetGenericTypeDefinition().Equals(typeof(Nullable)))
    {
    if (value == null)
    {
    return default(T);
    }
    else
    { NullableConverter nullableConverter = new NullableConverter(conversionType);

    conversionType = nullableConverter.UnderlyingType;
    }
    }

    return (T)Convert.ChangeType(value, conversionType);
    }

  • Does this work if the input value is not null but instead an empty string("")? I don't think conversion works from empty string to an underlying type like integer or double, maybe have to do string.isnullorempty(cstr(value)) and return null for that. Just a thought when dealing with user inputs...

    Really though, Microsoft needs to just fix this.

Comments have been disabled for this content.