Dynamic Linq with ".Any" in Where clausule (C# / .Net Core / EF Core)

.net-core c# ef-core-2.1 entity-framework entity-framework-core

Question

I'm trying to make some article-filtering based on properties that are stored in another dbset. I'm using some classes:

public class Article
{
    public string ArticleCode { get; set; }
    public string var1 { get; set; }
    public string var2 { get; set; }
    public string var3 { get; set; }        
    public virtual List<ArticleProperty> Properties { get; set; }
}

public class ArticleProperty
{
    public string ArticleCode { get; set; }
    public string PropertyCode { get; set; }
    public string var4 { get; set; }
    public string var5 { get; set; }
    public string var6 { get; set; }
}

public class ArticleSummary
{
    public string ArticleCode { get; set; }
    public string var7 { get; set; }
    public string var8 { get; set; }       
}

public class WebDbContext : DbContext
{

    public virtual DbSet<Article> Article{ get; set; } 
    public virtual DbSet<ArticleProperty> ArticleProperty{ get; set; }
    /* some more code */
}

When I create a query like this, it does what I want to do:

IQueryable<ArticleSummary> Articles = _DbContext.Article
    .Where(a => a.var1 == SomeLocalVariable1)
    .Where(a => a.var2 == SomeLocalVariable2 || a.var2 == SomeLocalVariable3)
    .Where(a =>
            a.Properties.Any(ap =>
               (
                   (ap.ArticleCode == a.ArticleCode && ap.var4 == "A" && ap.var5 == "X") ||
                   (ap.ArticleCode == a.ArticleCode && ap.var4 == "A" && ap.var5 == "Y")
               )
            )
            &&
            a.Eigenschappen.Any(ap =>
               (
                   (ap.ArticleCode == a.ArticleCode && ap.var4 == "B" && ap.var5 == "Z")
               )
            )
        )
    .OrderByDescending(a => a.var6)
    .Select(a => new ArticleSummary
    {
        ArticleCode = a.ArticleCode ,
        var7 = a.var1
        var8 = a.var3
    });

But now I want to create the last Where-statement dynamically, like this (dataFilter is a Dictionary< string, Dictionary< string, bool>> with some filter-properties):

var query ="";
bool firstA = true;
foreach (KeyValuePair<string, Dictionary<string, bool>> filter in dataFilter)
{
    if (firstA)
        query += "a => ";
    else
        query += " && ";

    query += "a.Properties.Any(ap =>"
            +    "(";

    bool firstB = true;
    foreach (KeyValuePair<string,bool> filterDetail in filter.Value)
    {
        if (!firstB)
            query += " || ";

        query += "(ap.ArticleCode == a.ArticleCode && ap.var4 == \""+filter.Key+"\" && ap.var5 == \""+filterDetail.Key+"\")";
        firstB = false;
    }

    query +=    ")"
            + ")";
    firstA = false;
}

IQueryable<ArticleSummary> Articles = _DbContext.Article
    .Where(a => a.var1 == SomeLocalVariable1)
    .Where(a => a.var2 == SomeLocalVariable2 || a.var2 == SomeLocalVariable3)
    .Where(query)
    .OrderByDescending(a => a.var6)
    .Select(a => new ArticleSummary
    {
        ArticleCode = a.ArticleCode ,
        var7 = a.var1
        var8 = a.var3
    });

The 'query' is as expected but the Where doesn't work, the error:

System.Linq.Dynamic.Core.Exceptions.ParseException: 'No applicable aggregate method 'Any' exists'

This only occurs when there are 2 'Any'- statements (divided by &&, the same as when I do it 'hard-coded'). I don't know why...

1
1
10/19/2018 9:27:42 AM

Accepted Answer

Instead of using a string, just use the query directly:

IQueryable<ArticleSummary> Articles = _DbContext.Article
    .Where(a => a.var1 == SomeLocalVariable1)
    .Where(a => a.var2 == SomeLocalVariable2 || a.var2 == SomeLocalVariable3)
    .Where(query);
foreach(...) {
    Articles = Articles.Where(...);
}
Articles = Articles.OrderByDescending(a => a.var6)
    .Select(a => new ArticleSummary
    {
        ArticleCode = a.ArticleCode ,
        var7 = a.var1
        var8 = a.var3
    });
0
10/19/2018 9:44:33 AM

Popular Answer

Dynamic LINQ has its own expression language. Lambda expressions do not start with a => or ap =>, there is something called current scope which simplifies some queries, but in general is problematic with accessing outer level parameters. All queryable extensions define single scoped parameter called it, which can be omitted.

Shortly, Dynamic LINQ is not well suitable for complex queries with nested lambda expression accessing outer lambda parameters.

The goal can be achieved relatively easy with the combination of compile time and runtime expressions. The idea is simple.

First you create a compile time lambda expression with additional parameters which serve as placeholders. Then you replace the placeholders with actual expressions using the following simple expression visitor:

public static class ExpressionExtensions
{
    public static Expression ReplaceParameter(this Expression expression, ParameterExpression source, Expression target)
    {
        return new ParameterReplacer { Source = source, Target = target }.Visit(expression);
    }

    class ParameterReplacer : ExpressionVisitor
    {
        public ParameterExpression Source;
        public Expression Target;
        protected override Expression VisitParameter(ParameterExpression node)
            => node == Source ? Target : base.VisitParameter(node);
    }
}

Pretty much like string.Format, but with expressions. Then you can use Expression.AndAlso and Expression.OrElse to produce the && and || parts.

With that being said, here is how it looks in your case:

Expression<Func<Article, string, string, bool>> detailExpr = (a, var4, var5) =>
    a.Properties.Any(ap => ap.ArticleCode == a.ArticleCode && ap.var4 == var4 && ap.var5 == var5);

var p_a = detailExpr.Parameters[0];
var p_var4 = detailExpr.Parameters[1];
var p_var5 = detailExpr.Parameters[2];

var body = dataFilter
    .Select(filter => filter.Value
        .Select(filterDetail => detailExpr.Body
            .ReplaceParameter(p_var4, Expression.Constant(filter.Key))
            .ReplaceParameter(p_var5, Expression.Constant(filterDetail.Key)))
        .Aggregate(Expression.OrElse))
    .Aggregate(Expression.AndAlso);

var predicate = Expression.Lambda<Func<Article, bool>>(body, p_a);

Then use Where(predicate) in place of your current Where(query).



Related Questions





Related

Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow