Search code examples
c#linqgenericsintersectiequalitycomparer

Intersect two generic lists by dynamic properties


i have two generic lists with a few properties to compare but i want that the key identifiers are dynamic by a List<string>.

So lets say we have the class:

class A
{
    string Name { get; set; }
    string Color1 { get; set; }
    string Color2 { get; set; }
    string Length { get; set; }
}

The user now can select from an user interface which properties of two lists of those objects need to overlap so that a correct pair is selected. This is stored in a List<string>. As example, if the list string contains "Name" and "Color1" there will be only objects returned where "Name" and "Color1" are overlapping.

I was trying to write a function, but unfortunately i'm not sure which collection i should cast the generic lists to and how do i apply the names of the properties on those? If the name of the "identificators" were always the same, it wouldn't be a problem with Linq/Lambda ;)

Thanks in advance


Solution

  • You need to use reflection for this. This works:

    public class A
    {
        public string Name { get; set; }
        public string Color1 { get; set; }
        public string Color2 { get; set; }
        public string Length { get; set; }
    
        public static IEnumerable<A> Intersecting(IEnumerable<A> input, List<string> propertyNames)
        { 
            if(input == null)
                throw new ArgumentNullException("input must not be null ", "input");
            if (!input.Any() || propertyNames.Count <= 1)
                return input;
    
            var properties = typeof(A).GetProperties();
            var validNames = properties.Select(p => p.Name);
            if (propertyNames.Except(validNames, StringComparer.InvariantCultureIgnoreCase).Any())
                throw new ArgumentException("All properties must be one of these: " + string.Join(",", validNames), "propertyNames");
    
            var props = from prop in properties
                        join name in validNames.Intersect(propertyNames, StringComparer.InvariantCultureIgnoreCase)
                        on prop.Name equals name
                        select prop;
            var allIntersecting = input
                .Select(a => new { 
                    Object = a,
                    FirstVal = props.First().GetValue(a, null),
                    Rest = props.Skip(1).Select(p => p.GetValue(a, null)),
                })
                .Select(x => new { 
                    x.Object, x.FirstVal, x.Rest,
                    UniqueValues = new HashSet<object>{ x.FirstVal }
                })
                .Where(x => x.Rest.All(v => !x.UniqueValues.Add(v)))
                .Select(x => x.Object);
            return allIntersecting;
        }
    }
    

    Sample data:

    var aList = new List<A> { 
        new A { Color1 = "Red", Length = "2", Name = "Red" }, new A { Color1 = "Blue", Length = "2", Name = "Blue" },
        new A { Color1 = "Red", Length = "2", Name = "A3" }, new A { Color1 = "Blue", Length = "2", Name = "A3" },
        new A { Color1 = "Red", Length = "3", Name = "Red" }, new A { Color1 = "Blue", Length = "2", Name = "A6" },
    };
    var intersecting = A.Intersecting(aList, new List<string> { "Color1", "Name" }).ToList();