将通用的List/Enumerable转换为DataTable?

312

我有几个返回不同泛型列表的方法。

在 .net 中是否存在将任何列表转换为数据表的类静态方法或其他方法?我唯一能想象的就是使用反射来实现这一点。

如果我有以下内容:

List<Whatever> whatever = new List<Whatever>();

(当然,以下代码不能正常工作,但我希望有这个可能性:

DataTable dt = (DataTable) whatever;

2
当然,一个好问题是“为什么?” - 当List<T>在许多情况下比DataTable更好时;-p 我想每个人都有自己的选择... - Marc Gravell
1
我认为这个问题可能是这个问题的重复:https://dev59.com/RnRB5IYBdhLWcg3wxZxK 它甚至有一个非常相似的答案。 :-) - mezoid
2
@MarcGravell:我的“为什么?”是关于List<T>操作(遍历列和行)。我正在尝试从List<T>制作一个数据透视表,并通过反射访问属性,但这很麻烦。我做错了吗? - Eduardo Molteni
1
@Eduardo,有许多工具可以消除反射痛苦,例如FastMember。也可能DataTable对于特定情况非常有用-这完全取决于上下文。也许最大的问题是人们仅仅因为它存在就使用DataTable来存储所有数据,而没有花时间考虑选项和场景。 - Marc Gravell
如果你只是想要进行数据透视,为什么不使用linqLib http://linqlib.codeplex.com/ 呢?它实现了几乎所有你能想到的IEnumerable操作。 - Pedro.The.Kid
显示剩余4条评论
28个回答

375

以下是使用NuGet中的FastMember进行的2013年更新:

IEnumerable<SomeType> data = ...
DataTable table = new DataTable();
using(var reader = ObjectReader.Create(data)) {
    table.Load(reader);
}

使用FastMember的元编程API可以实现最大性能。如果您想将其限制为特定成员(或强制执行顺序),也可以这样做:
IEnumerable<SomeType> data = ...
DataTable table = new DataTable();
using(var reader = ObjectReader.Create(data, "Id", "Name", "Description")) {
    table.Load(reader);
}

编辑的免责声明:FastMember 是 Marc Gravell 的项目。它很好用,而且非常流畅!


是的,这基本上是这个的完全相反;反射就足够了 - 或者如果您需要更快的话,在2.0中使用HyperDescriptor,或者在3.5中使用Expression。实际上,HyperDescriptor应该足够了。

例如:

// remove "this" if not on C# 3.0 / .NET 3.5
public static DataTable ToDataTable<T>(this IList<T> data)
{
    PropertyDescriptorCollection props =
        TypeDescriptor.GetProperties(typeof(T));
    DataTable table = new DataTable();
    for(int i = 0 ; i < props.Count ; i++)
    {
        PropertyDescriptor prop = props[i];
        table.Columns.Add(prop.Name, prop.PropertyType);
    }
    object[] values = new object[props.Count];
    foreach (T item in data)
    {
        for (int i = 0; i < values.Length; i++)
        {
            values[i] = props[i].GetValue(item);
        }
        table.Rows.Add(values);
    }
    return table;        
}

现在,只需一行代码,您就可以通过启用对象类型THyperDescriptor,使其比反射快很多倍。


编辑关于性能查询的内容;这里是一个带有结果的测试装置:

Vanilla 27179
Hyper   6997

我怀疑瓶颈已经从成员访问转移到了DataTable性能上...我不认为你会在这方面有太大的改进...

代码:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
public class MyData
{
    public int A { get; set; }
    public string B { get; set; }
    public DateTime C { get; set; }
    public decimal D { get; set; }
    public string E { get; set; }
    public int F { get; set; }
}

static class Program
{
    static void RunTest(List<MyData> data, string caption)
    {
        GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);
        GC.WaitForPendingFinalizers();
        GC.WaitForFullGCComplete();
        Stopwatch watch = Stopwatch.StartNew();
        for (int i = 0; i < 500; i++)
        {
            data.ToDataTable();
        }
        watch.Stop();
        Console.WriteLine(caption + "\t" + watch.ElapsedMilliseconds);
    }
    static void Main()
    {
        List<MyData> foos = new List<MyData>();
        for (int i = 0 ; i < 5000 ; i++ ){
            foos.Add(new MyData
            { // just gibberish...
                A = i,
                B = i.ToString(),
                C = DateTime.Now.AddSeconds(i),
                D = i,
                E = "hello",
                F = i * 2
            });
        }
        RunTest(foos, "Vanilla");
        Hyper.ComponentModel.HyperTypeDescriptionProvider.Add(
            typeof(MyData));
        RunTest(foos, "Hyper");
        Console.ReadLine(); // return to exit        
    }
}

