1

I am working on a "ToDataTable()" that I can use on IEnumerable. Lots of examples of that, such as here: Convert IEnumerable to DataTable. DataTables are desirable for me because I am coming from a Foxpro realm where we are used to cursors. So, I dig DataTables because they are relatively simple, easy to work with, and can house decent column meta-data. And I've already got a bunch of code that can display them nicely (with sort and filter grid headers), export to Excel, etc.

Someone made a really good comment on the thread I linked to above: "This doesn't work if the type is string as the properties are chars and length but the get value then tries to put the full string into the char column."

Problem is, certain things fit the extension method call (like a one-dimensional array of strings) but then throw errors when the conversion takes place. I wrote an extension method that adds IsValueType() to the mix, but the problem is that returns false for strings. Finally, I added two kludges: a parameter to force the item in the sequence to be treated as a value, and to treat the item in the sequence as a value if it is found to be of type "String".

Is there a better way of doing this, or are strings just an odd man out due to their unique breed of typing and reflection results? The following code works with every IEnumerable I can think of, and it works with one-dimensional arrays for all DataTable column types except for string (well, it works for strings with the hard-coded kludge, but would error out otherwise). It's not a big deal to hard-code the string scenario, I just wish there was a more elegant way to do this. Any thoughts?

    public static DataTable ToDataTable<T>(this IEnumerable<T> items, string tableName = "", bool treatItemAsValue = false)
    {
        // We want a single extension method that can take in an enumerable sequence (such as a LINQ query)
        // and return the result as a DataTable. We want this to be a one stop shop for converting
        // various objects into DataTable format, as DataTables are a nice parallel to Foxpro cursors.
        if (items == null) { return null; }
        Type itemType = typeof(T);
        bool typeIsNullable = itemType.IsGenericType && typeof(T).GetGenericTypeDefinition().Equals(typeof(Nullable<>));
        string itemTypeName = "";
        bool typeIsValue = false;
        Type itemUnderlyingType = itemType;
        if (typeIsNullable)
        {
            // Type of enumerable item is nullable, so we need to find its base type.
            itemUnderlyingType = Nullable.GetUnderlyingType(itemType);
        }
        typeIsValue = itemUnderlyingType.IsValueType;
        itemTypeName = itemUnderlyingType.Name;
        DataTable dt = new DataTable();
        DataColumn col = null;
        if ((treatItemAsValue) || (itemTypeName == "String"))
        {
            // We have been asked to treat the item in the sequence as a value, of the items
            // in the sequence are strings. Strings are NOT considered a value type in regards
            // to IsValueType(), but when item values are assessed, it will be the value
            // of the string that tries to pull in.
            typeIsValue = true;
        }
        if (itemTypeName == "DataRow")
        {
            // Special case. If our enumerable type is DataRow, then we can utilize a more appropriate
            // (built-in) extension method to convert enumerable DataRows to a DataTable.
            dt = ((IEnumerable<DataRow>)items).CopyToDataTable();
        }
        else
        {
            // We must have an enumerable sequence/collection of some other type, possibly anonymous.
            // Get properties of the enumerable to add as columns to the data table.
            if (typeIsValue)
            {
                // Our enumerable items are of a value type (e.g. integers in a one-dimensional array).
                col = dt.Columns.Add();
                col.AllowDBNull = typeIsNullable;
                col.ColumnName = itemTypeName;
                col.DataType = itemUnderlyingType;
                // Now walk through the enumeration and add rows to our data table (single values).
                foreach (var item in items)
                {
                    dt.Rows.Add(item);
                }
            }
            else
            {
                // The type should be something we can walk through the properties of in order to
                // generate properly named and typed columns of our DataTable.
                PropertyInfo[] props = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
                foreach (var prop in props)
                {
                    Type propType = prop.PropertyType;
                    // Is it a nullable type? Get the underlying type.
                    if (propType.IsGenericType && propType.GetGenericTypeDefinition().Equals(typeof(Nullable<>)))
                    {
                        propType = new NullableConverter(propType).UnderlyingType;
                    }
                    dt.Columns.Add(prop.Name, propType);
                }
                // Now walk through the enumeration and add rows to our data table.
                foreach (var item in items)
                {
                    var values = new object[props.Length];
                    for (int i = 0; i < props.Length; i++)
                    {
                        values[i] = props[i].GetValue(item, null);
                    }
                    dt.Rows.Add(values);
                }
            }
        }
        // Give the DataTable a reasonable name.
        if (tableName.Length == 0)
        {
            if (typeof(T).IsAnonymous())
            {
                // Anonymous types have really goofy names, so there is no use using that as table name.
                tableName = "Anonymous";
            }
            else
            {
                // This is NOT an anonymous type, so we can use the type name as table name.
                tableName = typeof(T).Name;
            }
        }
        return dt;
    }
