Convert.ChangeType() falla en tipos que aceptan valores NULL

Resuelto iboeno asked hace 14 años • 7 respuestas

Quiero convertir una cadena en un valor de propiedad de objeto, cuyo nombre tengo como cadena. Estoy tratando de hacer esto así:

string modelProperty = "Some Property Name";
string value = "SomeValue";
var property = entity.GetType().GetProperty(modelProperty);
if (property != null) {
    property.SetValue(entity, 
        Convert.ChangeType(value, property.PropertyType), null);
}

El problema es que esto falla y arroja una excepción de conversión no válida cuando el tipo de propiedad es un tipo que acepta valores NULL. Este no es el caso de que los valores no se puedan convertir; funcionarán si lo hago manualmente (p. ej. DateTime? d = Convert.ToDateTime(value);). He visto algunas preguntas similares pero todavía no puedo hacer que funcione.

iboeno avatar Aug 20 '10 20:08 iboeno
Aceptado

No probado, pero tal vez algo como esto funcione:

string modelProperty = "Some Property Name";
string value = "Some Value";

var property = entity.GetType().GetProperty(modelProperty);
if (property != null)
{
    Type t = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType;

    object safeValue = (value == null) ? null : Convert.ChangeType(value, t);

    property.SetValue(entity, safeValue, null);
}
LukeH avatar Aug 20 '2010 14:08 LukeH

Tienes que obtener el tipo subyacente para poder hacer eso...

Pruebe esto, lo he usado exitosamente con genéricos:

//Coalesce to get actual property type...
Type t = property.PropertyType();
t = Nullable.GetUnderlyingType(t) ?? t;

//Coalesce to set the safe value using default(t) or the safe type.
safeValue = value == null ? default(t) : Convert.ChangeType(value, t);

Lo uso en varios lugares de mi código, un ejemplo es un método auxiliar que uso para convertir valores de bases de datos de forma segura:

public static T GetValue<T>(this IDataReader dr, string fieldName)
{
    object value = dr[fieldName];

    Type t = typeof(T);
    t = Nullable.GetUnderlyingType(t) ?? t;

    return (value == null || DBNull.Value.Equals(value)) ? 
        default(T) : (T)Convert.ChangeType(value, t);
}

Llamado usando:

string field1 = dr.GetValue<string>("field1");
int? field2 = dr.GetValue<int?>("field2");
DateTime field3 = dr.GetValue<DateTime>("field3");

Escribí una serie de publicaciones de blog que incluyen esta en http://www.endswithsaurus.com/2010_07_01_archive.html (Desplácese hacia abajo hasta el Anexo, @JohnMacintyre detectó el error en mi código original que me llevó por el mismo camino que usted). ahora). Tengo un par de pequeñas modificaciones desde esa publicación que también incluye la conversión de tipos de enumeración, por lo que si su propiedad es una enumeración, aún puede usar la misma llamada al método. Simplemente agregue una línea para verificar los tipos de enumeración y estará listo para las carreras usando algo como:

if (t.IsEnum)
    return (T)Enum.Parse(t, value);

Normalmente tendría algún error al verificar o usaría TryParse en lugar de Parse, pero ya se hace una idea.

BenAlabaster avatar Aug 20 '2010 13:08 BenAlabaster

Esto funciona perfectamente incluso para tipos que aceptan valores NULL:

TypeConverter conv = TypeDescriptor.GetConverter(type);
return conv.ConvertFrom(value);

Para seguridad de tipos, también debe llamar conv.CanConvertFrom(type)al método antes de llamar ConvertFrom(). En caso de que devuelva falso, puede recurrir a ChangeTypeotra cosa.

Major avatar Jun 07 '2021 15:06 Major

Esto es un poco largo para poner un ejemplo, pero es un enfoque relativamente sólido y separa la tarea de convertir un valor desconocido a un tipo desconocido.

Tengo un método TryCast que hace algo similar y tiene en cuenta los tipos que aceptan valores NULL.

