Search code examples
c#entity-frameworklinqpredicatebuilder

Concatenate string members using Linq PredicateBuilder for text search


I have a REST WebAPI using EntityFramework database first. All code is generated off the EDMX file, entities, repository classes and API controllers etc.

I have added some filtering functionality which allows users to add conditions via the query string that translate to LinqKit PredicateBuilder / Linq expressions that filter results when hitting the db.

e.g. /api/Users?FirstName_contains=Rog

This will return all users with 'Rog' in the User.FirstName member. This uses PredicateBuilder to dynamically build an appropriate Linq expression to then use as a Where clause against the DbSet.

For example:

var fieldName = "FirstName";
var value = "Rog";

var stringContainsMethod = typeof(string).GetMethod("Contains", new[] { typeof(string) });

var parameter = Expression.Parameter(typeof(User), "m");
var fieldAccess = Expression.PropertyOrField(parameter, fieldName);
var fieldType = typeof(User).GetProperty(fieldName, BindingFlags.IgnoreCase | BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public).PropertyType;

var expression = Expression.Lambda<Func<User, bool>>(Expression.Call(fieldAccess, stringContainsMethod, Expression.Constant(value, fieldType))
    , parameter)

var andPredicate = PredicateBuilder.True<User>();
andPredicate = andPredicate.And(expression);

var query = Db.Users
    .AsQueryable()
    .AsExpandable()
    .Where(andPredicate);

Now the problem. I want the client to be able to match results based on a composition of members.

e.g. /api/Users?api_search[FirstName,LastName]=Rog

i.e. search first name + last name for matches of 'Rog', so I could search for 'Roger Sm' and get a result for first name = Roger and last name = Smith.

If I was to query the DbSet using fluent it would look like:

users.Where(u => (u.FirstName + " " + u.LastName).Contains("Rog"));

What I am struggling with is creating a predicate / linq expression that will handle the concatenation of string members FirstName + " " + LastName dynamically.


Solution

  • PredicateBuilder is not really needed here.

    The string concatenation expression can be generated using string.Concat method call which is supported by EF:

    static Expression<Func<T, string>> GenerateConcat<T>(IEnumerable<string> propertyNames)
    {
        var parameter = Expression.Parameter(typeof(T), "e");
        // string.Concat(params string[] values)
        var separator = Expression.Constant(" ");
        var concatArgs = Expression.NewArrayInit(typeof(string), propertyNames
            .SelectMany(name => new Expression[] { separator, Expression.PropertyOrField(parameter, name) })
            .Skip(1));
        var concatCall = Expression.Call(typeof(string).GetMethod("Concat", new[] { typeof(string[]) }), concatArgs);
        return Expression.Lambda<Func<T, string>>(concatCall, parameter);
    }
    

    The string contains predicate can be generated by simple string.Contains method call:

    static Expression<Func<T, bool>> GenerateContains<T>(Expression<Func<T, string>> member, string value)
    {
        var containsCall = Expression.Call(member.Body, "Contains", Type.EmptyTypes, Expression.Constant(value));
        return Expression.Lambda<Func<T, bool>>(containsCall, member.Parameters);
    }
    

    Combining them together with your example:

    var predicate = GenerateContains(GenerateConcat<User>(new[] { "FirstName", "LastName" }), "Rog");