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

我找到了一段代码,它获取一组对象并将它们映射到DataTable(见下文),但它只支持基本属性类型.

我认为这只能通过递归实现,但也许有更好的方法来实现.

数据模型

假设我们有ListCustomer个对象:

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

我想将该系列完全扁平化为一个DataTable,包括以下DataColumn:

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

示例实现

下面是在堆栈溢出上的其他地方找到的一个示例实现,但是如果对象只包含基本属性,而不包含其他嵌套对象,那么这将起作用.我在函数中添加了一条注释,我认为我们可以在其中应用递归,但我不完全确定它是否有效.

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

推荐答案

如果您只使用一种类型的业务对象(在本例中为Customer),那么我建议使用@Huntbook's answer,因为dramatically简化了这个问题.

这就是说,如果你真的认为这是一个generic方法,因为例如,你将处理各种不同的业务对象(即,不完全是Customer),那么你当然可以扩展你提议的CreateDataTableFromAnyCollection<T>()方法来支持递归,尽管这不是一个简单的任务.

我将在下面介绍basic个实现.这将有一些局限性,我将在最后讨论.这些可能对你的申请有影响,也可能无关紧要.无论如何,这将为动态地将对象图转换为平坦的DataTable奠定基础,您可以利用它来构建.

基本方法

这个过程并不像你期望的那样简单,因为你在一个对象集合中循环,但只需要确定每种类型DataTable的定义.

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

  1. EstablishDataTableFromType(),它基于给定的Type(以及任何嵌套类型)动态地建立DataTable的模式,以及
  2. GetValuesFromObject(),对于源列表中的每个(嵌套)对象,将每个属性中的值添加到值列表中,随后可以将值列表添加到DataTable.

挑战

然而,基本方法人掩盖了一些值得承认的挑战.这些措施包括:

  1. 我们如何确定一个属性是否是一个集合,从而受制于递归?我们可以用Type.GetGenericTypeDefinition()来做这个.

  2. 如果它是一个集合,我们如何确定该集合包含哪些type(例如,OrderItem)?我们可以用Type.GetGenericArguments()来做这个.

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

  4. 如果一个对象上有两个集合(按照@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

    这可能不是一个有效的假设,但您可以根据自己的需求调整逻辑.

  5. 如果一个对象有两个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型的IsGenericTypeGetGenericTypeDefinition().

/// <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编写)来验证功能.这是一个非常简单的测试,只需确认DataTableDataRow个实例的数量与预期的排列数量匹配;虽然我已经分别验证了数据的正确性,但它并没有验证每条记录中的实际数据:

[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();

}

局限性

这是一个复杂的问题,我的基本实现有很多限制:

  1. Complex Properties:支持的only类非基本属性类型是泛型List<T>.因此,任何其他类型的对象都可能导致异常,或者至少是意外数据.生产版本可能希望:

    • 在不存储在List<>中的复杂属性上递归,
    • 通过IList<>IDictionary<>而不是List<>支持其他收集格式,
    • 通过例如PropertyInfo.IsPrimitive属性检测不支持的属性类型,并跳过它们或引发验证异常,
    • 支持,例如,跳过特定属性映射的自定义属性(例如您无意支持的属性).
  2. List Initialization:assumes说明你的列表是作为你的model的一部分初始化的;它不会为您初始化列表,如果尚未初始化,它将抛出异常.如果你的数据模型不能保证你的列表会被填充,你可以在模型中通过.,

    public List<Item> Items { get; set; } = new();
    

    检测和跳过空属性显然并不困难.问题是,目前的GetValuesFromObject()法依赖于判断属性来增加columnIndex属性;为了支持空值,它需要一种更复杂的方法来跟踪哪个属性对应于数组中的哪个位置.因此,只需预先初始化这些属性就更容易了.

  3. Inheritance:与您的原始代码一样,这only查看模型的顶级属性.如果您希望您的模型从其他模型继承inherit个,那么这将带来问题(例如,如果Customer个从Person个继承).添加对继承属性的支持很简单,但如果从基类库中的类型继承,可能会发现其中包含一些不希望映射的属性.

这些限制都不会影响示例模型,如果您在编写模型时考虑到这些限制,它们也不会成为一个问题.但是,如果您使用的是一个更复杂、更成熟的模型,那么您可能需要改进我的代码以解决这些限制.然而,解决这些问题会使这个已经很复杂的代码更加复杂,所以我将把它们留给您在实现中解决.

结论

这将使您很好地了解如何动态地将对象图映射到平坦的DataTable,同时为基于模型的特定需求进行构建提供坚实的基础.

Csharp相关问答推荐

程序集.加载从exe的异常

通过EFCore上传大量数据.

迭代C#List并在数据库中 for each 元素执行SELECT语句—更有效的方式?

是否可以使用EF—Core进行临时部分更新?

将XPS转换为PDF C#

Polly使用泛型重试和重试包装函数

如何在没有额外副本的情况下将存储在IntPtr下的原始图像数据写入WinUI3中的Image控件?

在C#中,有没有一种方法可以集中定义跨多个方法使用的XML参数描述符?

如何解决提交按钮后 Select 选项错误空参照异常

HttpRequestMessage.SetPolicyExecutionContext不会将上下文传递给策略

正在寻找新的.NET8 Blazor Web应用程序.如何将.js添加到.razor页面?

如何让两个.NET版本不兼容的项目对话?

如何在.NET Maui中将事件与MVVM一起使用?

如何将%{v_扩展}转换为%{v_扩展}>>

在同一个捕获中可以有多种类型的异常吗?

C#System.Commandline:如何向命令添加参数以便向其传递值?

C#中使用ReadOnlySpan的泛型类型推理

单位中快照的倾斜方向

使用Try-Catch-Finally为API端点处理代码--有什么缺点?

缩写的MonthNames有问题