How to convert Object contains IList<class> which further contains 6 properties - 4 string, 1 IList<anotherclass>, 1 IList<string> to DataTable?

Viewed 239

I am trying to convert my Object which have only 1 item (IList of class), this class contains 6 properties - 4 string, 1 IList of anotherClass and 1 IList of string. I am using below generic method to convert :-

public static DataTable ToDataTable<T>(IList<T> items)
    {
        DataTable dataTable = new DataTable(typeof(T).Name);

        //Get all the properties
        PropertyInfo[] Props = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
        foreach (PropertyInfo prop in Props)
        {
            //Defining type of data column gives proper data table 
            var type = (prop.PropertyType.IsGenericType && prop.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>) ? Nullable.GetUnderlyingType(prop.PropertyType) : prop.PropertyType);
            //Setting column names as Property names
            dataTable.Columns.Add(prop.Name, type);
        }

        foreach (T item in items)
        {
            var values = new object[Props.Length];
            for (int i = 0; i < Props.Length; i++)
            {
                //inserting property values to datatable rows
                values[i] = Props[i].GetValue(item, null);
            }
            dataTable.Rows.Add(values);
        }
        //put a breakpoint here and check datatable
        return dataTable;
    }

At this point - dataTable.Rows.Add(values); I have the list values for 4 and 5 properties but when I add those to row, it shows text(type of property) in the row. While I need each item of 4 and 5 properties to come in next Row. For Example :-

values[0] - "FirstString",
values[1] - "SecondString",
values[2] - "ThirdString",
values[3] - "FourthString",
values[4] - Count 2 - 100, 200 (IList<AnotherClass>()),
values [5] - Count 2 - "10W", "20W" (IList<string>)

I want below type of output :-

1String     | 2String      | 3String     | 4String      | Per | W  |
:-----------|:------------:|:-----------:|:------------:|:---:|---:|
FirstString | SecondString | ThirdString | FourthString | 100 | 10W|
FirstString | SecondString | ThirdString | FourthString | 200 | 20W|

While the output I am currently getting is :-

1String     | 2String      | 3String     | 4String      | Per            | W                 |
:-----------|:------------:|:-----------:|:------------:|:--------------:|--------------:|
FirstString | SecondString | ThirdString | FourthString | typeofproperty | typeofproperty|

I just want my object's list to convert to DataTable.

Below is the debugging result as per the solution given below:- enter image description here

@Dennis, Still getting type of property as value for list type property. enter image description here

1 Answers

Your code works fine for T which contains only scalar properties (that is, primitive types, strings, dates, etc).
When it deals with collection or nested object, it tries to store that object or collection in data table's row as is, so the result is not as you expected.

To get the output you want, you need to flatten list items first. Since T could be anything, flatten logic cannot be implemented in generic manner. But the caller can do this for you, since he knows everything about object structure.

Having these types:

class Foo
{
    public string A { get; set; }

    public string B { get; set; }

    public string C { get; set; }

    public string D { get; set; }

    public IList<Bar> Bars { get; set; }

    public IList<string> Strings { get; set; }
}

class Bar
{
    public int A { get; set; }
}

you can write this code:

var items = new List<Foo>
{
    new Foo
    {
        A = "FirstString",
        B = "SecondString",
        C = "ThirdString",
        D = "FourthString",
        Bars = new List<Bar>
        {
            new Bar { A = 100 },
            new Bar { A = 200 }
        },
        Strings = new List<string>
        {
            "10W",
            "20W"
        }
    }
};

var dataTable = items
    .SelectMany(item => item.Bars.Zip(item.Strings, (bar, stringValue) => new
    {
        BarA = bar.A,
        StringValue = stringValue
    }),
    (item, _) => new
    {
        item.A,
        item.B,
        item.C,
        item.D,
        _.BarA,
        _.StringValue
    })
    .ToDataTable();

Result:

enter image description here

As you can see, flatten logic inside SelectMany depends on what T is. If there will be, say, 3 nested lists and Bar will contain public Boo { get; set; } property, that logic will change to reflect new object structure.

P.S. I've modified ToDataTable definition a little to make it extension method for IEnumerable<T>:

static class ConversionExtensions
{
    public static DataTable ToDataTable<T>(this IEnumerable<T> items)
    {
        // your code here
    }
}
Related