Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to stay DRY whilst using LINQ to Entities and helper methods?

Lets say that I have a particular way of deciding whether some strings "match", like this:

public bool stringsMatch(string searchFor, string searchIn)
{
  if (string.IsNullOrEmpty(searchFor))
  {
    return true;
  }

  return searchIn != null &&
    (searchIn.Trim().ToLower().StartsWith(searchFor.Trim().ToLower()) ||
     searchIn.Contains(" " + searchFor));
}

I would like to pull matches out of a database using Linq To Entities and this helper. However, when I try this:

IQueryable<Blah> blahs = query.Where(b => stringsMatch(searchText, b.Name);

I get "LINQ to Entities does not recognize the method..."

If I re-write the code as:

IQueryable<Blah> blahs = query.Where(b =>
      string.IsNullOrEmpty(searchText) ||
      (b.Name != null &&
        (b.Name.Trim().ToLower().StartsWith(searchText.Trim().ToLower()) ||
         b.Name.Contains(" " + searchText)));

Which is logically equivalent, then things work fine. The problem is that the code isn't as readable, and I have to re-write it for each different entity I want to match.

As far as I can tell from questions like this one, what I want to do is impossible at the moment, but I'm hoping that I'm missing something, am I?

like image 253
Dan Avatar asked Aug 15 '11 14:08

Dan


1 Answers

If all the 'blahs' (classes) that you will be filtering have the same structure, you can use a simple method like this. The main difference is that it returns an Expression that Linq should be able to parse and it brings in the whole instance and filters on Name instead of bringing in just the string name.

    public static Expression<Func<T, bool>> BuildStringMatch<T>(string searchFor) where T : IHasName
    {
        return b =>
              string.IsNullOrEmpty(searchFor) ||
              (b.Name != null &&
                (b.Name.Trim().ToLower().StartsWith(searchFor.Trim().ToLower()) ||
                 b.Name.Contains(" " + searchFor)));
    }

You can use that method like this:

    IQueryable<Blah> blahs = query.Where(BuildStringMatch<Blah>(searchText));

That assumes all your classes that you'd want to filter on implement some interface such as:

    public interface IHasName
    {
        string Name { get; }
    }

If you want to be filtering on different properties, I don't think that's something you can do with simple code like this. I believe you'll need to build the Expression yourself with reflection (or with the help of a library that uses reflection) - it's still possible but much more difficult.

Edit: It sounds like you need dynamic behavior, so I borrowed some logic from dtb's answer to this question and came up with this:

public static Expression<Func<T, bool>> BuildStringMatch<T>(Expression<Func<T, string>> property, string searchFor)
{
    var searchForExpression = Expression.Constant(searchFor, typeof(string));
    return
        Expression.Lambda<Func<T, bool>>(
            Expression.OrElse(
                Expression.Call(typeof(string), "IsNullOrEmpty", null, searchForExpression),
                Expression.AndAlso(
                    Expression.NotEqual(property.Body, Expression.Constant(null, typeof(string))),
                    Expression.OrElse(
                        Expression.Call(Expression.Call(Expression.Call(property.Body, "Trim", null), "ToLower", null), "StartsWith", null,
                            Expression.Call(Expression.Call(searchForExpression, "Trim", null), "ToLower", null)),
                        Expression.Call(property.Body, "Contains", null, Expression.Call(typeof(string), "Concat", null, Expression.Constant(" "), searchForExpression))
                    )
                )
            ),
            property.Parameters
        );
}

You would use it like:

        IQueryable<Blah> blahs2 = query.Where(BuildStringMatch<Blah>(b => b.Name, searchText));

It's long and verbose but you can see how it's similiar to the original method written in straight C# code. Note: I didn't test this code, so there could be a few small problems - but that is the general idea.

like image 104
Stephen McDaniel Avatar answered Sep 21 '22 17:09

Stephen McDaniel