0

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!

Tim
  • 1
  • 2
  • 1
    What is your expected output? I don't understand what you are trying to do. – Sweeper Jan 13 '21 at 03:08
  • Not sure you actually need `Expression`, you can do it with `CreateDelegate` of the property getter. But what **ARE** you trying to do? Get distinct combos of properties? Have you taken into account `{p1,p2}` - `{p2,p1}`? – Charlieface Jan 13 '21 at 03:16
  • Hi @Charlieface and @Sweeper yeah what I'm doing doesn't seem to make sense but is required in the third-party configuration engine. It's like trying to filter an Excel spreadsheet, two columns at a time - establishing the relationship of each column with all the others, individually. In this scenario {p1, p2} implies {p2, p1}. I'll look at ```CreateDelegate```. Thanks! – Tim Jan 13 '21 at 13:02
  • Happy to write something up for you if you want – Charlieface Jan 13 '21 at 13:22

2 Answers2

0

I hope I understand your problem correctly. This is simple extension which enumerates needed pairs:

var items = data.EnumeratePropPairs().Distinct().ToList();
items.ForEach(x => Console.WriteLine($"{x.Item1}, {x.Item2}"));

And implementation

public static class EnumerableExtensions
{
    public static IEnumerable<Tuple<string, string>> EnumeratePropPairs<T>(this IEnumerable<T> items)
    {
        var properties = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public);
        var param = Expression.Parameter(typeof(T));
        var accessors = properties.ToDictionary(p => p, p =>
        {
            var body = (Expression)Expression.MakeMemberAccess(param, p);
            if (body.Type != typeof(string))
            {
                body = Expression.Call(body, "ToString", Type.EmptyTypes);
            }

            var lambda = Expression.Lambda<Func<T, string>>(body, param);
            return lambda.Compile();
        });

        var pairs = new List<Tuple<Func<T, string>, Func<T, string>>>();

        for (var i = 0; i < properties.Length - 1; i++)
        {
            var prop1 = properties[i];
            var prop1Accessor = accessors[prop1];

            for (var j = i + 1; j < properties.Length; j++)
            {
                var prop2 = properties[j];
                var prop2Accessor = accessors[prop2];

                pairs.Add(Tuple.Create(prop1Accessor, prop2Accessor));
            }
        }

        return items.SelectMany(item => pairs.Select(p => Tuple.Create(p.Item1(item), p.Item2(item))));
    }
}
Svyatoslav Danyliv
  • 21,911
  • 3
  • 16
  • 32
  • I like the use of Tuple over anonymous types, but this answer requires all of the source properties to be string. My problem has a mix of string and decimal, and ultimately could have other simple types like bool, float, etc. I'll see if I can adapt to my needs, and will mark as answer if it works out. Thanks! – Tim Jan 13 '21 at 14:31
  • Actually I have converted other types to string. We can return `object` if it is ok ;) – Svyatoslav Danyliv Jan 13 '21 at 14:52
  • @Tim You can replace the lambda here with `CreateDelegate` which should have faster startup. `p => p.GetMethod.CreateDelegate(typeof(Func), null)` You can also replace `Tuple` with a ValueTuple `(string, string)` etc – Charlieface Jan 13 '21 at 20:51
0

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.

Tim
  • 1
  • 2