Applying an Expression<> to a LINQ Where clause - c#

I'm writing a custom Entity Framework filter. I have a list of IDs, and a user-supplied expression.
protected IEnumerable<TColumn> FilterIds;
protected Expression<Func<T, IEnumerable<TColumn>, bool>> Filter;
Now my question is how do I apply the Filter expression to a Where() clause?
public virtual IQueryable<T> ApplyFilter(IQueryable<T> query)
{
if (FilterMode == FilterModeMatchAny && HasFilterIds)
{
// Whoops! Can't do this!
return query.Where(x => Filter(x, FilterIds));
}
return query;

Using the Combine method from this post does pretty much everything you need. From there you just need to turn the literal value into an expression that computes it (or I guess alter the Combine method from that answer so that the intermediate value isn't computed from a lambda but rather is just any expression), and then call the function.
protected IEnumerable<TColumn> FilterIds;
protected Expression<Func<T, IEnumerable<TColumn>> FilterIdsExpression => _ => FilterIds;
protected Expression<Func<T, IEnumerable<TColumn>, bool>> Filter;
public virtual IQueryable<T> ApplyFilter(IQueryable<T> query)
{
if (FilterMode == FilterModeMatchAny && HasFilterIds)
{
return query.Where(FilterIdsExpression.Combine(Filter));
}
return query;
}

You can create a new expression to invoke the Filter using the FilterIds and use Expand from LinqKit
Edit Thanks to #Servy's comment. Now I expand only the internal expression instead of entire query.
...
protected IEnumerable<TColumn> FilterIds;
protected Expression<Func<T, IEnumerable<TColumn>, bool>> Filter;
public virtual IQueryable<T> ApplyFilter(IQueryable<T> query)
{
if (FilterMode == FilterModeMatchAny && HasFilterIds)
{
Expression<Func<T, bool>> expression = x => Filter.Invoke(x, FilterIds);
return query.Where(expression.Expand());
}
return query;
}
...

Related

EF.Functions.Like method not working after upgrading to .NET 6

I was using EF.Functions.Like method for my query on my ASP.NET Core 3.1 SPI but as soon as I upgraded it to .NET 6 along with with all nugets, this particular query started to throw following error
InvalidOperationException : The Like method is not supported because the query has switched to client-evaluation
I have done some research and I know InMemory databases can cause this issue, but thats not the case I've tried localhosting the API and also published SPI and in both cases the database is always on SQL Server (not local on my machine) so its not an inmemory database, also the fact it worked fine on asp.net core 3.1
Example query
Query Method
public static async Task<IQueryable<Evidence>> QueryEvidencesAsync(this EvidenceContext Context, EvidenceQuery query)
{
var predicate = PredicateBuilder.True<Evidence>();
predicate = predicate.And(x => x.IsActive && x.EvidenceStatus != EvidenceStatus.ReleasedToOwner && x.EvidenceStatus != EvidenceStatus.Transfer);
if (query.EvidenceStatus is not EvidenceStatus.Disposed)
{
predicate = predicate.And(x => x.IsActive && x.EvidenceStatus != EvidenceStatus.Disposed);
}
if (query.EvidenceStatus is not EvidenceStatus.Incoming)
{
predicate = predicate.And(x => x.IsActive && x.EvidenceStatus != EvidenceStatus.Incoming);
}
var predicateAndOr = query.IsOr ? PredicateBuilder.False<Evidence>() : PredicateBuilder.True<Evidence>();
if (!string.IsNullOrWhiteSpace(query.CaseNumber))
{
predicateAndOr = predicateAndOr.AndOr(x => EF.Functions.Like(x.CaseFileNumber, $"%{query.CaseNumber}%"), query.IsOr);
}
predicate = predicate.And(predicateAndOr);
var predicateCompiled = predicate.Compile();
return query.IncludeRequests
? Context.Evidences.Include(x => x.Requests).AsNoTracking().Where(predicateCompiled).AsQueryable().OrderByDescending(x => x.Modified)
: Context.Evidences.Where(predicateCompiled).AsQueryable().OrderByDescending(x => x.Modified);
}
PredicateBuilder
public static class PredicateBuilder
{
public static Expression<Func<T, bool>> True<T>()
{
return (T _) => true;
}
public static Expression<Func<T, bool>> False<T>()
{
return (T _) => false;
}
public static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> expr1, Expression<Func<T, bool>> expr2)
{
InvocationExpression right = Expression.Invoke(expr2, expr1.Parameters.Cast<Expression>());
return Expression.Lambda<Func<T, bool>>(Expression.OrElse(expr1.Body, right), expr1.Parameters);
}
public static Expression<Func<T, bool>> And<T>(this Expression<Func<T, bool>> expr1, Expression<Func<T, bool>> expr2)
{
InvocationExpression right = Expression.Invoke(expr2, expr1.Parameters.Cast<Expression>());
return Expression.Lambda<Func<T, bool>>(Expression.AndAlso(expr1.Body, right), expr1.Parameters);
}
public static Expression<Func<T, bool>> AndOr<T>(this Expression<Func<T, bool>> expr1, Expression<Func<T, bool>> expr2, bool or = false)
{
if (!or)
{
return expr1.And(expr2);
}
return expr1.Or(expr2);
}
}
in this particular example, query.IsOr will always be false so predicate will always build with And predicate. Also query.CaseNumber will have the string value for caseNumber
I have removed many more conditions and if statement all of them just checked for one property and add that check to predicate, but as I said they are not relevant here as in this scenario only the if statements I've shown here are true and hence validated.
You're building a predicate as an Expression<Func<Evidence, bool>>, but then you're .Compile()ing it into a Func<Evidence, bool>. That means when you call .Where() you're no longer calling Queryable.Where<T>(this IQueryable<T> source, Expression<Func<Evidence, bool>> criteria), but rather Enumerable.Where(this IEnumerable<T> source, Func<Evidence, bool> criteria).
The difference there is that the method you're calling will treat the query as an IEnumerable<T>, and rather than allowing Entity Framework to issue a query against the database with your criteria as a WHERE clause, it will bring all of the objects back from the database, and then pass them into the criteria function (which has been compiled into memory) to determine which ones to keep.
In other words, your code in the past was terribly inefficient, and you didn't realize it. EF.Functions.Like was never meant to be invoked: it was only supposed to be evaluated as part of a query. Earlier versions of EF.Functions.Like did their best to mimic the behavior of SQL's LIKE when they were called, whereas newer versions will just throw an exception to warn you that you're doing it wrong.
You need to avoid .Compile(), and pass predicate directly into your .Where() call.
predicate = predicate.And(predicateAndOr);
return query.IncludeRequests
? Context.Evidences.Include(x => x.Requests).AsNoTracking().Where(predicate).OrderByDescending(x => x.Modified)
: Context.Evidences.Where(predicate).OrderByDescending(x => x.Modified);
That may lead to other issues, but it'll get you one step closer to doing it right.

Making generic expression generating method in C#

I am constructing an IQueryable query using several methods. The methods are somehow complex, but the problem I want to solve can be extracted and simplified as follows. I have two methods
private Expression<Func<T, bool>> CreateExpressionA(string ValueA)
{
return a => a.PropertyA.ToLower() == ValueA;
}
private Expression<Func<T, bool>> CreateExpressionB(string ValueB)
{
return a => a.PropertyB.ToLower() == ValueB;
}
and what I would rather have is this:
private Expression<Func<T, bool>> CreateExpression(??? Selector, string Value)
{
return a => a.Selector.ToLower() == Value;
}
or a similar approach that would allow me to avoid having two same methods with the only difference being in what property of an object is being used there.
Is it possible to do this in some elegant way?
You can pass in a selector Func that returns a string property:
private Expression<Func<T, bool>> CreateExpression<T>(Func<T, string> selector, string value)
{
return a => selector(a).ToLower() == value;
}
Usage:
CreateExpression<MyType>(x => x.PropertyA, "thevalue");
You could use reflection, more precisely the class PropertyInfo as an argument, but the implementation would be more involved. The method could be implemented as follows.
private Expression<Func<T, bool>> CreateExpression(PropertyInfo iInfo, string Value)
{
return a => (iInfo.GetPropertyValue(a) as string).ToLower() == ValueB;
}
However, note that this will work only if the type of the property is string, otherwise an additional type parameter could be used.
What you can do is create the Expression from scratch:
private Expression<Func<T, bool>> CreateExpression(string propertyName, string value) {
ParameterExpression param = Expression.Parameter(typeof(T));
MemberExpression property = Expression.Property(param, propertyName);
var valExpr = Expression.Constant(value);
var body = Expression.Equal(property, valExpr);
return Expression.Lambda<Func<T, bool>>(body, param);
}
Call with:
var expression = CreateExpression<TypeOfA>("PropertyA", "ValueForPropertyA");
It's a bit off the top of my head, but I think this should at least get you started. Let me know if you need help.

Add And statement to Expression<Func<T, bool>>

I have this function:
public List<T> Find(Expression<Func<T, bool>> query)
{
}
Find(x => x.Id == 4);
Inside the method Find I want to chain And Condition.
something like:
query.And(x => x.Secured == false);//Secured is a memeber inside T like Id.
Your problem is that you want to access a member of T within the generic method. T could be anything at this point so the compiler will not let you access Secured since T may not have a Secured member.
You could cast T to dynamic, but this just changes a compile time error to a runtime error (plus it's horrible).
The best way would be to ensure T implements some known interface that has a Secured member.
public List<T> Find(Expression<Func<T, bool>> query) where T : ISecured
The expression must be "opened" and rebuilt, like this:
public List<T> Find<T>(Expression<Func<T, bool>> query)
{
ParameterExpression parameter = query.Parameters[0];
Expression body = query.Body;
MemberExpression property = Expression.Property(parameter, "Secured");
body = Expression.AndAlso(body, Expression.Not(property));
Expression<Func<T, bool>> query2 = Expression.Lambda<Func<T, bool>>(body, parameter);
// Now you can use query2
return null;
}
Note that I'm considering this x.Secured == false to be equivalent to !x.Secured. Clearly Secured could be a strange class that overloads the == operator, but I'll ignore that case.
As suggested by #Ralf, you could even simply do two .Where. like:
public List<T> Find<T>(Expression<Func<T, bool>> query)
{
ParameterExpression parameter = query.Parameters[0];
MemberExpression property = Expression.Property(parameter, "Secured");
Expression<Func<T, bool>> query2 = Expression.Lambda<Func<T, bool>>(Expression.Not(property), parameter);
return context.Set<T>
.Where(query)
.Where(query2)
.ToList();
}
(I'm using as an example context.Set<T>, that is very similar to what you would do if you are using Entity Framework, but in general nearly all the IQuerable<>/IEnumerable<> treat two .Where() like a single .Where() with an && condition)
Something like
Find(x => x.Id == 4 && x.Secured == false);

Convert Expression<T, string>> to Expression<T, bool>>

so I want to make filtering work automaticly based on some easy settings. The code I have is this:
public ActionResult Index() // here I want to add filtering for Status I only want to show the active ones
{
IQueryable<Ticket> cases = db.Cases().AsQueryable();
cases = cases.EnablePaging().EnableFilterFor(x => x.Status);
return View(cases);
}
EnableFilterFor looks like this:
public static IQueryable<T> EnableFilterFor<T>(this IQueryable<T> queryable, Expression<Func<T, string>> keySelector)
{
string filterValue= "Active";
//Expression<Func<T, bool>> whereexpresion = keySelector.Compile() == "Active"
queryable = queryable.Where(
//here do the magic !! so that the result will be 'x=>x.Status == filterValue');
);
return queryable;
}
I googled a lot, tried many different things but no success. I somehow have to combine the keySelector and the filterValue to work (I need an Expression for the Where method to work). Any help would be greatly appreciated.
EDIT: After testing both solutions (thank you both!) I found out that Poke had the best one. Poke his code is the only code that doesn't change the way that the SQL is generated. When I took a look at Servy his generated SQL it always did an EXTRA Sql select query and an EXTRA and in the WHERE clause... No idea why :)
IQueryable.Where requires an Expression<Func<T, bool>>, so that will be the thing we need to build. As we want to integrate something from another expression (a Expression<Func<T, string>>), we have to build the expression “by hand”.
So in the end, we want to call LambdaExpression.Lambda<Func<T, bool>>(…) to get our expression for Where, but we need to fill in the expression body:
// first, we reuse the parameter from the `keySelector` expression
ParameterExpression param = keySelector.Parameters[0];
// The body is now just an equality comparison of the `keySelector`
// body, and the constant `filterValue`
Expression body = Expression.Equal(keySelector.Body, Expression.Constant(filterValue));
// now we just need to create a lambda expression for that body with the
// saved parameter and it’s all done:
queryable = queryable.Where(Expression.Lambda<Func<T, bool>>(body, param));
What we'll need here is a Compose method, for expressions. It'll take an expression that uses a value, and another expression that conceptually will use the result of the first expression as its input, generating a new output.
public static Expression<Func<TFirstParam, TResult>>
Compose<TFirstParam, TIntermediate, TResult>(
this Expression<Func<TFirstParam, TIntermediate>> first,
Expression<Func<TIntermediate, TResult>> second)
{
var param = Expression.Parameter(typeof(TFirstParam), "param");
var newFirst = first.Body.Replace(first.Parameters[0], param);
var newSecond = second.Body.Replace(second.Parameters[0], newFirst);
return Expression.Lambda<Func<TFirstParam, TResult>>(newSecond, param);
}
It will require the ability to replace one expression with another, which we can do using the following:
public static Expression Replace(this Expression expression,
Expression searchEx, Expression replaceEx)
{
return new ReplaceVisitor(searchEx, replaceEx).Visit(expression);
}
internal class ReplaceVisitor : ExpressionVisitor
{
private readonly Expression from, to;
public ReplaceVisitor(Expression from, Expression to)
{
this.from = from;
this.to = to;
}
public override Expression Visit(Expression node)
{
return node == from ? to : base.Visit(node);
}
}
Now we can write:
public static IQueryable<T> EnableFilterFor<T>(
this IQueryable<T> queryable,
Expression<Func<T, string>> keySelector)
{
string filterValue= "Active";
return queryable.Where(keySelector.Compose(status => status == filterValue));
}

Passing a where clause in a predicate expression

I have the following predicate expression.
public IQueryable<T> Find(System.Linq.Expressions.Expression<Func<T, bool>> predicate)
{
return //boolean
}
Here is where I am calling the method:
public void TestMethod1()
{
author.AuthorName = authorName;
using (var context = new AspBlogRepository<Author>())
{
if (context.Find(e => e.AuthorName == authorName))
{
//do nothing
}
context.Add(author);
}
}
I get an error saying that I you cannot convert an IQueryable to bool. I just want to be able to use my predicate expression to see if the author is already in the database.
Any help would be much appreciated.
Thanks!
public IQueryable<T> Find(System.Linq.Expressions.Expression<Func<T, bool>> predicate)
{
return //boolean
}
The problem is here, if you indeed return boolean. If this method is inside your context, you would first need to resolve Author and then filter the Authors in your repository.
If this is an EF context, and you have access to the EF context in your repository, you should be able to get the db set like this:
var entitySet = context.Set<T>();
Then, you can run your predicate against the entitySet returning the filtered results.
Error says it all. context.Find(e => e.AuthorName == authorName) is returning IQueryable<T>. If is expecting a bool
So your usage of if (context.Find(e => e.AuthorName == authorName)) is wrong. Change it to
if (context.Find(e => e.AuthorName == authorName).Any())
{
}
context.Find returns an IQueryable<T>. Your code could instead look like:
if (context.Find(e => e.AuthorName == authorName).Count() == 1)
{
//do nothing
}

Categories

Resources