Search code examples
c#linqreflectionexpression-trees

Dynamic lambda for IEnumerable.Select


I need to break up a large table into a series of 2-column tables to dynamically create table rules for a configurator engine. This code demonstrates the problem:

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

namespace Spike
{
    class Program
    {
        static void Main(string[] args)
        {
            // The actual data I need to break down has ~20 properties of type string and decimal, over 18,000 rows
            var data = new List<MyData>()
            {
                new MyData("one", "two", 3m, "four"),
                new MyData("five", "six", 7m, "eight"),
                new MyData("nine", "ten", 11m, "twelve"),
                new MyData("thirteen", "fourteen", 15m, "sixteen"),
                new MyData("one", "five", 9m, "thirteen"),
                new MyData("two", "six", 10m, "fourteen"),
                new MyData("three", "seven", 11m, "fifteen"),
                new MyData("four", "eight", 12m, "sixteen")
            };

            // This shows the desired combinations of properties
            // The actual data will have ~230 combinations
            var properties = typeof(MyData).GetProperties(BindingFlags.Instance | BindingFlags.Public);
            for (var i = 0; i < properties.Length - 1; i++)
            {
                for (var j = i + 1; j < properties.Length; j++)
                {
                    Console.WriteLine($"{properties[i].Name} <=> {properties[j].Name}");
                }
            }
            /* output:
                P1 <=> P2
                P1 <=> P3
                P1 <=> P4
                P2 <=> P3
                P2 <=> P4
                P3 <=> P4
            */

            // This shows how I want one combination to appear
            // The challenge seems to be the creation of a dynamic lambda in the Select method.
            var items = data.Select(x => new { x.P2, x.P3 }).Distinct().ToList();
            Console.WriteLine();
            items.ForEach(x => Console.WriteLine($"{x.P2}, {x.P3}"));
            /* output:
                two, 3
                six, 7
                ten, 11
                fourteen, 15
                five, 9
                six, 10
                seven, 11
                eight, 12
            */

            Console.ReadKey();
        }
    }

    public class MyData
    {
        public string P1 { get; set; }
        public string P2 { get; set; }
        public decimal P3 { get; set; }
        public string P4 { get; set; }

        public MyData(string p1, string p2, decimal p3, string p4)
        {
            P1 = p1;
            P2 = p2;
            P3 = p3;
            P4 = p4;
        }
    }
}

I've researched Linq, Reflection, and Expression Trees and can't seem to get past the hurdle of dynamically building this expression:

var items = data.Select(x => new { x.P2, x.P3 }).Distinct().ToList();

where x.P2 and x.P3 are dynamic.

This post seems to be headed in the right direction, but I'm not getting the result to work.

Suggestions? Thanks in advance!


Solution

  • As luck would have it, I stumbled across the answer with this Fiddle, which uses the NuGet package LatticeUtils.Core. This snippet illustrates the result:

                var properties = typeof(MyData).GetProperties(BindingFlags.Instance | BindingFlags.Public);
                for (var i = 0; i < properties.Length - 1; i++)
                {
                    for (var j = i + 1; j < properties.Length; j++)
                    {
                        var subTable = data.SelectDynamic(new[] { properties[i].Name, properties[j].Name }).Distinct();
                        Console.WriteLine($"{properties[i].Name} <=> {properties[j].Name}: {subTable.Count()}");
                    }
                }
    

    With a source dataset of 18,753 rows and 21 columns, the output is

    P1 <=> P2: 26
    ...
    P2 <=> P3: 18
    ... and so forth
    

    This allows me to programmatically create the 210 two-column tables that the target system will accept, that is impractical for a human to enter using the vendor's UI.