4
好吧,“原样”来说,它的速度大约和反射一样快。如果您启用HyperDescriptor,它将轻松击败反射...我会进行快速测试...(2分钟) - Marc Gravell
3
@MarcGravell 是的,我对Expression解决方案非常感兴趣。因为需要快速实现并具有学习效果。谢谢Marc! - Elisabeth
2
值得一提的是,为了透明度,您是FastMember的作者。您的编辑看起来就像是您偶然发现了这个伟大的软件包。 - Ellesedil
19
@Ellesedil 我努力记住明确披露这样的事情,但由于我并不是在“销售”任何东西(而是免费提供了许多工作小时),我承认我在这里并没有太多的“内疚”感... - Marc Gravell
3
您的 ToDataTable 方法不支持可空字段: 其他信息:DataSet 不支持 System.Nullable<>。 - Dainius Kreivys
显示剩余9条评论

289

我不得不修改Marc Gravell的示例代码,以处理可空类型和空值。我在下面包含了一个可工作的版本。谢谢Marc。

public static DataTable ToDataTable<T>(this IList<T> data)
{
    PropertyDescriptorCollection properties = 
        TypeDescriptor.GetProperties(typeof(T));
    DataTable table = new DataTable();
    foreach (PropertyDescriptor prop in properties)
        table.Columns.Add(prop.Name, Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType);
    foreach (T item in data)
    {
        DataRow row = table.NewRow();
        foreach (PropertyDescriptor prop in properties)
             row[prop.Name] = prop.GetValue(item) ?? DBNull.Value;
        table.Rows.Add(row);
    }
    return table;
}

