46

I've just learned about Generics and I'm wondering whether I can use it to dynamically build datatables from my classes.

Or I might be missing the point here. Here is my code, what I'm trying to do is create a datatable from my existing class and populate it. However I'm getting stuck in my thought process.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using System.Data;

namespace Generics
{
    public class Dog
    {
        public string Breed { get; set; }
        public string Name { get; set; }
        public int legs { get; set; }
        public bool tail { get; set; }
    }

    class Program
    {
        public static DataTable CreateDataTable(Type animaltype)
        {
            DataTable return_Datatable = new DataTable();
            foreach (PropertyInfo info in animaltype.GetProperties())
            {
                return_Datatable.Columns.Add(new DataColumn(info.Name, info.PropertyType));
            }
            return return_Datatable;
        }

        static void Main(string[] args)
        {
            Dog Killer = new Dog();
            Killer.Breed = "Maltese Poodle";
            Killer.legs = 3;
            Killer.tail = false;
            Killer.Name = "Killer";

            DataTable dogTable = new DataTable();
            dogTable = CreateDataTable(Dog);
//How do I continue from here?


        }      
    }
}    

Now At the DataTable point it errors. Also, being new to reflection and Generics, how will I actually populate the data with the Killer class?

Mike Perrenoud
  • 66,820
  • 29
  • 157
  • 232
vwdewaal
  • 975
  • 2
  • 11
  • 25
  • Okay, so you're not actually using generics, and what is the error you're getting? – Mike Perrenoud Sep 11 '13 at 16:04
  • I'm not? yikes. Error 2 The best overloaded method match for 'Generics.Program.CreateDataTable(System.Type)' has some invalid arguments Error 3 Argument 1: cannot convert from 'Generics.Dog' to 'System.Type' Error 1 'Generics.Dog' is a 'type' but is used like a 'variable' – vwdewaal Sep 11 '13 at 16:08
  • 2
    `Generics + DataBase` = `Entity Framework`. Don't reinvent the wheel, please. – Federico Berasategui Sep 11 '13 at 16:08
  • Ok, so I got the generics thing wrong, but what I'm still trying to do is create dynamic datatables off existing classes – vwdewaal Sep 11 '13 at 16:09

9 Answers9

112

Building up on all the previous answers, here is a version that creates a DataTable from any collection:

public static DataTable CreateDataTable<T>(IEnumerable<T> list)
{
    Type type = typeof(T);
    var properties = type.GetProperties();      
    
    DataTable dataTable = new DataTable();
    dataTable.TableName = typeof(T).FullName;
    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);
        }
        
        dataTable.Rows.Add(values);
    }
    
    return dataTable;
}
David Airapetyan
  • 5,301
  • 4
  • 40
  • 62
  • 3
    Excellent, I haven't touched that part of the code in over a year, but this comes just in time for something else I'm doing. Thanks David – vwdewaal Jun 11 '14 at 13:10
  • 1
    Modified the code to incorporate a suggested edit to make this code support nullable types. – David Airapetyan May 10 '15 at 16:08
  • What about nested types in the main class? How do you flatten those – HaBo Sep 15 '16 at 13:18
  • Flattening nested types would make sense if they do not contain repeating elements (otherwise this becomes pretty complex). Here is a method that can flatten a type into its basic properties: https://gist.github.com/anonymous/aeab3b4f95e6efc0d7130cf91a773bfb – David Airapetyan Sep 16 '16 at 03:05
  • 1
    How to do the same for values is left as an exercise for the reader :) – David Airapetyan Sep 16 '16 at 03:05
  • Good answer. Some suggestions for improvement: use an overloaded "Add", use var, use `IEnumerable.Select(...).ToArray`, and you can have a more compact version. See: https://stackoverflow.com/a/52673263/184528 – cdiggins Oct 05 '18 at 21:06
  • What is the purpose for `Nullable.GetUnderlyingType(info.PropertyType) ?? info.PropertyType`? Wouldn't this always get `info.PropertyType`, then if for some reason info.PropertyType was null, throw a null reference error? – Mr. Anderson Mar 26 '20 at 15:15
  • A small but worthwhile improvement for me was to add the line: dataTable.TableName = typeof(T).FullName; This sets the table name to the type name. Before putting this in, was checking results in the debugger and got concerned when the dataTable showed up as "{}" - thought it was empty. Then realized that the columns and rows were in there. – Paul Evans Mar 31 '21 at 03:28
  • Great tweak, thank you @Paul Evans, added. – David Airapetyan Mar 31 '21 at 16:40
