4
\$\begingroup\$

I'm looking for a function to take a list of generic classes, and map it to different layout of datatable.

That means, I can't just directly map my class property names and values to grid, I need to convert it to the new values.

The solution that I came up with works, but it feels ... sloppy. Can anyone recommend a better way of doing it?

class Program
{
    static void Main(string[] args)
    {
        var list = new List<InputClass>()
        {
            new InputClass {SomeField = "A", OtherField = 1},
            new InputClass {SomeField = "B", OtherField = 2},
            new InputClass {SomeField = "C", OtherField = 3},
            new InputClass {SomeField = "D", OtherField = 4}
        };

        var dict = new Dictionary<string, Func<InputClass, object>>()
        {
            {"ColA", (f) => $"{f.SomeField}ABCD"},
            {"ColB", (f) => DateTime.Now}
        };

        var dt = ToDataTable(list, dict);
        Console.ReadKey();
    }

    public static DataTable ToDataTable<T>(List<T> list, Dictionary<string, Func<T, object>> columns) where T : class 
    {
        var dt = new DataTable();

        foreach (var key in columns.Keys)
        {
            var func = columns[key];
            Type[] funcParams = (func.GetType()).GenericTypeArguments;
            dt.Columns.Add(key, funcParams[1]);
        }

        foreach (var record in list)
        {
            var objects = new List<object>();
            foreach (var key in columns.Keys)
            {
                objects.Add(columns[key](record));
            }

            dt.Rows.Add(objects.ToArray());
        }

        return dt;
    }
}

class InputClass
{
    public string SomeField { get; set; }

    public int OtherField { get; set; }
}
\$\endgroup\$
  • 1
    \$\begingroup\$ Do you have a concrete example of your implementation? It would help a lot for review. \$\endgroup\$ – IEatBagels Jul 24 '18 at 14:29
  • \$\begingroup\$ This does not make any sense: (func.GetType()).GenericTypeArguments. The Func is always returning an object so why don't you just write dt.Columns.Add(key, typeof(object));? \$\endgroup\$ – t3chb0t Jul 24 '18 at 15:04
  • \$\begingroup\$ Here's a blog post with some very nice code to do the same thing. \$\endgroup\$ – tinstaafl Jul 25 '18 at 15:52
2
\$\begingroup\$

Cases like this I would hide the "sloppy"

I would create a class that holds the mappings from the class to the table and create methods to build the mappings. This way there is no need for reflection and we can save the funcs to be reused.

public class Mapper<TSource>
{
    private readonly DataTable _source = new DataTable();
    private readonly List<Func<TSource, object>> _mappings = new List<Func<TSource, object>>();

    public Mapper<TSource> Configure<TProperty, TData>(Func<TSource, TProperty> property, string colName,
        Func<TProperty, TData> mapper)
    {
        _source.Columns.Add(colName, typeof (TData));
        Func<TSource, object> map = s => mapper(property(s));
        _mappings.Add(map);
        return this;
    }

    public Mapper<TSource> Configure<TProperty>(Func<TSource, TProperty> property, string colName)
    {
        _source.Columns.Add(colName, typeof(TProperty));
        Func<TSource, object> map = s => property(s);
        _mappings.Add(map);
        return this;
    }

    public Mapper<TSource> Configure<TData>(string colName, Func<TData> mapper)
    {
        _source.Columns.Add(colName, typeof(TData));
        Func<TSource, object> map = _ => mapper();
        _mappings.Add(map);
        return this;
    }

}

With this class we now have a datatable in _source that holds the columns that we want and a list of mappings from our source to the datatable. Now we just need to create a ToDataTable method to use these in the same class.

public DataTable ToDataTable(IEnumerable<TSource> items)
{
    // make a new datatable with the same columns as the source
    var dt = _source.Clone();

    foreach (var item in items)
    {
        dt.Rows.Add(_mappings.Select(f => f(item)).ToArray());
    }

    return dt;
}

We would use it like so. Typically you would only do the configurations once and then just reuse it with different lists.

var inputClassMapper = new Mapper<InputClass>();
inputClassMapper.Configure(f => f.SomeField, "ColA", s => $"{s}ABCD")
                .Configure("ColB", () => DateTime.Now);

// have mapper convert input to datatables
var dt = inputClassMapper.ToDataTable(list);
\$\endgroup\$
  • 1
    \$\begingroup\$ Too simple and does a lot of boxing - we need expressions ;-) \$\endgroup\$ – t3chb0t Jul 24 '18 at 15:07

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.