Home > Net >  LINQ dynamic expression in order by clause
LINQ dynamic expression in order by clause

Time:10-12

I have the following query:

 product = product.OrderByDescending(d => d.ProductAttributeItem
                            .Where(ai => ai.AttributeItem.AttributeId == (int)DefaultAttributes.Name)
                            .SelectMany(p => p.AttributeItem.AttributeItemValue)
                            .Any(o => EF.Functions.Like(o.Value, "%apple%") || EF.Functions.Like(o.Value, "%samsung%"))

which in fact is working perfectly fine. product is a rather complex query built with many predicates based on input filters. Here we are interested in the Any() part of the query, specifically the predicate part - how EF.Functions.Like(o.Value, "%apple%") || EF.Functions.Like(o.Value, "%samsung%") can be generated dynamically.

I am already using some predicate builder extension methods in our project and they are working great for non nested cases like:

var condition = PredicateBuilder.True<AttributeItemValue>();

if(filters.OnlyActivated)
     condition = condition.And(product => product.IsActive);

product = _context.Product.Where(condition);

So I have tried to build the predicate in loop:

var aivCond = PredicateBuilder.True<AttributeItemValue>();
foreach (var s in searchQueryArray)
{
      aivCond = aivCond.Or(f =>
      EF.Functions.Like(f.Value, "%"   s   "%"));
}

So now the aivCond is of type Expression<Func<AttributItemValue, bool> but this can't be used to replace the lambda in Any() because it expects Func<TSource, bool>. Tried to compile it this way aivCond.Compile() but the following error occurs:

System.ArgumentException: Expression of type 'System.Func`2[AttributeItemValue,System.Boolean]' cannot be used for parameter of type 'System.Linq.Expressions.Expression`1[System.Func`2[AttributeItemValue,System.Boolean]]' of method 'Boolean Any[AttributeItemValue](System.Linq.IQueryable`1[AttributeItemValue]

I have also tried to build the lambda expression from string:

var filter = "f => EF.Functions.Like(f.Value, \"%apple%\") || f => EF.Functions.Like(f.Value, \"%samsung%\")";

                var options = ScriptOptions.Default
                    .AddReferences(typeof(AttributeItemValue).Assembly)
                    .AddReferences(typeof(Microsoft.EntityFrameworkCore.EF).Assembly)
                    .AddReferences(typeof(DbFunctions).Assembly)
                    .AddImports("Microsoft.EntityFrameworkCore");

                Func<AttributeItemValue, bool> filterExpression = await CSharpScript.EvaluateAsync<Func<AttributeItemValue, bool>>(filter, options);

with no luck.

I know I missing knowledge for expression trees, compiling and invocation for delegates so any help(and explanation) would be really appreciated!

EDIT / SOLUTION

So there is a solution thanks to the help of Richard Deeming. He was right for starting the predicate with False. I stupidly copy/pasted code from a different method without noticing.

As about his second comment, adding AsQueryable() allows to pass an Expression<Func<TSource, bool>> which is pretty obvious but not for me. This translates fine and the compiler is ok.

Anyway, there is another approach, using LINQKit's AsExpandble() method and the inbuilt Compile() method in the Expression class and as described in LINQKit:

Compile is an inbuilt method in the Expression class. It converts the Expression<Func<Purchase,bool> into a plain Func<Purchase,bool> which satisfies the compiler. Of course, if this method actually ran, we'd end up with compiled IL code instead of an expression tree, and LINQ to SQL or Entity Framework would throw an exception. But here's the clever part: Compile never actually runs; nor does LINQ to SQL or Entity Framework ever get to see it. The call to Compile gets stripped out entirely by a special wrapper that was created by calling AsExpandable, and substituted for a correct expression tree.

So the code would look like this:

product = product.AsExpandable().OrderBy(d => d.ProductAttributeItem
          .Where(ai => ai.AttributeItem.AttributeId == (int)DefaultAttributes.Name)
          .SelectMany(p => p.AttributeItem.AttributeItemValue)
          .Any(aivCond.Compile()));

Thank you everyone for the help and I hope this question helps somebody lost in the code... !

CodePudding user response:

As mentioned in the comments, you just need to use the AsQueryable method on the collection to pass in the Expression<Func<TItem, bool>> as the filter.

product = product.OrderByDescending(d => d.ProductAttributeItem
    .Where(ai => ai.AttributeItem.AttributeId == (int)DefaultAttributes.Name)
    .SelectMany(p => p.AttributeItem.AttributeItemValue)
    .AsQueryable().Any(aivCond);
  • Related