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.
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");