如果您只使用一种类型的业务对象(在本例中为Customer
),那么我建议使用@Huntbook's answer,因为dramatically简化了这个问题.
这就是说,如果你真的认为这是一个generic方法,因为例如,你将处理各种不同的业务对象(即,不完全是Customer
),那么你当然可以扩展你提议的CreateDataTableFromAnyCollection<T>()
方法来支持递归,尽管这不是一个简单的任务.
我将在下面介绍basic个实现.这将有一些局限性,我将在最后讨论.这些可能对你的申请有影响,也可能无关紧要.无论如何,这将为动态地将对象图转换为平坦的DataTable
奠定基础,您可以利用它来构建.
基本方法
这个过程并不像你期望的那样简单,因为你在一个对象集合中循环,但只需要确定每种类型DataTable
的定义.
因此,将递归功能分离为两个单独的方法更有意义,一个用于建立模式,另一个用于填充DataTable
.我提议:
EstablishDataTableFromType()
,它基于给定的Type
(以及任何嵌套类型)动态地建立DataTable
的模式,以及
GetValuesFromObject()
,对于源列表中的每个(嵌套)对象,将每个属性中的值添加到值列表中,随后可以将值列表添加到DataTable
.
挑战
然而,基本方法人掩盖了一些值得承认的挑战.这些措施包括:
我们如何确定一个属性是否是一个集合,从而受制于递归?我们可以用Type.GetGenericTypeDefinition()
来做这个.
如果它是一个集合,我们如何确定该集合包含哪些type(例如,Order
、Item
)?我们可以用Type.GetGenericArguments()
来做这个.
考虑到每个嵌套项都需要额外的一行,我们如何确保表示all个数据?我们需要为对象图中的每个排列建立一个新记录.
如果一个对象上有两个集合(按照@DBC's question in the comments),会发生什么?我的代码假设你需要对每一个进行排列.所以如果你把Addresses
加到Customer
,这可能会产生如下结果:
Customer.Name |
… |
Customer.Orders.ID |
Customer.Orders.Items.SKU |
Customer.Addresses.PostalCode |
Bill Gates |
… |
0 |
001 |
98052 |
Bill Gates |
… |
0 |
002 |
98052 |
Bill Gates |
… |
0 |
001 |
98039 |
Bill Gates |
… |
0 |
002 |
98039 |
这可能不是一个有效的假设,但您可以根据自己的需求调整逻辑.
如果一个对象有两个Type
个相同的集合,会发生什么?你的提议推断出DataColumn
个名字应该用Type
个来划分,但这会导致命名冲突.为了解决这个问题,我假设应该使用属性名称作为轮廓线,而不是属性Type
.
解决方案
代码相当复杂.下面我将简要总结每种方法.此外,我在代码中添加了some条注释.但是,如果您对任何特定功能有疑问,请询问,我将提供进一步的说明.
100:该方法将基于给定的Type
建立DataTable
定义.然而,这种方法将在发现的List<T>
种类型上递归,而不是简单地循环遍历值.
/// <summary>
/// Populates a <paramref name="dataTable"/> with <see cref="DataColumn"/>
/// definitions based on a given <paramref name="type"/>. Optionally prefixes
/// the <see cref="DataColumn"/> name with a <paramref name="prefix"/> to
/// handle nested types.
/// </summary>
/// <param name="type">
/// The <see cref="Type"/> to derive the <see cref="DataColumn"/> definitions
/// from, based on properties.
/// </param>
/// <param name="dataTable">
/// The <see cref="DataTable"/> to add the <see cref="DataColumn"/> definitions to.
/// </param>
/// <param name="prefix">
/// The prefix to prepend to the <see cref="DataColumn"/> definition.
/// </param>
private static void EstablishDataTableFromType(Type type, DataTable dataTable, string prefix = "") {
var properties = type.GetProperties();
foreach (System.Reflection.PropertyInfo property in properties)
{
if (!IsList(property.PropertyType))
{
dataTable.Columns.Add(
new DataColumn(
prefix + property.Name,
Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType
)
);
}
else
{
// If the property is a generic list, detect the generic type used
// for that list
var listType = property.PropertyType.GetGenericArguments().First();
// Recursively call this method in order to define columns for
// nested types
EstablishDataTableFromType(listType, dataTable, prefix + property.Name + ".");
}
}
}
100:此方法将获取源Object
,并将每个属性的值添加到源object[]
中.如果Object
包含List<>
属性,它将在该属性上递归, for each 置换建立object[]
.
/// <summary>
/// Populates a <paramref name="target"/> list with an array of <see cref="
/// object"/> instances representing the values of each property on a <paramref
/// name="source"/>.
/// </summary>
/// <remarks>
/// If the <paramref name="source"/> contains a nested <see cref="List{T}"/>,
/// then this method will be called recursively, resulting in a new record for
/// every nested <paramref name="source"/> in that <see cref="List{T}"/>.
/// </remarks>
/// <param name="source">
/// The source <see cref="Object"/> from which to pull the property values.
/// </param>
/// <param name="target">
/// A <see cref="List{T}"/> to store the <paramref name="source"/> values in.
/// </param>
/// <param name="columnIndex">
/// The index associated with the property of the <paramref name="source"/>
/// object.
/// </param>
private static void GetValuesFromObject(Object source, List<object[]> target, ref int columnIndex)
{
var type = source.GetType();
var properties = type.GetProperties();
for (int i = 0; i < properties.Length; i++)
{
var property = properties[i];
var value = property.GetValue(source, null);
var baseIndex = columnIndex;
// If the property is not a list, write its value to every instance of
// the target object. If there are multiple objects, the value should be
// written to every permutation
if (!IsList(property.PropertyType))
{
foreach (var row in target)
{
row[columnIndex] = value;
}
columnIndex++;
}
// If the property is a generic list, recurse over each instance of that
// object. As part of this, establish copies of the objects in the target
// storage to ensure that every a new permutation is created for every
// nested object.
else
{
var list = (value as IList)!;
var collated = new List<Object[]>();
foreach (var item in list)
{
columnIndex = baseIndex;
var values = new List<Object[]>();
foreach (var baseItem in target)
{
values.Add((object[])baseItem.Clone());
}
GetValuesFromObject(item, values, ref columnIndex);
collated.AddRange(values);
}
target.Clear();
target.AddRange(collated);
}
}
}
100:您提供的原始方法显然需要更新,以调用EstablishDataTableFromType()
和GetValuesFromObject()
方法,从而支持递归,而不是简单地在一个简单的属性列表上循环.这很容易做到,尽管考虑到我写GetValuesFromObject()
签名的方式,它确实需要一些脚手架.
/// <summary>
/// Given a <paramref name="list"/> of <typeparamref name="T"/> objects, will
/// return a <see cref="DataTable"/> with a <see cref="DataRow"/> representing
/// each instance of <typeparamref name="T"/>.
/// </summary>
/// <remarks>
/// If <typeparamref name="T"/> contains any nested <see cref="IList{T}"/>, the
/// schema will be flattened. As such, each instances of <typeparamref name=
/// "T"/> will have one record for every nested item in each <see cref=
/// "IList{T}"/>.
/// </remarks>
/// <typeparam name="T">
/// The <see cref="Type"/> that the source <paramref name="list"/> contains a
/// list of.
/// </typeparam>
/// <param name="list">
/// A list of <typeparamref name="T"/> instances to be added to the <see cref=
/// "DataTable"/>.
/// </param>
/// <returns>
/// A <see cref="DataTable"/> containing (at least) one <see cref="DataRow"/>
/// for each item in <paramref name="list"/>.
/// </returns>
public static DataTable CreateDataTableFromAnyCollection<T>(IEnumerable<T> list)
{
var dataTable = new DataTable();
EstablishDataTableFromType(typeof(T), dataTable, typeof(T).Name + ".");
foreach (T source in list)
{
var values = new List<Object[]>();
var currentIndex = 0;
// Establish an initial array to store the values of the source object
values.Add(new object[dataTable.Columns.Count]);
if (source is not null)
{
GetValuesFromObject(source, values, ref currentIndex);
}
// If the source object contains nested lists, then multiple permutations
// of source object will be returned
foreach (var value in values)
{
dataTable.Rows.Add(value);
}
}
return dataTable;
}
100:最后,我添加了一个简单的助手方法,用于确定给定属性的Type
是否为泛型List<>
.EstablishDataTableFromType()
和GetValuesFromObject()
都使用它.这取决于Type
型的IsGenericType
和GetGenericTypeDefinition()
.
/// <summary>
/// Simple helper function to determine if a given <paramref name="type"/> is a
/// generic <see cref="List{T}"/>.
/// </summary>
/// <param name="type">
/// The <see cref="Type"/> to determine if it is a <see cref="List{T}"/>
/// </param>
/// <returns>
/// Returns <c>true</c> if the <paramref name="type"/> is a generic <see cref=
/// "List{T}"/>.
/// </returns>
private static bool IsList(Type type) => type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>);
验证
下面是一个简单的单元测试(为XUnit编写)来验证功能.这是一个非常简单的测试,只需确认DataTable
中DataRow
个实例的数量与预期的排列数量匹配;虽然我已经分别验证了数据的正确性,但它并没有验证每条记录中的实际数据:
[Fact]
public void CreateDataTableFromAnyCollection()
{
// ARRANGE
var customers = new List<Customer>();
// Create an object graph of Customer, Order, and Item instances, three per
// collection
for (var i = 0; i < 3; i++)
{
var customer = new Customer() {
Email = "Customer" + i + "@domain.tld",
Name = "Customer " + i
};
for (var j = 0; j < 3; j++)
{
var order = new Order()
{
ID = i + "." + j
};
for (var k = 0; k < 3; k++)
{
order.Items.Add(
new Item()
{
Description = "Item " + k,
SKU = "0123-" + k,
Price = i + (k * .1)
}
);
}
customer.Orders.Add(order);
}
customers.Add(customer);
}
// ACT
var dataTable = SqlCommandExtensions.CreateDataTableFromAnyCollection<Customer>(customers);
// ASSERT
Assert.Equal(27, dataTable.Rows.Count);
// CLEANUP VALUES
dataTable.Dispose();
}
局限性
这是一个复杂的问题,我的基本实现有很多限制:
Complex Properties:支持的only类非基本属性类型是泛型List<T>
.因此,任何其他类型的对象都可能导致异常,或者至少是意外数据.生产版本可能希望:
- 在不存储在
List<>
中的复杂属性上递归,
- 通过
IList<>
和IDictionary<>
而不是List<>
支持其他收集格式,
- 通过例如
PropertyInfo.IsPrimitive
属性检测不支持的属性类型,并跳过它们或引发验证异常,
- 支持,例如,跳过特定属性映射的自定义属性(例如您无意支持的属性).
List Initialization:这assumes说明你的列表是作为你的model的一部分初始化的;它不会为您初始化列表,如果尚未初始化,它将抛出异常.如果你的数据模型不能保证你的列表会被填充,你可以在模型中通过.,
public List<Item> Items { get; set; } = new();
检测和跳过空属性显然并不困难.问题是,目前的GetValuesFromObject()
法依赖于判断属性来增加columnIndex
属性;为了支持空值,它需要一种更复杂的方法来跟踪哪个属性对应于数组中的哪个位置.因此,只需预先初始化这些属性就更容易了.
Inheritance:与您的原始代码一样,这only查看模型的顶级属性.如果您希望您的模型从其他模型继承inherit个,那么这将带来问题(例如,如果Customer
个从Person
个继承).添加对继承属性的支持很简单,但如果从基类库中的类型继承,可能会发现其中包含一些不希望映射的属性.
这些限制都不会影响示例模型,如果您在编写模型时考虑到这些限制,它们也不会成为一个问题.但是,如果您使用的是一个更复杂、更成熟的模型,那么您可能需要改进我的代码以解决这些限制.然而,解决这些问题会使这个已经很复杂的代码更加复杂,所以我将把它们留给您在实现中解决.
结论
这将使您很好地了解如何动态地将对象图映射到平坦的DataTable
,同时为基于模型的特定需求进行构建提供坚实的基础.