这是一个很棒的答案。我希望能看到这个示例扩展出来,以处理包含项目属性并且以同样的方式创建列的分组列表。 - Unknown Coder
3
为了实现这个目标@Jim Beam,需要改变方法签名以接受GroupBy的返回值:public static DataTable ToDataTable<TKey, T>(this IEnumerable<IGrouping<TKey, T>> data)然后,在foreach循环之前添加一个额外的列:table.Columns.Add("Key", Nullable.GetUnderlyingType(typeof(TKey)) ?? typeof(TKey));接下来,在遍历数据组时,需要在数据循环周围添加一个循环:foreach (IGrouping<TKey, T> group in data) { foreach (T item in group.Items) {请参考此GIST获取完整信息: https://gist.github.com/rickdailey/8679306 - Rick Dailey
嘿,有没有办法处理具有内部对象的对象?我只想要父对象列后面出现内部属性列。 - heyNow
@heyNow,我相信有这样的功能。但是,对于我所做的事情,我并没有真正需要那个功能,所以我把它留给其他人来扩展。 :) - Mary Hamlin
2
这是一篇旧帖子,所以不确定这个评论有多少用处,但是在 ToDataTable 方法中有一个难以察觉的 bug。如果 T 实现了一个接口,则 typeof(T) 可能会返回接口类型而不是对象的实际类,导致 DataTable 为空。将其替换为 data.First().GetType() 应该可以解决问题。 - Janilson
显示剩余3条评论

18

另一种方法如下:

  List<WhateEver> lst = getdata();
  string json = Newtonsoft.Json.JsonConvert.SerializeObject(lst);
  DataTable pDt = JsonConvert.DeserializeObject<DataTable>(json);

非常好...但它抛出了类型为'System.OutOfMemoryException'的异常。我使用了它500,000个项目...但还是谢谢你。 - st_stefanov
2
这绝对是我在网络上找到的最干净的解决方案。做得好! - Sarah
请注意,DataTable 的数据类型与 List<object> 不同。例如:对象中的 decimal 类型在 DataTable 中为 double 类型。 - qnguyen

15

这是解决方案的简单混合体。

它适用于可空类型。

public static DataTable ToDataTable<T>(this IList<T> list)
{
  PropertyDescriptorCollection props = TypeDescriptor.GetProperties(typeof(T));
  DataTable table = new DataTable();
  for (int i = 0; i < props.Count; i++)
  {
    PropertyDescriptor prop = props[i];
    table.Columns.Add(prop.Name, Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType);
  }
  object[] values = new object[props.Count];
  foreach (T item in list)
  {
    for (int i = 0; i < values.Length; i++)
      values[i] = props[i].GetValue(item) ?? DBNull.Value;
    table.Rows.Add(values);
  }
  return table;
}

1
这个解决方案容易出错,因为它依赖于T类中属性声明的顺序。 - Vahid Ghadiri

15

Marc的回答做了一点小修改,使它能够适用于像List<string>这样的值类型到数据表:

public static DataTable ListToDataTable<T>(IList<T> data)
{
    DataTable table = new DataTable();

    //special handling for value types and string
    if (typeof(T).IsValueType || typeof(T).Equals(typeof(string)))
    {

        DataColumn dc = new DataColumn("Value", typeof(T));
        table.Columns.Add(dc);
        foreach (T item in data)
        {
            DataRow dr = table.NewRow();
            dr[0] = item;
            table.Rows.Add(dr);
        }
    }
    else
    {
        PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(typeof(T));
        foreach (PropertyDescriptor prop in properties)
        {
            table.Columns.Add(prop.Name, Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType);
        }
        foreach (T item in data)
        {
            DataRow row = table.NewRow();
            foreach (PropertyDescriptor prop in properties)
            {
                try
                {
                    row[prop.Name] = prop.GetValue(item) ?? DBNull.Value;
                }
                catch (Exception ex)
                {
                    row[prop.Name] = DBNull.Value;
                }
            }
            table.Rows.Add(row);
        }
    }
    return table;
}

如何将其应用于 List<int>? - Muflix
1
以上方法同样适用于 int(以及其他值类型)... int 是值类型。请参阅:https://msdn.microsoft.com/zh-cn/library/s1ax56ch.aspx - Onur Omer
我喜欢这个方法,因为它不依赖于使用扩展方法。对于可能无法访问扩展方法的旧代码库来说,这个方法非常有效。 - webworm

13

这个MSDN链接值得一看:如何:实现CopyToDataTable<T>方法,其中的泛型类型T不是DataRow

它添加了一个扩展方法使你可以这样做:

// Create a sequence. 
Item[] items = new Item[] 
{ new Book{Id = 1, Price = 13.50, Genre = "Comedy", Author = "Gustavo Achong"}, 
  new Book{Id = 2, Price = 8.50, Genre = "Drama", Author = "Jessie Zeng"},
  new Movie{Id = 1, Price = 22.99, Genre = "Comedy", Director = "Marissa Barnes"},
  new Movie{Id = 1, Price = 13.40, Genre = "Action", Director = "Emmanuel Fernandez"}};

// Query for items with price greater than 9.99.
var query = from i in items
             where i.Price > 9.99
             orderby i.Price
             select i;

// Load the query results into new DataTable.
DataTable table = query.CopyToDataTable();

@PaulWilliams 谢谢,我使用这段代码已经好几年了,到目前为止还没有出现任何问题。但是由于我没有复制微软的示例代码,而只是链接到了网站,其他解决方案至少更符合最佳实践答案 https://stackoverflow.com/help/how-to-answer - Jürgen Steinblock

9
List<YourModel> data = new List<YourModel>();
DataTable dataTable = Newtonsoft.Json.JsonConvert.DeserializeObject<DataTable>(Newtonsoft.Json.JsonConvert.SerializeObject(data));

1
虽然这段代码可能回答了问题,但提供关于它是如何解决问题的额外上下文信息会提高答案的长期价值。 - Klaus Gütter
太棒了。我使用这种方式来实现这个案例。 - toha

7
public DataTable ConvertToDataTable<T>(IList<T> data)
{
    PropertyDescriptorCollection properties =
        TypeDescriptor.GetProperties(typeof(T));

    DataTable table = new DataTable();

    foreach (PropertyDescriptor prop in properties)
            table.Columns.Add(prop.Name, Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType);

    foreach (T item in data)
    {
        DataRow row = table.NewRow();
        foreach (PropertyDescriptor prop in properties)
        {
           row[prop.Name] = prop.GetValue(item) ?? DBNull.Value;
        }
        table.Rows.Add(row);
    }
    return table;
}

这个解决方案容易出错,因为它依赖于T类中属性声明的顺序。 - Vahid Ghadiri

6

试一试

public static DataTable ListToDataTable<T>(IList<T> lst)
{

    currentDT = CreateTable<T>();

    Type entType = typeof(T);

    PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(entType);
    foreach (T item in lst)
    {
        DataRow row = currentDT.NewRow();
        foreach (PropertyDescriptor prop in properties)
        {

            if (prop.PropertyType == typeof(Nullable<decimal>) || prop.PropertyType == typeof(Nullable<int>) || prop.PropertyType == typeof(Nullable<Int64>))
            {
                if (prop.GetValue(item) == null)
                    row[prop.Name] = 0;
                else
                    row[prop.Name] = prop.GetValue(item);
            }
            else
                row[prop.Name] = prop.GetValue(item);                    

        }
        currentDT.Rows.Add(row);
    }

    return currentDT;
}

public static DataTable CreateTable<T>()
{
    Type entType = typeof(T);
    DataTable tbl = new DataTable(DTName);
    PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(entType);
    foreach (PropertyDescriptor prop in properties)
    {
        if (prop.PropertyType == typeof(Nullable<decimal>))
             tbl.Columns.Add(prop.Name, typeof(decimal));
        else if (prop.PropertyType == typeof(Nullable<int>))
            tbl.Columns.Add(prop.Name, typeof(int));
        else if (prop.PropertyType == typeof(Nullable<Int64>))
            tbl.Columns.Add(prop.Name, typeof(Int64));
        else
             tbl.Columns.Add(prop.Name, prop.PropertyType);
    }
    return tbl;
}

无法编译 - aron

6
It's also possible through XmlSerialization.
The idea is - serialize to `XML` and then `readXml` method of `DataSet`.

I use this code (from an answer in SO, forgot where)

        public static string SerializeXml<T>(T value) where T : class
    {
        if (value == null)
        {
            return null;
        }

        XmlSerializer serializer = new XmlSerializer(typeof(T));

        XmlWriterSettings settings = new XmlWriterSettings();

        settings.Encoding = new UnicodeEncoding(false, false);
        settings.Indent = false;
        settings.OmitXmlDeclaration = false;
        // no BOM in a .NET string

        using (StringWriter textWriter = new StringWriter())
        {
            using (XmlWriter xmlWriter = XmlWriter.Create(textWriter, settings))
            {
               serializer.Serialize(xmlWriter, value);
            }
            return textWriter.ToString();
        }
    }

so then it's as simple as:

            string xmlString = Utility.SerializeXml(trans.InnerList);

        DataSet ds = new DataSet("New_DataSet");
        using (XmlReader reader = XmlReader.Create(new StringReader(xmlString)))
        { 
            ds.Locale = System.Threading.Thread.CurrentThread.CurrentCulture;
            ds.ReadXml(reader); 
        }

Not sure how it stands against all the other answers to this post, but it's also a possibility.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接