Reputation: 6894
I need to execute a partial text search, alongside other filters, via a generic repository using expressions.
I have a generic method that returns paged results from my database (via a common repository layer).
In the following working example;
PagedRequest
contains the current pagesize and page number, and is used during respective Skip
/ Take
operations.PagedResult
contains a collection of the results, along with the total number of records.public Task<PagedResult<Person>> GetPeopleAsync(PersonSearchParams searchParams,
PagedRequest pagedRequest = null)
{
ParameterExpression argParam = Expression.Parameter(typeof(Locum), "locum");
// start with a "true" expression so we have an expression to "AndAlso" with
var alwaysTrue = Expression.Constant(true);
var expr = Expression.Equal(alwaysTrue, alwaysTrue);
if (searchParams != null)
{
BinaryExpression propExpr;
if (searchParams.DateOfBirth.HasValue)
{
propExpr = GetExpression(searchParams.DateStart,
nameof(Incident.IncidentDate),
argParam,
ExpressionType.GreaterThanOrEqual);
expr = Expression.AndAlso(expr, propExpr);
}
if (searchParams.DateOfDeath.HasValue)
{
propExpr = GetExpression(searchParams.DateEnd,
nameof(Incident.IncidentDate),
argParam,
ExpressionType.LessThanOrEqual);
expr = Expression.AndAlso(expr, propExpr);
}
if (searchParams.BranchId.HasValue && searchParams.BranchId.Value != 0)
{
propExpr = GetExpression(searchParams.BranchId,
nameof(Incident.BranchId), argParam);
expr = Expression.AndAlso(expr, propExpr);
}
}
var lambda = Expression.Lambda<Func<Locum, bool>>(expr, argParam);
return _unitOfWork.Repository.GetAsync(filter: lambda, pagedRequest: pagedRequest);
}
This is using my static GetExpression
method for Expression.Equal
, Expression.GreaterThanOrEqual
and Expression.LessThanOrEqual
queries as follows;
private static BinaryExpression GetExpression<TValue>(TValue value,
string propName, ParameterExpression argParam, ExpressionType? exprType = null)
{
BinaryExpression propExpr;
var prop = Expression.Property(argParam, propName);
var valueConst = Expression.Constant(value, typeof(TValue));
switch (exprType)
{
case ExpressionType.GreaterThanOrEqual:
propExpr = Expression.GreaterThanOrEqual(prop, valueConst);
break;
case ExpressionType.LessThanOrEqual:
propExpr = Expression.LessThanOrEqual(prop, valueConst);
break;
case ExpressionType.Equal:
default:// assume equality
propExpr = Expression.Equal(prop, valueConst);
break;
}
return propExpr;
}
NOTE: this code is working correctly.
Using example from other SO answers I have tried the following;
I have tried getting the contains via an Expression
;
static Expression<Func<bool>> GetContainsExpression<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<bool>>(containsMethodExp);
}
This has to be converted to a BinaryExpression
so it can be added to the expression tree using AndAlso
. I've tried to compare the Expression
with a true
value, but this isn't working
if (searchParams.FirstName.IsNotNullOrWhiteSpace())
{
var propExpr = GetContainsExpression<Locum>(nameof(Locum.Firstname),
searchParams.FirstName);
var binExpr = Expression.MakeBinary(ExpressionType.Equal, propExpr, propExpr);
expr = Expression.AndAlso(expr, binExpr);
}
I also tried returning the MethodCallExpression
(instead of the Lambda above), using the following;
static MethodCallExpression GetContainsMethodCallExpression<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 containsMethodExp;
}
I used this as follows;
if (searchParams.FirstName.IsNotNullOrWhiteSpace())
{
var propExpr = GetContainsMethodCallExpression<Person>(nameof(Person.FirstName),
searchParams.FirstName);
var binExpr = Expression.MakeBinary(ExpressionType.Equal, propExpr, alwaysTrue);
expr = Expression.AndAlso(expr, binExpr);
}
These expression are passed to a generic method that pages information out of the database, and the exceptions are thrown during the first execution of the query when I Count the total matching number of record on the constructed query
.
System.InvalidOperationException: 'The LINQ expression 'DbSet() .Where(p => True && p.FirstName.Contains("123") == True)' could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to 'AsEnumerable ', 'AsAsyncEnumerable ', 'ToList ', or 'ToListAsync '. See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.'
This exception is thrown on a Count
method I am using in my paging code. This code is already working without any filters, and with the ExpressionType
filters described at the top, so I haven't included this code as I don't believe it is relevant.
pagedResult.RowCount = query.Count();
Upvotes: 2
Views: 1095
Reputation: 205759
This has to be converted to a BinaryExpression so it can be added to the expression tree using
AndAlso
Negative. There is no requirement Expression.AndAlso
(or Expression.OrElse
) operands to be binary expressions (it would have been strange like requiring left or right operand of &&
or ||
to be always comparison operators). The only requirement is them to be bool
returning expressions, hence call to string Contains
is a perfectly valid operand expression.
So start by changing the type of the inner local variable from BinaryExpression
to Expression
:
if (searchParams != null)
{
Expression propExpr;
// ...
}
The same btw applies for the initial expression - you don't need true == true
, simple
Expression expr = Expression.Constant(true);
would do the same.
Now you could emit method call to string.Contains
in a separate method similar to the other that you've posted (passing the ParameterExpression
and building property selector expression) or inline similar to:
if (searchParams.FirstName.IsNotNullOrWhiteSpace())
{
var propExpr = Expression.Property(argParam, nameof(Person.FirstName));
var valueExpr = Expression.Constant(searchParams.FirstName);
var containsExpr = Expression.Call(
propExpr, nameof(string.Contains), Type.EmptyTypes, valueExpr);
expr = Expression.AndAlso(expr, containsExpr);
}
Upvotes: 2