将嵌套对象集合展平到数据表中的通用方法?

Joe*_*oey 9 .net c# collections datatable

我有一个对象列表,其中又包含其他对象的嵌套列表。我想将对象图展平为DataTable.

我找到了接受对象集合并将它们映射到 a DataTable(下面引用)的代码,但它假设属性是可以可靠地转换为字符串值的简单类型。

我认为这只能通过递归来实现,但也许有更好的方法来做到这一点。

数据模型

想象一下我们有一个ListofCustomer对象:

public class Item
{
    public string SKU { get; set; }
    public string Description { get; set; }
    public double Price { get; set; }
}

public class Order
{
    public string ID { get; set; }
    public List<Item> Items { get; set; }
}

public class Customer
{
    public string Name { get; set; }
    public string Email { get; set; }
    public List<Order> Orders { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

我想将集合完全扁平化为DataTable具有以下DataColumn内容的单个集合:

  • Customer.Name
  • Customer.Email
  • Customer.Order.ID
  • Customer.Order.Item.SKU
  • Customer.Order.Item.Description
  • Customer.Order.Item.Price

示例实施

以下是在 Stack Overflow 上其他地方找到的示例实现,但这在对象仅包含简单属性(例如基元或字符串)而不包含其他嵌套对象时才有效。我在函数中添加了一条注释,我认为我们可以应用递归,但我不完全确定它会起作用。

public static DataTable CreateDataTableFromAnyCollection<T>(IEnumerable<T> list)
{
    Type type = typeof(T);
    var properties = type.GetProperties();

    DataTable dataTable = new DataTable();
    foreach (PropertyInfo info in properties)
    {
        dataTable.Columns.Add(new DataColumn(info.Name, Nullable.GetUnderlyingType(info.PropertyType) ?? info.PropertyType));
    }

    foreach (T entity in list)
    {
        object[] values = new object[properties.Length];
        for (int i = 0; i < properties.Length; i++)
        {
            values[i] = properties[i].GetValue(entity,null); // if not primitive pass a recursive call
        }

        dataTable.Rows.Add(values);
    }

    return dataTable;
}
Run Code Online (Sandbox Code Playgroud)

Jer*_*ney 8

如果您只使用一种类型的模型对象(在本例中为Customer),那么我推荐@Huntbook 的答案,因为这极大地简化了这个问题。

\n

也就是说,如果您确实需要这是一个通用方法,因为例如您将处理各种不同的模型对象(即,不仅仅是 Customer,那么您当然可以扩展您提出的CreateDataTableFromAnyCollection<T>()方法以支持递归,尽管它这不是一件微不足道的任务。

\n

方法

\n

递归过程并不像您想象的那么直接,因为您正在循环访问对象集合,但只需要确定一次的定义DataTable

\n

因此,将递归功能分成两个单独的方法更有意义:一个用于建立架构,另一个用于填充DataTable. 我提议:

\n
    \n
  1. EstablishDataTableFromType()DataTable,它根据给定Type(以及任何嵌套类型)动态建立 的架构,以及
  2. \n
  3. GetValuesFromObject(),对于源列表中的每个(嵌套)对象,将每个属性的值添加到值列表中,随后可以将其添加到DataTable.
  4. \n
\n

挑战

\n

上述方法掩盖了处理复杂对象和集合时引入的许多挑战。这些包括:

\n
    \n
  1. 我们如何确定一个属性是否是一个集合\xe2\x80\x94,从而受到递归的影响?我们将能够使用Type.GetInterfaces()Type.GetGenericTypeDefinition()方法来识别类型是否实现了ICollection<>. 我在下面的私有IsList()方法中实现了这一点。

    \n
  2. \n
  3. 如果它一个集合,我们如何确定该集合包含什么类型Order(例如, ,Item)?我们将能够使用 来Type.GetGenericArguments()确定 的泛型类型参数是什么ICollection<>。我在下面的私有GetListType()方法中实现了这一点。

    \n
  4. \n
  5. 考虑到每个嵌套项都需要一个额外的行,我们如何确保所有数据都得到表示?我们需要为对象图中的每个排列建立一个新记录。

    \n
  6. \n
  7. 如果您在一个对象上有两个集合,按照@DBC\'s 评论中的问题,会发生什么?我的代码假设您需要每个排列。因此,如果您添加AddressesCustomer,这可能会产生如下结果:

    \n
    \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
    顾客姓名\xe2\x80\xa6客户.订单.ID客户.订单.项目.SKU客户.地址.邮政编码
    比尔盖茨\xe2\x80\xa6000198052
    比尔盖茨\xe2\x80\xa6000298052
    比尔盖茨\xe2\x80\xa6000198039
    比尔盖茨\xe2\x80\xa6000298039
    \n
  8. \n
  9. 如果一个对象有两个相同的集合会发生什么Type您的建议推断名称DataColumn应该由 来描述Type,但这会引入命名冲突。为了解决这个问题,我认为应该使用 propertyName来作为轮廓符,而不是 property Type。例如,在您的示例模型中,DataColumnwill 是Customer.Orders.Items.SKU, not Customer.Order.Item.SKU

    \n
  10. \n
  11. 如何区分复杂对象和“原始”对象?或者,更准确地说,可以可靠地序列化为有意义的值的对象?您的问题假设集合属性将包含复杂的对象,而其他属性则不会,但这不一定是真的。例如,指向复杂对象的属性,或者相反,指向包含简单对象的集合:

    \n
    public class Order\n{\n    public List<string> CouponCodes { get; set; } = new();\n    public Address ShipTo { get; set; }\n}\n
    Run Code Online (Sandbox Code Playgroud)\n

    为了解决这个问题,我依靠@julealgon\'s 的回答如何判断一个类型是否是“简单”类型?即保存单个值。我在下面的私有IsSimple()方法中实现了这一点。

    \n
  12. \n
\n

解决方案

\n

此问题的解决方案比您引用的示例代码复杂得多我将在下面提供每种方法的简要总结。此外,我还在代码中添加了 XML 文档和一些注释。但是,如果您对任何特定功能有疑问,请询问,我将提供进一步的说明。

\n

EstablishDataTableFromType()DataTable该方法将根据给定建立一个定义Type。然而,此方法不是简单地循环遍历值,而是对发现的任何复杂类型\xe2\x80\x94(包括集合中包含的类型)进行递归。

\n
/// <summary>\n///   Populates a <paramref name="dataTable"/> with <see cref="DataColumn"/>\n///   definitions based on a given <paramref name="type"/>. Optionally prefixes\n///   the <see cref="DataColumn"/> name with a <paramref name="prefix"/> to\n///   handle nested types.\n/// </summary>\n/// <param name="type">\n///   The <see cref="Type"/> to derive the <see cref="DataColumn"/> definitions\n///   from, based on properties.\n/// </param>\n/// <param name="dataTable">\n///   The <see cref="DataTable"/> to add the <see cref="DataColumn"/>s to.\n/// </param>\n/// <param name="prefix">\n///   The prefix to prepend to the <see cref="DataColumn"/> name.\n/// </param>\n\nprivate static void EstablishDataTableFromType(Type type, DataTable dataTable, string prefix = "") {\n    var properties = type.GetProperties();\n    foreach (System.Reflection.PropertyInfo property in properties)\n    {\n\n        // Handle properties that can be meaningfully converted to a string\n        if (IsSimple(property.PropertyType))\n        {\n            dataTable.Columns.Add(\n                new DataColumn(\n                    prefix + property.Name,\n                    Nullable.GetUnderlyingType(property.PropertyType)?? property.PropertyType\n                )\n            );\n        }\n\n        // Handle collections\n        else if (IsList(property.PropertyType))\n        {\n            // If the property is a generic list, detect the generic type used\n            // for that list\n            var listType = GetListType(property.PropertyType);\n            // Recursively call this method in order to define columns for\n            // nested types\n            EstablishDataTableFromType(listType, dataTable, prefix + property.Name + ".");\n        }\n\n        // Handle complex properties\n        else {\n            EstablishDataTableFromType(property.PropertyType, dataTable, prefix + property.Name + ".");\n        }\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

GetValuesFromObject()此方法将获取一个源Object,并且对于每个属性,将属性的值添加到object[]. 如果 anObject包含一个ICollection<>属性,它将对该属性进行递归,object[]为每个排列建立 an 。

\n
/// <summary>\n///   Populates a <paramref name="target"/> list with an array of <see cref="\n///   object"/> instances representing the values of each property on a <paramref\n///   name="source"/>.\n/// </summary>\n/// <remarks>\n///   If the <paramref name="source"/> contains a nested <see cref="ICollection{T}"/>,\n///   then this method will be called recursively, resulting in a new record for\n///   every nested <paramref name="source"/> in that <see cref="ICollection{T}"/>.\n/// </remarks>\n/// <param name="type">\n///   The expected <see cref="Type"/> of the <paramref name="source"/> object.\n/// </param>\n/// <param name="source">\n///   The source <see cref="Object"/> from which to pull the property values.\n/// </param>\n/// <param name="target">\n///   A <see cref="List{T}"/> to store the <paramref name="source"/> values in.\n/// </param>\n/// <param name="columnIndex">\n///   The index associated with the property of the <paramref name="source"/>\n///   object.\n/// </param>\n\nprivate static void GetValuesFromObject(Type type, Object? source, List<object?[]> target, ref int columnIndex)\n{\n\n    var properties          = type.GetProperties();\n\n    // For each property, either write the value or recurse over the object values\n    for (int i = 0; i < properties.Length; i++)\n    {\n\n        var property        = properties[i];\n        var value           = source is null? null : property.GetValue(source, null);\n        var baseIndex       = columnIndex;\n\n        // If the property is a simple type, write its value to every instance of\n        // the target object. If there are multiple objects, the value should be\n        // written to every permutation\n        if (IsSimple(property.PropertyType))\n        {\n            foreach (var row in target)\n            {\n                row[columnIndex] = value;\n            }\n            columnIndex++;\n        }\n\n        // If the property is a generic list, recurse over each instance of that\n        // object. As part of this, establish copies of the objects in the target\n        // storage to ensure that every a new permutation is created for every\n        // nested object.\n        else if (IsList(property.PropertyType))\n        {\n            var list        = value as ICollection;\n            var collated    = new List<Object?[]>();\n\n            // If the list is null or empty, rely on the type definition to insert \n            // null values into each DataColumn.\n            if (list is null || list.Count == 0) {\n                GetValuesFromObject(GetListType(property.PropertyType), null, collated, ref columnIndex);\n                continue;\n            }\n\n            // Otherwise, for each item in the list, create a new row in the target \n            // list for its values.\n            foreach (var item in list)\n            {\n                columnIndex = baseIndex;\n                var values  = new List<Object?[]>();\n                foreach (var baseItem in target)\n                {\n                    values.Add((object?[])baseItem.Clone());\n                }\n                GetValuesFromObject(item.GetType(), item, values, ref columnIndex);\n                collated.AddRange(values);\n            }\n\n            // Finally, write each permutation of values to the target collection\n            target.Clear();\n            target.AddRange(collated);\n\n        }\n\n        // If the property is a complex type, recurse over it so that each of its\n        // properties are written to the datatable.\n        else\n        {\n            GetValuesFromObject(property.PropertyType, value, target, ref columnIndex);\n        }\n\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

CreateDataTableFromAnyCollection您提供的原始方法显然需要更新以调用EstablishDataTableFromType()GetValuesFromObject()方法,从而支持递归,而不是简单地循环遍历属性的平面列表。这很容易做到,但考虑到我编写签名的方式,它确实需要一些脚手架GetValuesFromObject()

\n
/// <summary>\n///   Given a <paramref name="list"/> of <typeparamref name="T"/> objects, will\n///   return a <see cref="DataTable"/> with a <see cref="DataRow"/> representing\n///   each instance of <typeparamref name="T"/>.\n/// </summary>\n/// <remarks>\n///   If <typeparamref name="T"/> contains any nested <see cref="ICollection{T}"/>, the\n///   schema will be flattened. As such, each instances of <typeparamref name=\n///   "T"/> will have one record for every nested item in each <see cref=\n///   "ICollection{T}"/>.\n/// </remarks>\n/// <typeparam name="T">\n///   The <see cref="Type"/> that the source <paramref name="list"/> contains a\n///   list of.\n/// </typeparam>\n/// <param name="list">\n///   A list of <typeparamref name="T"/> instances to be added to the <see cref=\n///   "DataTable"/>.\n/// </param>\n/// <returns>\n///   A <see cref="DataTable"/> containing (at least) one <see cref="DataRow"/>\n///   for each item in <paramref name="list"/>.\n/// </returns>\n\npublic static DataTable CreateDataTableFromAnyCollection<T>(IEnumerable<T> list)\n{\n\n    var dataTable           = new DataTable();\n\n    EstablishDataTableFromType(typeof(T), dataTable, typeof(T).Name + ".");\n\n    foreach (T source in list)\n    {\n        var values          = new List<Object?[]>();\n        var currentIndex    = 0;\n\n        // Establish an initial array to store the values of the source object\n        values.Add(new object[dataTable.Columns.Count]);\n\n        // Assuming the source isn\'t null, retrieve its values and add them to the \n        // DataTable.\n        if (source is not null)\n        {\n            GetValuesFromObject(source.GetType(), source, values, ref currentIndex);\n        }\n\n        // If the source object contains nested lists, then multiple permutations\n        // of the source object will be returned.\n        foreach (var value in values)\n        {\n            dataTable.Rows.Add(value);\n        }\n\n    }\n\n    return dataTable;\n\n}\n
Run Code Online (Sandbox Code Playgroud)\n

IsSimple()一种辅助方法,用于确定属性类型是否可以可靠地序列化为有意义的字符串值。如果不能,则上述函数将对其进行递归,将其每个属性值设置为DataColumn. 这是基于@julealgon\n\'s 回答如何判断一个类型是否是“简单”类型?即保存单个值

\n
/// <summary>\n///   Determine if a given <see cref="Type"/> can be reliably converted to a single\n///   <see cref="String"/> value in the <see cref="DataTable"/>.\n/// </summary>\n/// <param name="type">\n///   The <see cref="Type"/> to determine if it is a simple type.\n/// </param>\n/// <returns>\n///   Returns <c>true</c> if the <paramref name="type"/> can be reliably converted\n///   to a meaningful <see cref="String"/> value.\n/// </returns>\n\nprivate static bool IsSimple(Type type) =>\n  TypeDescriptor.GetConverter(type).CanConvertFrom(typeof(string));\n
Run Code Online (Sandbox Code Playgroud)\n

IsList()在这里,我添加了一个简单的辅助方法来确定给定属性是否是Type泛型。以及 都ICollection<>使用它。这依赖于类型和。我使用not ,因为例如实现(你不希望为字符串中的每个字符创建一个新列!)EstablishDataTableFromType()GetValuesFromObject()TypeIsGenericTypeGetGenericTypeDefinition()ICollection<>IEnumerable<>StringIEnumerable<>

\n
/// <summary>\n///   Simple helper function to determine if a given <paramref name="type"/> is a\n///   generic <see cref="ICollection{T}"/>.\n/// </summary>\n/// <param name="type">\n///   The <see cref="Type"/> to determine if it is an <see cref="ICollection{T}"/>.\n/// </param>\n/// <returns>\n///   Returns <c>true</c> if the <paramref name="type"/> is a generic <see cref=\n///   "ICollection{T}"/>.\n/// </returns>\n\nprivate static bool IsList(Type type) => type\n    .GetInterfaces()\n    .Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ICollection<>));\n
Run Code Online (Sandbox Code Playgroud)\n

GetListType()最后,我添加了另一个简单的辅助方法来确定给定Type泛型的泛型ICollection<>EstablishDataTableFromType()以及 都使用它GetValuesFromObject()。这与IsList()上面的方法非常相似,只不过它返回特定的Type,而不是仅仅确认属性类型实现了ICollection<>接口。

\n
/// <summary>\n///   Simple helper function to determine the generic <paramref name="type"/> of\n///   an <see cref="ICollection{T}"/>.\n/// </summary>\n/// <param name="type">\n///   The <see cref="Type"/> implementing <see cref="ICollection{T}"/>.\n/// </param>\n/// <returns>\n///   Returns the generic <see cref="Type"/> associated with the <see cref=\n///   "ICollection{T}"/> implemented for the <paramref name="type"/>.\n/// </returns>\n\nprivate static Type GetListType(Type type) => type\n    .GetInterfaces()\n    .Where(i => i.IsGenericType && typeof(ICollection<>) == i.GetGenericTypeDefinition())\n    .FirstOrDefault()\n    .GetGenericArguments()\n    .Last();\n
Run Code Online (Sandbox Code Playgroud)\n

验证

\n

这是一个非常简单的测试(为 XUnit 编写)来验证基本功能。DataRow这仅确认了实例中的实例数量DataTable与预期的排列数量相匹配;它不会验证每个记录中的实际数据\xe2\x80\x94,尽管我已经单独验证了数据是否正确:

\n
[Fact]\npublic void CreateDataTableFromAnyCollection() \n{\n    \n    // ARRANGE\n\n    var customers           = new List<Customer>();\n\n    // Create an object graph of Customer, Order, and Item instances, three per\n    // collection \n    for (var i = 0; i < 3; i++) \n    {\n        var customer        = new Customer() {\n            Email           = "Customer" + i + "@domain.tld",\n            Name            = "Customer " + i\n        };\n        for (var j = 0; j < 3; j++) \n        {\n            var order = new Order() \n            {\n                ID = i + "." + j\n            };\n            for (var k = 0; k < 3; k++) \n            {\n                order.Items.Add(\n                    new Item() \n                    {\n                        Description = "Item " + k,\n                        SKU = "0123-" + k,\n                        Price = i + (k * .1)\n                    }\n                );\n            }\n            customer.Orders.Add(order);\n        }\n        customers.Add(customer);\n    }\n\n    // ACT\n    var dataTable = ParentClass.CreateDataTableFromAnyCollection<Customer>(customers);\n\n    // ASSERT\n    Assert.Equal(27, dataTable.Rows.Count);\n\n    // CLEANUP VALUES\n    dataTable.Dispose();\n\n}\n
Run Code Online (Sandbox Code Playgroud)\n
\n

注意:这假设您的CreateDataTableFromAnyCollection()方法放置在名为ParentClass;显然,您需要根据应用程序的结构进行调整。

\n
\n

结论

\n

这应该可以让您很好地了解如何将对象图动态映射到扁平化的DataTable,同时还可以解决您可能会遇到的常见场景,例如引用复杂对象的属性(例如ShipTo上面的示例)以及 null 或空收藏。显然,您的特定数据模型可能会带来我的实现中无法预见的额外挑战;在这种情况下,这应该为您的构建提供坚实的基础。

\n