public static bool TryCast<T>(this object value, out T result)
{
    var type = typeof (T);

    // If the type is nullable and the result should be null, set a null value.
    if (type.IsNullable() && (value == null || value == DBNull.Value))
    {
        result = default(T);
        return true;
    }

    // Convert.ChangeType fails on Nullable<T> types.  We want to try to cast to the underlying type anyway.
    var underlyingType = Nullable.GetUnderlyingType(type) ?? type;

    try
    {
        // Just one edge case you might want to handle.
        if (underlyingType == typeof(Guid))
        {
            if (value is string)
            {
                value = new Guid(value as string);
            }
            if (value is byte[])
            {
                value = new Guid(value as byte[]);
            }

            result = (T)Convert.ChangeType(value, underlyingType);
            return true;
        }

        result = (T)Convert.ChangeType(value, underlyingType);
        return true;
    }
    catch (Exception ex)
    {
        result = default(T);
        return false;
    }
}

Por supuesto, TryCast es un método con un parámetro de tipo, por lo que para llamarlo dinámicamente debes construir MethodInfo tú mismo:

var constructedMethod = typeof (ObjectExtensions)
    .GetMethod("TryCast")
    .MakeGenericMethod(property.PropertyType);

Luego, para establecer el valor real de la propiedad:

public static void SetCastedValue<T>(this PropertyInfo property, T instance, object value)
{
    if (property.DeclaringType != typeof(T))
    {
        throw new ArgumentException("property's declaring type must be equal to typeof(T).");
    }

    var constructedMethod = typeof (ObjectExtensions)
        .GetMethod("TryCast")
        .MakeGenericMethod(property.PropertyType);

    object valueToSet = null;
    var parameters = new[] {value, null};
    var tryCastSucceeded = Convert.ToBoolean(constructedMethod.Invoke(null, parameters));
    if (tryCastSucceeded)
    {
        valueToSet = parameters[1];
    }

    if (!property.CanAssignValue(valueToSet))
    {
        return;
    }
    property.SetValue(instance, valueToSet, null);
}

Y los métodos de extensión para tratar con property.CanAssignValue...

public static bool CanAssignValue(this PropertyInfo p, object value)
{
    return value == null ? p.IsNullable() : p.PropertyType.IsInstanceOfType(value);
}

public static bool IsNullable(this PropertyInfo p)
{
    return p.PropertyType.IsNullable();
}

public static bool IsNullable(this Type t)
{
    return !t.IsValueType || Nullable.GetUnderlyingType(t) != null;
}
bopapa_1979 avatar May 31 '2012 18:05 bopapa_1979

Yo tenía una necesidad similar y la respuesta de LukeH me indicó la dirección. Se me ocurrió esta función genérica para hacerlo más fácil.

    public static Tout CopyValue<Tin, Tout>(Tin from, Tout toPrototype)
    {
        Type underlyingT = Nullable.GetUnderlyingType(typeof(Tout));
        if (underlyingT == null)
        { return (Tout)Convert.ChangeType(from, typeof(Tout)); }
        else
        { return (Tout)Convert.ChangeType(from, underlyingT); }
    }

El uso es así:

        NotNullableDateProperty = CopyValue(NullableDateProperty, NotNullableDateProperty);

Tenga en cuenta que el segundo parámetro solo se usa como prototipo para mostrarle a la función cómo convertir el valor de retorno, por lo que en realidad no tiene que ser la propiedad de destino. Lo que significa que también puedes hacer algo como esto:

        DateTime? source = new DateTime(2015, 1, 1);
        var dest = CopyValue(source, (string)null);

Lo hice de esta manera en lugar de usar out porque no puedes usarlo con propiedades. Tal como está, puede funcionar con propiedades y variables. También podría crear una sobrecarga para pasar el tipo si lo desea.

Steve In CO avatar Jan 06 '2015 20:01 Steve In CO