Convert.ChangeType() не работает в Nullable Types

Я хочу преобразовать строку в значение свойства объекта, имя которой у меня есть как строка. Я пытаюсь сделать это так:

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

Проблема заключается в том, что это ошибка и бросание Invalid Cast Exception, когда тип свойства является типом с нулевым значением. Это не тот случай, когда значения не могут быть преобразованы - они будут работать, если я сделаю это вручную (например, DateTime? d = Convert.ToDateTime(value);). Я видел некоторые похожие вопросы, но до сих пор не могу заставить его работать.

Ответ 1

Неподтвержденный, но может быть что-то вроде этого будет работать:

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

Ответ 2

Чтобы сделать это, вы должны получить базовый тип...

Попробуйте это, я успешно использовал его с generics:

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

Я использую его в нескольких местах в моем коде, один пример - вспомогательный метод, который я использую для преобразования значений базы данных в виде:

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

Вызывается с помощью:

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

Я написал серию сообщений в блогах, в том числе на http://www.endswithsaurus.com/2010_07_01_archive.html (прокрутите вниз до добавления, @JohnMacintyre действительно обнаружил ошибку в моем исходном коде, который привел меня к тому же пути, на котором вы сейчас находитесь). У меня есть несколько небольших модификаций с этой должности, которая включает в себя преобразование типов перечислений, поэтому, если ваше свойство Enum, вы все равно можете использовать один и тот же вызов метода. Просто добавьте строку для проверки типов перечислений, и вы отправитесь на гонки, используя что-то вроде:

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

Обычно у вас есть проверка ошибок или использование TryParse вместо Parse, но вы получаете изображение.

Ответ 3

Это немного длинный пример для примера, но это относительно надежный подход и отделяет задачу каста от неизвестного значения до неизвестного типа

У меня есть метод TryCast, который делает что-то подобное, и учитывает типы 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;
    }
}

Конечно, TryCast - это метод с параметром типа, поэтому для динамического вызова вам необходимо самостоятельно построить MethodInfo:

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

Затем установите фактическое значение свойства:

public static void SetCastedValue<T>(this PropertyInfo property, T instance, object value)
{
    if (property.DeclaringType != typeof(T))
    {
        throw new ArgumentException("property 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);
}

И методы расширения для работы с 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;
}

Ответ 4

У меня была аналогичная потребность, и ответ от Люка Н указал мне в направлении. Я придумал эту общую функцию, чтобы сделать ее легкой.

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

Использование выглядит так:

        NotNullableDateProperty = CopyValue(NullableDateProperty, NotNullableDateProperty);

Обратите внимание, что второй параметр используется только как прототип, чтобы показать функцию, как отличать возвращаемое значение, поэтому на самом деле это не должно быть свойство назначения. Это означает, что вы можете сделать что-то вроде этого:

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

Я сделал это так, вместо того, чтобы использовать out, потому что вы не можете использовать его со свойствами. Как есть, он может работать со свойствами и переменными. Вы также можете создать перегрузку, чтобы передать тип, если хотите.

Ответ 5

Спасибо @LukeH
Я немного изменился:

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