Convert.ChangeType () échoue sur les types Nullable

301

Je veux convertir une chaîne en une valeur de propriété d'objet, dont j'ai le nom en tant que chaîne. J'essaie de faire ça comme ça:

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

Le problème est qu'il échoue et lève une exception de conversion non valide lorsque le type de propriété est un type nullable. Ce n'est pas le cas des valeurs ne pouvant pas être converties - elles fonctionneront si je le fais manuellement (par exemple DateTime? d = Convert.ToDateTime(value);), j'ai vu des questions similaires mais je n'arrive toujours pas à le faire fonctionner.

iboeno
la source
1
J'utilise ExecuteScalar <int?> Avec PetaPoco 4.0.3 et il échoue pour la même raison: return (T) Convert.ChangeType (val, typeof (T)) à la ligne 554
Larry

Réponses:

409

Non testé, mais peut-être que quelque chose comme ça fonctionnera:

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
la source
12
J'avais juste besoin de ce morceau de code moi-même. Merci pour Nullable.GetUnderlyingType! J'ai beaucoup aidé lorsque j'ai construit ModelBinder d'un pauvre homme pour un projet qui en avait besoin. Je te dois une bière!
Maxime Rouiller
3
Peut-être au lieu d' (value == null) ? nullutiliser (value == null) ? default(t)?
threadster le
Ne semble pas fonctionner pour uniqueidentifier à la chaîne.
Anders Lindén
Y a-t-il une raison particulière de créer la safeValuevariable au lieu de simplement la réaffecter à value?
coloradocolby
@threadster Vous ne pouvez pas utiliser d'opérateur par défaut sur une variable de type 'Type'. Voir stackoverflow.com/questions/325426/…
andy250
75

Vous devez obtenir le type sous-jacent pour ce faire ...

Essayez ceci, je l'ai utilisé avec succès avec des génériques:

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

Je l'utilise à plusieurs endroits dans mon code, un exemple est une méthode d'aide que j'utilise pour convertir les valeurs de base de données de manière sécurisée:

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

Appelé en utilisant:

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

J'ai écrit une série de billets de blog, notamment à http://www.endswithsaurus.com/2010_07_01_archive.html (faites défiler vers le bas jusqu'à l'addendum, @JohnMacintyre a en fait repéré le bogue dans mon code d'origine, ce qui m'a conduit sur le même chemin que vous. maintenant). J'ai quelques petites modifications depuis ce post qui inclut également la conversion des types d'énumération, donc si votre propriété est une énumération, vous pouvez toujours utiliser le même appel de méthode. Ajoutez simplement une ligne pour vérifier les types d'énumération et vous partez pour les courses en utilisant quelque chose comme:

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

Normalement, vous auriez une erreur de vérification ou utiliser TryParse au lieu de Parse, mais vous obtenez l'image.

BenAlabaster
la source
Merci - il me manque encore une étape ou je ne comprends pas quelque chose. J'essaie de définir une valeur de propriété, pourquoi est-ce que j'obtiens l'objet dans lequel il se trouve dans le type sous-jacent? Je ne sais pas non plus comment passer de mon code à une méthode d'extension comme la vôtre. Je ne sais pas quel sera le type pour faire quelque chose comme value.Helper <Int32?> ().
iboeno
@iboeno - Désolé, j'étais en réunion alors je n'ai pas pu vous aider à relier les points. Heureux que vous ayez trouvé une solution.
BenAlabaster
9

C'est un peu long pour un exemple, mais c'est une approche relativement robuste, et sépare la tâche de transtypage d'une valeur inconnue en un type inconnu

J'ai une méthode TryCast qui fait quelque chose de similaire et prend en compte les types nullables.

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

Bien sûr TryCast est une méthode avec un paramètre de type, donc pour l'appeler dynamiquement, vous devez construire vous-même le MethodInfo:

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

Ensuite, pour définir la valeur réelle de la propriété:

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

Et les méthodes d'extension pour gérer les propriétés.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
la source
6

J'avais un besoin similaire, et la réponse de LukeH m'a orienté dans la direction. J'ai créé cette fonction générique pour le rendre facile.

    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'utilisation est comme ceci:

        NotNullableDateProperty = CopyValue(NullableDateProperty, NotNullableDateProperty);

Notez que le deuxième paramètre est juste utilisé comme prototype pour montrer à la fonction comment convertir la valeur de retour, il ne doit donc pas réellement être la propriété de destination. Cela signifie que vous pouvez également faire quelque chose comme ceci:

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

Je l'ai fait de cette façon au lieu d'utiliser un out parce que vous ne pouvez pas l'utiliser avec des propriétés. En l'état, il peut fonctionner avec des propriétés et des variables. Vous pouvez également créer une surcharge pour transmettre le type à la place si vous le souhaitez.

Steve In CO
la source
0

Merci @LukeH
j'ai un peu changé:

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
la source
0

Je l'ai fait de cette façon

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
la source