Filter list based on KeyValuePairs

2019-06-08 22:35发布

问题:

I have a list that i would like to filter based on a List of KeyValuePairs. All the keys in the KeyValuePair exist in the object.

So let's say i have a list of objects from this class:

public class filters
{
    public string Name { get; set; }
    public string Age { get; set; }
    public string Country { get; set; }
}

And I have a KeyValuePair with:

Key: "Name", Value: "test"
Key: "Country", Value: "SE"

Is it possible to generate some kind of LINQ predicate from the KeyValuePair that is usable as list.Where(predicate), and the predicate would be the same as if I would have written list.Where(c => c.Name == "test" && c.Country == "SE") ?

Or how should I approach this?

回答1:

As a one-liner:

var filters = new Dictionary<string, string>{{"Name", "test"}, {"Country", "SE"}};
var result = list.Where(item => filters.All(f => (string)(item.GetType().GetProperty(f.Key)?.GetValue(item)) == f.Value));

This enables you to have an unlimited number of filters.

For each item in your list the All predicate will check the validity for each filter. item.GetType() gets the Type (ie information about your class) for your item. GetProperty(f.Key) gets information for the specific property, named by the Key of the current filter f. GetValue(item) gets the value of the property for the current item. The ? is a new feature of c# 6, ie it's an inline check for null, ie if the property is not found, it does not try to execute GetValue -- which would raise a NullReferenceException -- but returns null. You then have to cast the property value to string and compare it to the Value of the current filter. You can also use String::Compare (or any other comparison method you prefer).

All only returns true if all filters are met and false otherwise. So the result of this query will contain all elements which meet all the filters in your Dictionary



回答2:

Something like this? Getting the Propertyname through reflection and do an equality check.

Func<filters, IEnumerable<KeyValuePair<string, string>>, bool> filter = (filters, pairs) =>
{
    foreach (var valuePair in pairs)
    {
        if (filters.GetType().GetProperty(valuePair.Key).GetValue(filters) != valuePair.Value)
        {
            return false;
        }
    }
    return true;
};

List<filters> list = new List<filters>();
list.Add(new filters() { Name = "Name1", Country = "DE"});
list.Add(new filters() { Name = "Name2", Country = "SE"});

var element = list.FirstOrDefault(x => filter(x, new List<KeyValuePair<string, string>>() {
    new KeyValuePair<string, string>("Name", "Name2"),
    new KeyValuePair<string, string>("Country", "SE"),
}));

Console.WriteLine(element.Name);


回答3:

I might have mis understood you here but does this do what you want:

// say I have a list of ilters like this 
// assume there are actually some filters in here though
var filterCollection = new List<filters>() 

// build dictionary of key values
var keyedSet = filterCollection.ToDictionary(i => i.Name + i.Country, i => i);

// query them using key ...
var filterItem = keyedSet["testSE"];

... or you can wrap the predicate in an extension method ...

public IEnumerable<filters> ByNameAndCountry(this IEnumerable<filters> collection, string name, string country)
{
     return collection.Where(i => i.Name == name && i => i.Country == country);
}

... having done that you can filter the original list like this ...

var result = filterCollection.ByNameAndCountry("test", "ES");


回答4:

This should do the trick:

void Main()
{
    List<Filter> filters = new List<Filter>() {
        new Filter {Name = "Filter1", Age = 1, Country ="De"},
        new Filter {Name = "Filter2", Age = 2, Country ="Fr"},
        new Filter {Name = "Filter3", Age = 3, Country ="It"},
        new Filter {Name = "Filter4", Age = 4, Country ="Es"},
    };

    KeyValuePair<string, string> kvp = new KeyValuePair<string, string>("Filter1", "De");

    var result = filters.AsQueryable().Where (GetPredicate(kvp));
    result.Dump();
}
//Create the predicate as an expression, which takes a Filter as input and a kvp as a parameter
private static Expression<Func<Filter, bool>> GetPredicate(KeyValuePair<string,string> kvp)
{
        return (f) => f.Name == kvp.Key && f.Country == kvp.Value;
}

Result:



回答5:

You want a Predicate<filters> generated from KeyValuePair, which is IEnumerable<KeyValuePair<string, string>>. So it's

Func<IEnumerable<KeyValuePair<string, string>>, Predicate<filters>>

Use reflection to enumerate All properties listed in KeyValuePair.Key and check if each property value match KeyValuePair.Value. Full code is like

var lists = new List<filters> { new filters { Name = "abc", Country = "def" } };

Func<IEnumerable<KeyValuePair<string, string>>, Predicate<filters>> predicateBuilder =
( keyValueParis ) => filter => ( from kp in keyValueParis
                                 let pi = typeof( filters ).GetProperty( kp.Key )
                                 select pi.GetValue( filter ) == kp.Value )
                              .All( r => r );

var predicates = new List<KeyValuePair<string, string>>
{
    new KeyValuePair<string, string>("Name", "abc" ),
    new KeyValuePair<string, string>("Country", "def")
};
Predicate<filters> predicate = predicateBuilder( predicates );

Console.WriteLine( lists.FindAll(predicate).Count);


回答6:

You could use an approach like this:

void Main()
{
    var keyValuePairs = new List<KeyValuePair>
    {
        new KeyValuePair {Key = "Name", Value = "Test"},
        new KeyValuePair {Key = "Age", Value = "42"},
        new KeyValuePair {Key = "Country", Value = "USA"}
    };

    var list = new List<Filter>();
    list.Add(new Filter { Name = "Test", Age = "42", Country = "USA" });

    list.Where(keyValuePairs.ToPredicate<Filter>()).Dump();
}

public class Filter
{
    public string Name { get; set; }
    public string Age { get; set; }
    public string Country { get; set; }
}

public class KeyValuePair
{
    public string Key { get; set; }

    public string Value { get; set; }
}

public static class KeyValuePairExtensions
{
    public static Func<T, bool> ToPredicate<T>(this IEnumerable<KeyValuePair> keyValuePairs)
    {
        if (keyValuePairs == null || !keyValuePairs.Any())
            return t => false; // default value in case the enumerable is empty

        var parameter = Expression.Parameter(typeof(T));

        var equalExpressions = new List<BinaryExpression>();

        foreach (var keyValuePair in keyValuePairs)
        {
            var propertyInfo = typeof(T).GetProperty(keyValuePair.Key);

            var property = Expression.Property(parameter, propertyInfo);

            var value = Expression.Constant(keyValuePair.Value, propertyInfo.PropertyType);

            var equalExpression = Expression.Equal(property, value);
            equalExpressions.Add(equalExpression);
        }

        var expression = equalExpressions.First();

        if (equalExpressions.Count > 1)
        {
            // combine expression with and
            expression = Expression.AndAlso(equalExpressions[0], equalExpressions[1]);

            for (var i = 2; i < equalExpressions.Count; i++)
            {
                expression = Expression.AndAlso(expression, equalExpressions[i]);
            }
        }

        var lambda = (Func<T, bool>)Expression.Lambda(expression, parameter).Compile();
        return lambda;
    }
}

You could also extend the ToPredicate method to combine the KeyValuePairs with something else than and.



标签: c# lambda