Convert.ChangeType () non riesce sui tipi nullable


301

Voglio convertire una stringa in un valore di proprietà dell'oggetto, il cui nome ho come stringa. Sto cercando di farlo in questo modo:

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);
}

Il problema è che questo non riesce e genera un'eccezione Cast non valida quando il tipo di proprietà è un tipo nullable. Questo non è il caso in cui i valori non possono essere convertiti - funzioneranno se lo faccio manualmente (ad es. DateTime? d = Convert.ToDateTime(value);) Ho visto alcune domande simili ma ancora non riesco a farlo funzionare.


1
Sto usando ExecuteScalar <int?> Con PetaPoco 4.0.3 e non riesce per lo stesso motivo: return (T) Convert.ChangeType (val, typeof (T)) alla riga 554
Larry

Risposte:


409

Non testato, ma forse qualcosa del genere funzionerà:

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);
}

12
Avevo solo bisogno di quel pezzo di codice da solo. Grazie per Nullable.GetUnderlyingType! Mi ha aiutato molto quando ho costruito ModelBinder di un povero per un progetto che ne aveva bisogno. Ti devo una birra!
Maxime Rouiller,

3
Forse invece di (value == null) ? nullusare (value == null) ? default(t)?
Threadster

Non sembra funzionare per uniqueidentifier su stringa.
Anders Lindén,

C'è qualche motivo particolare per creare la safeValuevariabile anziché semplicemente riassegnarla value?
coloradocolby,

@threadster Non è possibile utilizzare l'operatore predefinito su una variabile di tipo 'Tipo'. Vedere stackoverflow.com/questions/325426/...
andy250

75

Devi ottenere il tipo sottostante per farlo ...

Prova questo, l'ho usato con successo con generici:

//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 in diversi punti del mio codice, un esempio è un metodo di supporto che utilizzo per convertire i valori del database in modo semplice:

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);
}

Chiamato usando:

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

Ho scritto una serie di post sul blog tra cui questo all'indirizzo http://www.endswithsaurus.com/2010_07_01_archive.html (Scorri verso il basso fino all'addendum , @JohnMacintyre ha effettivamente individuato il bug nel mio codice originale che mi ha portato lungo lo stesso percorso che sei adesso). Ho un paio di piccole modifiche da quel post che include anche la conversione dei tipi di enum, quindi se la tua proprietà è un Enum puoi comunque usare lo stesso metodo di chiamata. Basta aggiungere una linea per verificare i tipi di enum e sei fuori per le gare usando qualcosa come:

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

Normalmente avresti qualche errore nel controllare o usare TryParse invece di Parse, ma ottieni l'immagine.


Grazie - Mi manca ancora un passo o non capisco qualcosa. Sto cercando di impostare un valore di proprietà, perché sto ottenendo l'oggetto che si trova nel tipo Sottostante? Inoltre non sono sicuro di come passare dal mio codice a un metodo di estensione come il tuo. Non saprò quale sarà il tipo per fare qualcosa come value.Helper <Int32?> ().
iboeno,

@iboeno - Siamo spiacenti, ero in riunione, quindi non potevo aiutarti a collegare i punti. Sono contento che tu abbia una soluzione però.
BenAlabaster,

9

Questo è un po 'lungo per un esempio, ma questo è un approccio relativamente robusto e separa il compito di trasmettere da un valore sconosciuto a un tipo sconosciuto

Ho un metodo TryCast che fa qualcosa di simile e tiene conto dei tipi nullable.

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;
    }
}

Naturalmente TryCast è un metodo con un parametro di tipo, quindi per chiamarlo dinamicamente devi costruire il MethodInfo da solo:

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

Quindi per impostare il valore effettivo della proprietà:

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);
}

E i metodi di estensione per gestire 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;
}

6

Avevo un bisogno simile e la risposta di LukeH mi indicò la direzione. Ho ideato questa funzione generica per semplificare.

    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); }
    }

L'utilizzo è così:

        NotNullableDateProperty = CopyValue(NullableDateProperty, NotNullableDateProperty);

Si noti che il secondo parametro viene utilizzato solo come prototipo per mostrare alla funzione come eseguire il cast del valore restituito, quindi non deve essere effettivamente la proprietà di destinazione. Significa che puoi fare anche qualcosa del genere:

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

L'ho fatto in questo modo invece di usare un out perché non puoi usarlo con le proprietà. Così com'è, può funzionare con proprietà e variabili. Se lo desideri, puoi anche creare un sovraccarico per passare il tipo.


0

Grazie @LukeH
ho cambiato un po ':

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;
}

0

L'ho fatto in questo modo

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;
    }
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.