Convert.ChangeType () falla en tipos anulables

301

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 genera una excepción de conversión no válida cuando el tipo de propiedad es un tipo anulable. Este no es el caso de los valores que no se pueden convertir: funcionarán si lo hago manualmente (por ejemplo DateTime? d = Convert.ToDateTime(value);), he visto algunas preguntas similares pero todavía no puedo hacer que funcione.

iboeno
fuente
1
Estoy usando ExecuteScalar <int?> Con PetaPoco 4.0.3 y falla por la misma razón: return (T) Convert.ChangeType (val, typeof (T)) en la línea 554
Larry

Respuestas:

409

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
fuente
12
Solo necesitaba esa pieza de código yo mismo. Gracias por Nullable.GetUnderlyingType! Me ayudó mucho cuando construí ModelBinder de un hombre pobre para un proyecto que lo necesitaba. ¡Te debo una cerveza!
Maxime Rouiller
3
Tal vez en lugar de (value == null) ? nullusar (value == null) ? default(t)?
threadster
No parece funcionar para un identificador único para la cadena.
Anders Lindén
¿Hay alguna razón particular para crear la safeValuevariable en lugar de simplemente reasignarla value?
coloradocolby
@threadster No puede usar el operador predeterminado en una variable de tipo 'Tipo'. Ver stackoverflow.com/questions/325426/…
andy250
75

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

Prueba esto, lo he usado con éxito 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 los valores de la base de datos de una manera 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 esto en http://www.endswithsaurus.com/2010_07_01_archive.html (Desplácese hasta el Anexo, @JohnMacintyre realmente 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 incluye la conversión de tipos de enumeración tambié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 saldrá a las carreras usando algo como:

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

Normalmente tendrías algún error al comprobar o usar TryParse en lugar de Parse, pero obtienes la imagen.

BenAlabastro
fuente
Gracias. Todavía me falta un paso o no entiendo algo. Estoy tratando de establecer un valor de propiedad, ¿por qué obtengo el objeto que está en el Tipo subyacente? Tampoco estoy seguro de cómo pasar de mi código a un método de extensión como el suyo. No sabré cuál será el tipo para hacer algo como value.Helper <Int32?> ().
iboeno
@iboeno - Lo siento, estaba en una reunión, así que no pude ayudarte a conectar los puntos. Aunque me alegra que hayas encontrado una solución.
BenAlabaster
9

Esto es un poco largo para un ejemplo, pero este es un enfoque relativamente robusto, y separa la tarea de convertir de valor desconocido a tipo desconocido.

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

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 debe construir la información del método usted 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
fuente
6

Tenía una necesidad similar, y la respuesta de LukeH me señaló en la dirección. Se me ocurrió esta función genérica para que sea 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 mostrar a la función cómo emitir 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 una salida porque no se puede usar con propiedades. Como es, puede trabajar con propiedades y variables. También podría crear una sobrecarga para pasar el tipo si lo desea.

Steve en CO
fuente
0

Gracias @LukeH
cambié un poco:

public static object convertToPropType(PropertyInfo property, object value)
{
    object cstVal = null;
    if (property != null)
    {
        Type propType = Nullable.GetUnderlyingType(property.PropertyType);
        bool isNullable = (propType != null);
        if (!isNullable) { propType = property.PropertyType; }
        bool canAttrib = (value != null || isNullable);
        if (!canAttrib) { throw new Exception("Cant attrib null on non nullable. "); }
        cstVal = (value == null || Convert.IsDBNull(value)) ? null : Convert.ChangeType(value, propType);
    }
    return cstVal;
}
hs586sd46s
fuente
0

Lo hice de esta manera

public static List<T> Convert<T>(this ExcelWorksheet worksheet) where T : new()
    {
        var result = new List<T>();
        int colCount = worksheet.Dimension.End.Column;  //get Column Count
        int rowCount = worksheet.Dimension.End.Row;

        for (int row = 2; row <= rowCount; row++)
        {
            var obj = new T();
            for (int col = 1; col <= colCount; col++)
            {

                var value = worksheet.Cells[row, col].Value?.ToString();
                PropertyInfo propertyInfo = obj.GetType().GetProperty(worksheet.Cells[1, col].Text);
                propertyInfo.SetValue(obj, Convert.ChangeType(value, Nullable.GetUnderlyingType(propertyInfo.PropertyType) ?? propertyInfo.PropertyType), null);

            }
            result.Add(obj);
        }

        return result;
    }
AnishJain87
fuente