Sto cercando di creare un filtro degli articoli basato su proprietà archiviate in un altro dbset. Sto usando alcune classi:
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 */
}
Quando creo una query come questa, fa ciò che voglio fare:
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
});
Ma ora voglio creare dinamicamente l'ultima istruzione Where, come questa (dataFilter è un dizionario <stringa, dizionario <stringa, bool >> con alcune proprietà filtro):
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
});
La 'query' è come previsto ma il dove non funziona, l'errore:
System.Linq.Dynamic.Core.Exceptions.ParseException: 'No applicable aggregate method 'Any' exists'
Questo si verifica solo quando ci sono 2 affermazioni "Any" (divise per &&, lo stesso di quando lo faccio "hard-coded"). Non so perché ...
Invece di usare una stringa, usa direttamente la query:
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
});
Dynamic LINQ ha il proprio linguaggio di espressione . Le espressioni Lambda non iniziano con a =>
o ap =>
, c'è qualcosa chiamato current scope che semplifica alcune query, ma in generale è problematico con l'accesso ai parametri di livello esterno. Tutte le estensioni Queryable definiscono singolo parametro scope chiamato it
, che può essere omesso.
In breve, Dynamic LINQ non è adatto per query complesse con espressioni lambda nidificate che accedono ai parametri lambda esterni.
L'obiettivo può essere raggiunto in modo relativamente semplice con la combinazione di tempo di compilazione ed espressioni di runtime. L'idea è semplice.
Per prima cosa create un'espressione lambda di compilazione con parametri aggiuntivi che fungono da segnaposto. Quindi si sostituiscono i segnaposti con le espressioni effettive utilizzando il seguente visitatore di espressioni semplici:
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);
}
}
Piuttosto simile a string.Format
, ma con espressioni. Quindi puoi usare Expression.AndAlso
ed Expression.OrElse
per produrre il &&
e ||
parti.
Detto questo, ecco come appare nel tuo caso:
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);
Quindi utilizzare Where(predicate)
al posto del percorso corrente Where(query)
.