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.NameCustomer.EmailCustomer.Order.IDCustomer.Order.Item.SKUCustomer.Order.Item.DescriptionCustomer.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)
如果您只使用一种类型的模型对象(在本例中为Customer),那么我推荐@Huntbook 的答案,因为这极大地简化了这个问题。
也就是说,如果您确实需要这是一个通用方法,因为例如您将处理各种不同的模型对象(即,不仅仅是) Customer,那么您当然可以扩展您提出的CreateDataTableFromAnyCollection<T>()方法以支持递归,尽管它这不是一件微不足道的任务。
递归过程并不像您想象的那么直接,因为您正在循环访问对象集合,但只需要确定一次的定义DataTable。
因此,将递归功能分成两个单独的方法更有意义:一个用于建立架构,另一个用于填充DataTable. 我提议:
EstablishDataTableFromType()DataTable,它根据给定Type(以及任何嵌套类型)动态建立 的架构,以及GetValuesFromObject(),对于源列表中的每个(嵌套)对象,将每个属性的值添加到值列表中,随后可以将其添加到DataTable.上述方法掩盖了处理复杂对象和集合时引入的许多挑战。这些包括:
\n我们如何确定一个属性是否是一个集合\xe2\x80\x94,从而受到递归的影响?我们将能够使用Type.GetInterfaces()和Type.GetGenericTypeDefinition()方法来识别类型是否实现了ICollection<>. 我在下面的私有IsList()方法中实现了这一点。
如果它是一个集合,我们如何确定该集合包含什么类型Order(例如, ,Item)?我们将能够使用 来Type.GetGenericArguments()确定 的泛型类型参数是什么ICollection<>。我在下面的私有GetListType()方法中实现了这一点。
考虑到每个嵌套项都需要一个额外的行,我们如何确保所有数据都得到表示?我们需要为对象图中的每个排列建立一个新记录。
\n如果您在一个对象上有两个集合,按照@DBC\'s 评论中的问题,会发生什么?我的代码假设您需要每个排列。因此,如果您添加Addresses到Customer,这可能会产生如下结果:
| 顾客姓名 | \xe2\x80\xa6 | 客户.订单.ID | 客户.订单.项目.SKU | 客户.地址.邮政编码 |
|---|---|---|---|---|
| 比尔盖茨 | \xe2\x80\xa6 | 0 | 001 | 98052 |
| 比尔盖茨 | \xe2\x80\xa6 | 0 | 002 | 98052 |
| 比尔盖茨 | \xe2\x80\xa6 | 0 | 001 | 98039 |
| 比尔盖茨 | \xe2\x80\xa6 | 0 | 002 | 98039 |
如果一个对象有两个相同的集合会发生什么Type?您的建议推断名称DataColumn应该由 来描述Type,但这会引入命名冲突。为了解决这个问题,我认为应该使用 propertyName来作为轮廓符,而不是 property Type。例如,在您的示例模型中,DataColumnwill 是Customer.Orders.Items.SKU, not Customer.Order.Item.SKU。
如何区分复杂对象和“原始”对象?或者,更准确地说,可以可靠地序列化为有意义的值的对象?您的问题假设集合属性将包含复杂的对象,而其他属性则不会,但这不一定是真的。例如,指向复杂对象的属性,或者相反,指向包含简单对象的集合:
\npublic class Order\n{\n public List<string> CouponCodes { get; set; } = new();\n public Address ShipTo { get; set; }\n}\nRun Code Online (Sandbox Code Playgroud)\n为了解决这个问题,我依靠@julealgon\'s 的回答如何判断一个类型是否是“简单”类型?即保存单个值。我在下面的私有IsSimple()方法中实现了这一点。
此问题的解决方案比您引用的示例代码复杂得多。我将在下面提供每种方法的简要总结。此外,我还在代码中添加了 XML 文档和一些注释。但是,如果您对任何特定功能有疑问,请询问,我将提供进一步的说明。
\nEstablishDataTableFromType():DataTable该方法将根据给定建立一个定义Type。然而,此方法不是简单地循环遍历值,而是对发现的任何复杂类型\xe2\x80\x94(包括集合中包含的类型)进行递归。
/// <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}\nRun Code Online (Sandbox Code Playgroud)\nGetValuesFromObject():此方法将获取一个源Object,并且对于每个属性,将属性的值添加到object[]. 如果 anObject包含一个ICollection<>属性,它将对该属性进行递归,object[]为每个排列建立 an 。
/// <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}\nRun Code Online (Sandbox Code Playgroud)\nCreateDataTableFromAnyCollection:您提供的原始方法显然需要更新以调用EstablishDataTableFromType()和GetValuesFromObject()方法,从而支持递归,而不是简单地循环遍历属性的平面列表。这很容易做到,但考虑到我编写签名的方式,它确实需要一些脚手架GetValuesFromObject()。
/// <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}\nRun Code Online (Sandbox Code Playgroud)\nIsSimple():一种辅助方法,用于确定属性类型是否可以可靠地序列化为有意义的字符串值。如果不能,则上述函数将对其进行递归,将其每个属性值设置为DataColumn. 这是基于@julealgon\n\'s 回答如何判断一个类型是否是“简单”类型?即保存单个值。
/// <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));\nRun Code Online (Sandbox Code Playgroud)\nIsList():在这里,我添加了一个简单的辅助方法来确定给定属性是否是Type泛型。以及 都ICollection<>使用它。这依赖于类型和。我使用not ,因为例如实现(你不希望为字符串中的每个字符创建一个新列!)EstablishDataTableFromType()GetValuesFromObject()TypeIsGenericTypeGetGenericTypeDefinition()ICollection<>IEnumerable<>StringIEnumerable<>
/// <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<>));\nRun Code Online (Sandbox Code Playgroud)\nGetListType():最后,我添加了另一个简单的辅助方法来确定给定Type泛型的泛型ICollection<>。EstablishDataTableFromType()以及 都使用它GetValuesFromObject()。这与IsList()上面的方法非常相似,只不过它返回特定的Type,而不是仅仅确认属性类型实现了ICollection<>接口。
/// <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();\nRun Code Online (Sandbox Code Playgroud)\n这是一个非常简单的测试(为 XUnit 编写)来验证基本功能。DataRow这仅确认了实例中的实例数量DataTable与预期的排列数量相匹配;它不会验证每个记录中的实际数据\xe2\x80\x94,尽管我已经单独验证了数据是否正确:
[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}\nRun Code Online (Sandbox Code Playgroud)\n\n\n注意:这假设您的
\nCreateDataTableFromAnyCollection()方法放置在名为ParentClass;显然,您需要根据应用程序的结构进行调整。
这应该可以让您很好地了解如何将对象图动态映射到扁平化的DataTable,同时还可以解决您可能会遇到的常见场景,例如引用复杂对象的属性(例如ShipTo上面的示例)以及 null 或空收藏。显然,您的特定数据模型可能会带来我的实现中无法预见的额外挑战;在这种情况下,这应该为您的构建提供坚实的基础。
| 归档时间: |
|
| 查看次数: |
1772 次 |
| 最近记录: |