15

Here is a more compact version of David's answer that is also an extension function. I've posted the code in a C# project on Github.

public static class Extensions
{
    public static DataTable ToDataTable<T>(this IEnumerable<T> self)
    {
        var properties = typeof(T).GetProperties();

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

        foreach (var entity in self)
            dataTable.Rows.Add(properties.Select(p => p.GetValue(entity)).ToArray());

        return dataTable;
    }     
}

I have found that this works very well in conjunction with code to write a DataTable to CSV.

cdiggins
  • 17,602
  • 7
  • 105
  • 102
9

my favorite homemade function. it create and populate all at same time. throw any object.

 public static DataTable ObjectToData(object o)
 {
    DataTable dt = new DataTable("OutputData");

    DataRow dr = dt.NewRow();
    dt.Rows.Add(dr);

    o.GetType().GetProperties().ToList().ForEach(f =>
    {
        try
        {
            f.GetValue(o, null);
            dt.Columns.Add(f.Name, f.PropertyType);
            dt.Rows[0][f.Name] = f.GetValue(o, null);
        }
        catch { }
    });
    return dt;
 }
Franck
  • 4,438
  • 1
  • 28
  • 55
5

The error can be resolved by changing this:

dogTable = CreateDataTable(Dog);

to this:

dogTable = CreateDataTable(typeof(Dog));

But there are some caveats with what you're trying to do. First, a DataTable can't store complex types, so if Dog has an instance of Cat on it, you won't be able to add that as a column. It's up to you what you want to do in that case, but keep it in mind.

Second, I would recommend that the only time you use a DataTable is when you're building code that knows nothing about the data its consuming. There are valid use cases for this (e.g. a user-driven data mining tool). If you already have the data in the Dog instance, just use it.

Another little tidbit, this:

DataTable dogTable = new DataTable();
dogTable = CreateDataTable(Dog);

can be condensed to this:

DataTable dogTable = CreateDataTable(Dog);
Mike Perrenoud
  • 66,820
  • 29
  • 157
  • 232
3

Here is a little bit modified code, which fixed time zone issue for datatime fields:

    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++)
            {
                if (props[i].PropertyType == typeof(DateTime))
                {
                    DateTime currDT = (DateTime)props[i].GetValue(item);
                    values[i] = currDT.ToUniversalTime();
                }
                else
                {
                    values[i] = props[i].GetValue(item);
                }
            }
            table.Rows.Add(values);
        }
        return table;
    }
1

Here's a VB.Net version that creates a data table from a generic list passed to the function as an object. There is also a helper function (ObjectToDataTable) that creates a data table from an object.

Imports System.Reflection

   Public Shared Function ListToDataTable(ByVal _List As Object) As DataTable

    Dim dt As New DataTable

    If _List.Count = 0 Then
        MsgBox("The list cannot be empty. This is a requirement of the ListToDataTable function.")
        Return dt
    End If

    Dim obj As Object = _List(0)
    dt = ObjectToDataTable(obj)
    Dim dr As DataRow = dt.NewRow

    For Each obj In _List

        dr = dt.NewRow

        For Each p as PropertyInfo In obj.GetType.GetProperties

            dr.Item(p.Name) = p.GetValue(obj, p.GetIndexParameters)

        Next

        dt.Rows.Add(dr)

    Next

    Return dt

End Function

