How can I put together a predicate function when I have to choose between several options

2

I have a class

public class TestClass
{
    public int? a { get; set; }
    public int? b { get; set; }

    public TestClass(int _a, int _b)
    {
        a = _a;
        b = _b;
    }
}

And for this class, I generate a list with elements of it.

List<TestClass> lista = new List<TestClass>
{
    new TestClass(1,2),
    new TestClass(2,3),
    new TestClass(1,3)
};

Now, I would like to generate a method that according to the options that the user selects, allows me to return or all the elements of the list (if I do not select anything), or those that comply with what the user selected.

If I have that class with only those two properties I could do something like this:

List<TestClass> resul;
if (seleccionA == null && seleccionB == null)
{
    resul = lista;
}
if (seleccionA == null && seleccionB != null)
{
    resul = lista.Where(v => seleccionB == v.b).ToList();
}
if (seleccionA != null && seleccionB == null)
{
    resul = lista.Where(v => seleccionA == v.a).ToList();
}
if (seleccionA != null && seleccionB != null)
{
    resul = lista.Where(v => seleccionA == v.a && seleccionB == v.b).ToList();
}

But if they were more than two properties, the if is getting worse. How can I solve this in a more efficient way to write in the code?

    
asked by gbianchi 19.07.2018 в 19:02
source

2 answers

7

For these cases we can make use of the anonymous functions and build a predicate that changes depending on the selected options.

For this, we will define the following:

Func<TestClass, bool> predicate = null;

void ArmarPredicado(Func<TestClass, bool> newPredicate)
{
    if (predicate == null)
    {
        predicate = newPredicate;
        return;
    }
    var oldPredicate = predicate;
    predicate = s => oldPredicate(s) && newPredicate(s);
}

And now, for each value that we want to add (or not) to the predicate, we're going to do the following:

if (valora != null)
{
    ArmarPredicado(v => (valora == v.a));
}

And we are going to write a function to analyze the predicate and return the necessary list:

List<TestClass> DevolverLista(List<TestClass> lista, Func<TestClass, bool> predicate)
{
    if (predicate == null)
    {
        return lista;
    }
    return lista.Where(predicate).ToList();
}

In this way, if we have to add more conditions, we just have to add it to the predicate we are building.

ArmarPredicado is an anonymous function, which returns exactly what the where needs, which is a condition and the value that will return it.

When we pass v => (valora == v.a) , it transforms that into another anonymous function, and it concatenating those functions.

As the definition of where

says
Where<TSource>(IEnumerable<TSource>, Func<TSource, Boolean>)

it receives a Func <, >, which is exactly what we declare as predicate. and the other thing we did, was to concatenate them according to need.

    
answered by 19.07.2018 / 19:12
source
1

here is my answer using expressions:

First we import the following namespaces:

using System;
using System.Collections.Generic;
using System.Linq.Expressions;

Then, I also recommend creating an alias for this type:

using Exp = Tuple<object, Expression<Func<TestClass, object>>>;

And the ArmarPredicado method would like this:

Func<TestClass, bool> ArmarPredicado(params Exp[] exp)
{
    Func<TestClass, bool> result = null;
    foreach(var item in exp) {
        if (item.Item1 != null) {
            var compiled = item.Item2.Compile();
            Func<TestClass, bool> oldPredicate = result;
            Func<TestClass, bool> newPredicate = s => item.Item1.Equals(compiled.Invoke(s));
            if (oldPredicate == null) {
                result = newPredicate;
            }
            else {
                result = s => newPredicate(s) && oldPredicate(s);
            }
        }
    }
    return result ?? (x => true);
}

And you use it like this:

var filtro = ArmarPredicado(
    new Exp(valora, x => x.a),
    new Exp(valorb, x => x.b));

List<TestClass> resul = lista.Where(filtro).ToList();

If you value and valueb are null, it returns the entire source list, and if not, create the filter with those that are not null.

In this way you do not have to crar the predicate with if's, a single line creates you the predicate, you only pass the pair of objects that it is, the one that contains the value that you are going to filter, and the expression that indicates with which property the object will be compared.

And since you pass an array with params format, you can add all the "filter - > property" pairs that you require.

With C # 7 and the ValueTuple type is even better.

EDIT: The only bad thing is that it is only for equality, that is, it only verifies if they are equal, a more structured filter is not valid, although that can be solved.

EDIT: An idea so that it can be created with any expression, not just equality:

var predicate = new PredicateBuilder()
    .Add(valora, x => x.a, (x, y) => x == y)
    .Add(valorb, x => x.b, (x, y) => x > y)
    .Build();

List<TestClass> result = predicate == null 
    ? lista 
    : lista.Where(predicate).ToList();

If you are interested, I can create the class and edit it and put it on.

    
answered by 21.07.2018 в 21:38