Community
  • 1
  • 1
sutekh137
  • 681
  • 1
  • 6
  • 18
  • Why are you writing one yourself when, as you mentioned, there are lots of existing implementations available? – Servy Mar 09 '16 at 19:59
  • Because they are incomplete. For example, if you pass a one-dimensional array of strings to most implementations out there, it will error out. That's why I am asking. I want to write a fully comprehensive ToDataTable() that can handle anything that gets thrown its way. So, I modified some existing solutions to get closer to that goal. I'm just asking if there is a better way to handle the string case that I might be missing. (And I would always have to write one myself, even if by "write" it means to cut and paste -- there is no "built-in" ToDataTable() method that works across the board.) – sutekh137 Mar 09 '16 at 23:04

1 Answers1

1

Well, no responses except a comment that pretty much missed the point, so I'll just post what I ended up with. This final version also accounts for items with no properties (such as "Object" itself), and does some better handing of null items/values:

    public static DataTable ToDataTable<T>(this IEnumerable<T> items, string tableName = "", bool treatItemAsValue = false)
    {
        // We want a single extension method that can take in an enumerable sequence (such as a LINQ query)
        // and return the result as a DataTable. We want this to be a one stop shop for converting
        // various objects into DataTable format, as DataTables are a nice parallel to Foxpro cursors.
        if (items == null) { return null; }
        Type itemType = typeof(T);
        bool typeIsNullable = itemType.IsGenericType && typeof(T).GetGenericTypeDefinition().Equals(typeof(Nullable<>));
        string itemTypeName = "";
        bool typeIsValue = false;
        Type itemUnderlyingType = itemType;
        if (typeIsNullable)
        {
            // Type of enumerable item is nullable, so we need to find its base type.
            itemUnderlyingType = Nullable.GetUnderlyingType(itemType);
        }
        typeIsValue = itemUnderlyingType.IsValueType;
        itemTypeName = itemUnderlyingType.Name;
        DataTable dt = new DataTable();
        DataColumn col = null;
        PropertyInfo[] props = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
        if ((treatItemAsValue) || (itemTypeName == "String") || (props.Length == 0))
        {
            // We have been asked to treat the item in the sequence as a value, or the items
            // in the sequence is a string which cannot be "flattened" properly by analyzing properties.
            // OR, the type has no properties to put on display, so we should just use the item directly.
            // (like the base "Object" type).
            typeIsValue = true;
        }
        if (itemTypeName == "DataRow")
        {
            // Special case. If our enumerable type is DataRow, then we can utilize a more appropriate
            // (built-in) extension method to convert enumerable DataRows to a DataTable.
            dt = ((IEnumerable<DataRow>)items).CopyToDataTable();
        }
        else
        {
            // We must have an enumerable sequence/collection of some other type, possibly anonymous.
            // Get properties of the enumerable to add as columns to the data table.
            if (typeIsValue)
            {
                // Our enumerable items are of a value type (e.g. integers in a one-dimensional array).
                col = dt.Columns.Add();
                // Whether or not the type is nullable, the value might be null (e.g. for type "Object").
                col.AllowDBNull = true;
                col.ColumnName = itemTypeName;
                col.DataType = itemUnderlyingType;
                // Now walk through the enumeration and add rows to our data table (single values).
                foreach (var item in items)
                {
                    dt.Rows.Add(item);
                }
            }
            else
            {
                // The type should be something we can walk through the properties of in order to
                // generate properly named and typed columns of our DataTable.
                foreach (var prop in props)
                {
                    Type propType = prop.PropertyType;
                    // Is it a nullable type? Get the underlying type.
                    if (propType.IsGenericType && propType.GetGenericTypeDefinition().Equals(typeof(Nullable<>)))
                    {
                        propType = new NullableConverter(propType).UnderlyingType;
                    }
                    dt.Columns.Add(prop.Name, propType);
                }
                // Now walk through the enumeration and add rows to our data table.
                foreach (var item in items)
                {
                    if (item != null)
                    {
                        // Can only add an item as a row if it is not null.
                        var values = new object[props.Length];
                        for (int i = 0; i < props.Length; i++)
                        {
                            values[i] = props[i].GetValue(item, null);
                        }
                        dt.Rows.Add(values);
                    }
                }
            }
        }
        // Give the DataTable a reasonable name.
        if (tableName.Length == 0)
        {
            if (typeof(T).IsAnonymous())
            {
                // Anonymous types have really goofy names, so there is no use using that as table name.
                tableName = "Anonymous";
            }
            else
            {
                // This is NOT an anonymous type, so we can use the type name as table name.
                tableName = typeof(T).Name;
            }
        }
        return dt;
    }

Hope someone finds this useful...

sutekh137
  • 681
  • 1
  • 6
  • 18