Public Shared Function ObjectToDataTable(ByVal o As Object) As DataTable

    Dim dt As New DataTable
    Dim properties As List(Of PropertyInfo) = o.GetType.GetProperties.ToList()

    For Each prop As PropertyInfo In properties

        dt.Columns.Add(prop.Name, prop.PropertyType)

    Next

    dt.TableName = o.GetType.Name

    Return dt

End Function
0

Using the answer provided by @neoistheone I've changed the following sections. Works fine now.

DataTable dogTable = new DataTable();
        dogTable = CreateDataTable(typeof(Dog));

        dogTable.Rows.Add(Killer.Breed, Killer.Name,Killer.legs,Killer.tail);

        foreach (DataRow row in dogTable.Rows)
        {
            Console.WriteLine(row.Field<string>("Name") + " " + row.Field<string>("Breed"));
            Console.ReadLine();
        }
vwdewaal
  • 975
  • 2
  • 11
  • 25
0

you can convert the object to xml then load the xml document to a dataset, then extract the first table out of the data set. However i dont see how this be practical as it infers creating streams, datasets & datatables and using converstions to create the xml document.

I guess for proof of concept i can understand why. Here is an example, but somewhat hesitant to use it.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using System.Data;
using System.Xml.Serialization;

namespace Generics
{
public class Dog
{
    public string Breed { get; set; }
    public string Name { get; set; }
    public int legs { get; set; }
    public bool tail { get; set; }
}

class Program
{
    public static DataTable CreateDataTable(Object[] arr)
    {
        XmlSerializer serializer = new XmlSerializer(arr.GetType());
        System.IO.StringWriter sw = new System.IO.StringWriter();
        serializer.Serialize(sw, arr);
        System.Data.DataSet ds = new System.Data.DataSet();
        System.Data.DataTable dt = new System.Data.DataTable();
        System.IO.StringReader reader = new System.IO.StringReader(sw.ToString());

        ds.ReadXml(reader);
        return ds.Tables[0];
    }

    static void Main(string[] args)
    {
        Dog Killer = new Dog();
        Killer.Breed = "Maltese Poodle";
        Killer.legs = 3;
        Killer.tail = false;
        Killer.Name = "Killer";

        Dog [] array_dog = new Dog[5];
        Dog [0] = killer;
        Dog [1] = killer;
        Dog [2] = killer;
        Dog [3] = killer;
        Dog [4] = killer;

        DataTable dogTable = new DataTable();
        dogTable = CreateDataTable(array_dog);

        // continue here

        }      
    }
}

look the following example here

0

If you want to set columns order/ Include only some columns/ exclude some columns try this:

        private static DataTable ConvertToDataTable<T>(IList<T> data, string[] fieldsToInclude = null,
string[] fieldsToExclude = null)
    {
        PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(typeof(T));
        DataTable table = new DataTable();
        foreach (PropertyDescriptor prop in properties)
        {
            if ((fieldsToInclude != null && !fieldsToInclude.Contains(prop.Name)) ||
                (fieldsToExclude != null && fieldsToExclude.Contains(prop.Name)))
                continue;
            table.Columns.Add(prop.Name, Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType);
        }

        foreach (T item in data)
        {
            var atLeastOnePropertyExists = false;
            DataRow row = table.NewRow();
            foreach (PropertyDescriptor prop in properties)
            {

                if ((fieldsToInclude != null && !fieldsToInclude.Contains(prop.Name)) ||
(fieldsToExclude != null && fieldsToExclude.Contains(prop.Name)))
                    continue;

                row[prop.Name] = prop.GetValue(item) ?? DBNull.Value;
                atLeastOnePropertyExists = true;
            }

            if(atLeastOnePropertyExists) table.Rows.Add(row);
        }


        if (fieldsToInclude != null)
            SetColumnsOrder(table, fieldsToInclude);

        return table;

    }

    private static void SetColumnsOrder(DataTable table, params String[] columnNames)
    {
        int columnIndex = 0;
        foreach (var columnName in columnNames)
        {
            table.Columns[columnName].SetOrdinal(columnIndex);
            columnIndex++;
        }
    }
Ahmed_mag
  • 232
  • 2
  • 6