通用对象更新程序 - 将数组转换为列表

时间:2017-03-16 21:15:40

标签: c# generics

我正在尝试创建一个泛型类,它能够更新对象的任何属性。

我让它适用于几种情况,例如:单值属性(intstringbool等)。如果属性是IEnumerable<T>,它也可以很好地工作,在这种情况下,列表或数组工作正常。

但是如果属性为List<T>,我会遇到一个场景,但是要分配的值是Array(或者相反)。在“标准”情况下,我可以简单地使用ToList()ToArray,但在一般情况下,我很困惑如何。

这是代码

public static class ObjectUpdater
{
    public static T Patch<T>(T obj, IEnumerable<KeyValuePair<string, object>> delta)
    {
        if (obj == null || delta == null)
        {
            return obj;
        }
        foreach (var deltaItem in delta)
        {
            Patch(obj, deltaItem.Key, deltaItem.Value);
        }
        return obj;
    }

    private static void Patch<T>(T obj, string propertyName, object value)
    {
        var propertyInfo = obj.GetType().GetProperty(propertyName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);
        if (propertyInfo == null || !propertyInfo.CanRead || !propertyInfo.CanWrite)
        {
            throw new ArgumentException($"Property '{propertyName}' doesn't exist or cannot be updated");
        }
        SetPropertyValue(obj, value, propertyInfo);
    }

    private static void SetPropertyValue(object obj, object value, PropertyInfo propertyInfo)
    {
        if (propertyInfo.PropertyType.IsEnum)
        {
            propertyInfo.SetValue(obj, Convert.ToInt32(value));
            return;
        }

        // property parsing is based on the target property's TryParse() method
        // big / small enough float / DateTime values had issues, as the ToString() might lose precision >> they're handled separately
        if (value is float)
        {
            SetPropertyValueAsFloat(obj, (float)value, propertyInfo);
            return;
        }

        if (value is DateTime)
        {
            SetPropertyValueAsDateTime(obj, (DateTime)value, propertyInfo);
            return;
        }

        var systemType = propertyInfo.PropertyType.UnderlyingSystemType;
        var tryParse = systemType.GetMethod("TryParse", new[] {typeof(string), systemType.MakeByRefType()});
        if (tryParse == null)
        {
            propertyInfo.SetValue(obj, value);
            return;
        }
        var parameters = new object[]
                         {
                             value.ToString(),
                             null
                         };
        var canParse = (bool) tryParse.Invoke(null, parameters);
        propertyInfo.SetValue(obj, canParse ? parameters[1] : value);
    }

    private static void SetPropertyValueAsDateTime(object obj, DateTime value, PropertyInfo propertyInfo)
    {
        // code to handle DateTime value
    }
}

// and two test methods
[Fact]
private void Patch_StringListPropertyFromArrayValue()
{
    var sourceObject = new TestClassWithCollectionProperties
                       {
                           StringListProperty = null
                       };
    var expectedResult = new TestClassWithCollectionProperties
                         {
                             StringListProperty = new List<string>
                                                  {
                                                      "abc",
                                                      "def"
                                                  }
                         };
    var delta = new List<KeyValuePair<string, object>>
                {
                    new KeyValuePair<string, object>("StringListProperty",
                        new []
                        {
                            "abc",
                            "def"
                        })
                };

    var result = ObjectUpdater.Patch(sourceObject, delta);

    result.ShouldBeEquivalentTo(expectedResult);
}

[Fact]
private void Patch_StringArrayPropertyFromListValue()
{
    var sourceObject = new TestClassWithCollectionProperties
                       {
                           StringArrayProperty = null
                       };
    var expectedResult = new TestClassWithCollectionProperties
                         {
                             StringArrayProperty = new[]
                                                   {
                                                       "abc",
                                                       "def"
                                                   }
                         };
    var delta = new List<KeyValuePair<string, object>>
                {
                    new KeyValuePair<string, object>("StringArrayProperty",
                        new List<string>
                        {
                            "abc",
                            "def"
                        })
                };

    var result = ObjectUpdater.Patch(sourceObject, delta);

    result.ShouldBeEquivalentTo(expectedResult);
}

但是此测试失败,因为ObjectUpdater.Patch使用以下消息抛出System.ArgumentException

  

'System.String []'类型的对象无法转换为'System.Collections.Generic.List`1 [System.String]'类型。

有什么建议吗?

1 个答案:

答案 0 :(得分:1)

嗯,您可以在tryParse尝试之前在代码中的某处处理具体案例:

if (value != null && value.GetType() != propertyInfo.PropertyType
    // from T[]
    && value.GetType().IsArray && value.GetType().GetArrayRank() == 1
    // to List<T>
    && propertyInfo.PropertyType.IsGenericType
    && propertyInfo.PropertyType.GetGenericTypeDefinition() == typeof(List<>)
    && propertyInfo.PropertyType.GetGenericArguments()[0] == value.GetType().GetElementType())
{
    var T = value.GetType().GetElementType();
    var listT = typeof(List<>).MakeGenericType(T);
    // new List<T>(IEnumerable<T> items)
    var enumerableT = typeof(IEnumerable<>).MakeGenericType(T);
    var newListT = listT.GetConstructor(new Type [] { enumerableT });
    var list = newListT.Invoke(new[] { value });
    propertyInfo.SetValue(obj, list);
    return;
}

以上处理分配表单T[]List<T>。要处理相反的分配,请通过反射添加类似if条件并交换类型并简单地调用List<T>.ToArray)方法:

if (value != null && value.GetType() != propertyInfo.PropertyType
    // to T[]
    && propertyInfo.PropertyType.IsArray && propertyInfo.PropertyType.GetArrayRank() == 1
    // from List<T>
    && value.GetType().IsGenericType
    && value.GetType().GetGenericTypeDefinition() == typeof(List<>)
    && value.GetType().GetGenericArguments()[0] == propertyInfo.PropertyType.GetElementType())
{
    // List<T>.ToArray()
    var toArray = value.GetType().GetMethod("ToArray", Type.EmptyTypes);
    var array = toArray.Invoke(value, null);
    propertyInfo.SetValue(obj, array);
    return;
}