vendredi 4 décembre 2020

Flattening Class collections into DataTable C#

I have two classes that look a bit like this:

public class Order
{
    public string Reference { get; set; }
    //some more string, int, date fields excluded for brevity
    public HashSet<OrderLine> OrderLines { get; set; }
}
public class OrderLine
{
    public int LineNum { get; set; }
    public int Qty { get; set; }
    public string ItemName { get; set; }
}

I have been asked to export the a list of orders into an Excel spreadsheet for people to view, at the click of a button in a Windows Application. I have decided that I should do this by creating a datatable with columns names of (in the example above) Reference,LineNum,Qty,ItemName, which I can store in memory and display when requested.

To do this, I decided to write a method to add column names for all the simple properties of a class, and to iterate into the type of item in all collection properties to do the same for each item in that collection, repeating the parent properties for each row. Thus the output will look something like this:

Reference   LineNum    Qty    ItemName               //In this case, there were two Orders, the first
0001        1          12     Foo                    //with order.OrderLines.Count == 2, the second
0001        2          23     Bar                    //with order.OrderLines.Count == 1.
0002        1          34     Foo

I have (relying heavily on other StackOverflow answers!) written this code:

public static class Class_Shredder
{
    public static DataTable Flatten<T>(List<T> masterClass)
    {
        var dt = AddColumnsForProperties(masterClass[0]);
        // some more code here

        return dt;
    }

    public static DataTable AddColumnsForProperties<T>(T classObject)
    {
        DataTable dt = new DataTable(typeof(T).Name);
        AddColumnsForProperties(classObject, dt);
        return dt;
    }
    public static DataTable AddColumnsForProperties<T>(T classObject, DataTable dt)
    {
        foreach (var property in typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance))
        {
            
            if (property.PropertyType.IsImplementationOfGenericType(typeof(ICollection<>)))
            {
                //if we have a collection property, we list the properties of one object in the collection, not the name of the collection itself
                //PROBLEM HERE
                AddColumnsForProperties(property.PropertyType.GetGenericArguments().Single(), dt);
            }
            else
            {
                dt.Columns.Add(property.Name);
            }
        }
        return dt;
    }
}

NB. the GetGenericArguments() and IsImplementationOfGenericType are listed at the bottom of this question.

When I call the Flatten method on a 'List', it starts off by working well, returning the properties of the order, recognising when it hits a collection. The problem is that when it reaches the HashSet<OrderLine> property, it doesn't seem to call AddColumnsForProperties with an OrderLine - the properties it finds are MemberType, DeclaringType and more... I think that means it has received an object, but I'm not sure...

So... what I would like to know is how do I call the method in the line marked with ERROR HERE above with an object of type T, given that it is dealing with a HashSet<T> (or other collection) property type?


Answers that helped me get this far:

getting item type for a generic list

check whether T inherits or implements an interface

How to know if a property is a collection

Exporting an EF Model to Excel (C# Corner)


For Reference:

    public static bool IsGenericTypeOf(this Type type, Type genericTypeDefinition)=> type.IsGenericType && type.GetGenericTypeDefinition() == genericTypeDefinition;

    public static bool IsImplementationOfGenericType(this Type type, Type genericTypeDefinition)
    {
        if (!genericTypeDefinition.IsGenericTypeDefinition)
            return false;

        // looking for generic interface implementations
        if (genericTypeDefinition.IsInterface)
        {
            foreach (Type i in type.GetInterfaces())
            {
                if (i.Name == genericTypeDefinition.Name && i.IsGenericTypeOf(genericTypeDefinition))
                    return true;
            }

            return false;
        }

        // looking for generic [base] types
        for (Type t = type; type != null; type = type.BaseType)
        {
            if (t.Name == genericTypeDefinition.Name && t.IsGenericTypeOf(genericTypeDefinition))
                return true;
        }

        return false;
    }




Aucun commentaire:

Enregistrer un commentaire