0

I am wanting to implement the following method

public static class Filters
{
   public static Expression<Func<T,bool>> ContainsText<T>(
      string text, params Expression<Func<T,string>>[] fields)
   {
       //..
   }
}

so that if I wanted to (for example) find anyone whose name contains "Mark" or whose dad's name contains "Mark", I can do something like this:

var textFilter = Filters.ContainsText<Individual>("Mark", i=>i.FirstName, i=>i.LastName, i=>i.Father.FirstName, i => i.Father.LastName);
var searchResults = _context.Individuals.Where(textFilter).ToList();

My end goal is to be able to create a ContainsTextSpecification to simplify text-based filtering that I can use like so:

var textSpec = new ContainsTextSpecification<Individual>(i=>i.FirstName, i=> i.LastName, i=>i.DepartmentName, i=>i.SSN, i=>i.BadgeNumber);
textSpec.Text = FormValues["filter"];
var results = individuals.Find(textSpec);

I found something that gets me close to what I want here, but I want to be able to specify the fields I want to filter by using a Func<T,string> rather than just the name of the field. (edit: I want to be able to specify the -values- that will be checked, not the name of the fields)

static Expression<Func<T, bool>> GetExpression<T>(string propertyName, string propertyValue)
{
    var parameterExp = Expression.Parameter(typeof(T), "type");
    var propertyExp = Expression.Property(parameterExp, propertyName);
    MethodInfo method = typeof(string).GetMethod("Contains", new[] { typeof(string) });
    var someValue = Expression.Constant(propertyValue, typeof(string));
    var containsMethodExp = Expression.Call(propertyExp, method, someValue);

    return Expression.Lambda<Func<T, bool>>(containsMethodExp, parameterExp);
}
var results = individualRepo.Get(textSpec);
NuclearProgrammer
  • 806
  • 1
  • 9
  • 19
  • And why not just use `static Expression> GetExpression(Func propertyName, string propertyValue)`? – Camilo Terevinto Aug 29 '18 at 18:12
  • EF needs to be able to use the Expression returned by the function to create the SQL query. If I'm building the expression using calls to c# functions (instead of expressions) then EF won't know how to translate the query to SQL. – NuclearProgrammer Aug 29 '18 at 18:29
  • I know that, but nothing prevents you from getting the result of the `Func` before passing it into `Expression.Property` – Camilo Terevinto Aug 29 '18 at 18:32
  • @CamiloTerevinto How is accepting a method that returns the string name of a property any better than accepting the string name of the property? – Servy Aug 29 '18 at 18:37
  • @Servy Well, " I want to be able to specify the fields I want to filter by using a Func rather than just the name of the field." – Camilo Terevinto Aug 29 '18 at 18:41
  • @CamiloTerevinto They want to be able to specify them with an `Expression>` representing the projection to have the datbase use. Using a `Func` to come up with a property name doesn't help that. – Servy Aug 29 '18 at 18:43
  • @CamiloTerevinto As @Servy pointed out, the purpose of the `Expression>` is to return the value that will be compared, not the name of the field. This way if I only wanted to check the last 4 chararacters of a persons SSN I could do `ContainsText("3252", i=>i.SSN.Substring(5) )` this is useful if I don't want the user to be able to see the full SSN but be able to search based on what they can see. – NuclearProgrammer Aug 29 '18 at 21:50

1 Answers1

3

A few generic operations really help in solving most expression related problems. In this case there's two that really help you, being able to compose two expressions together, and computing an expression that Ors together N predicates. If you have those two operations your implementation becomes as simple as:

public static Expression<Func<T, bool>> ContainsText<T>(
    string text, params Expression<Func<T, string>>[] fields)
{
    var predicates = fields.Select(projection => projection.Compose(value => value.Contains(text)));            
    return OrAll(predicates);
}

To compose two expressions together it's helpful to create a method that replaces all instances of one parameter expression with another:

public static Expression<Func<TSource, TResult>> Compose<TSource, TIntermediate, TResult>(
    this Expression<Func<TSource, TIntermediate>> first,
    Expression<Func<TIntermediate, TResult>> second)
{
    var param = Expression.Parameter(typeof(TSource));
    var intermediateValue = first.Body.ReplaceParameter(first.Parameters[0], param);
    var body = second.Body.ReplaceParameter(second.Parameters[0], intermediateValue);
    return Expression.Lambda<Func<TSource, TResult>>(body, param);
}
public static Expression ReplaceParameter(this Expression expression,
    ParameterExpression toReplace,
    Expression newExpression)
{
    return new ParameterReplaceVisitor(toReplace, newExpression)
        .Visit(expression);
}
public class ParameterReplaceVisitor : ExpressionVisitor
{
    private ParameterExpression from;
    private Expression to;
    public ParameterReplaceVisitor(ParameterExpression from, Expression to)
    {
        this.from = from;
        this.to = to;
    }
    protected override Expression VisitParameter(ParameterExpression node)
    {
        return node == from ? to : base.Visit(node);
    }
}

To Or N predicates together is a very similar process, just done for more than two values at once:

public static Expression<Func<T, bool>> OrAll<T>(IEnumerable<Expression<Func<T, bool>>> predicates)
{
    var parameter = Expression.Parameter(typeof(T));
    var newBody = predicates.Select(predicate => predicate.Body.ReplaceParameter(predicate.Parameters[0], parameter))
        .DefaultIfEmpty(Expression.Constant(false))
        .Aggregate((a, b) => Expression.OrElse(a, b));
    return Expression.Lambda<Func<T, bool>>(newBody, parameter);
}
Servy
  • 202,030
  • 26
  • 332
  • 449