Combinazione di due espressioni (Expression < Func < T, bool > >)
-
19-08-2019 - |
Domanda
Ho due espressioni di tipo Expression < Func < T, bool > >
e voglio prendere OR, AND o NOT di questi e ottenere una nuova espressione dello stesso tipo
Expression<Func<T, bool>> expr1;
Expression<Func<T, bool>> expr2;
...
//how to do this (the code below will obviously not work)
Expression<Func<T, bool>> andExpression = expr AND expr2
Soluzione
Bene, puoi usare Expression.AndAlso
/ OrElse
ecc per combinare espressioni logiche, ma il problema sono i parametri; stai lavorando con lo stesso ParameterExpression
in expr1 ed expr2? In tal caso, è più semplice:
var body = Expression.AndAlso(expr1.Body, expr2.Body);
var lambda = Expression.Lambda<Func<T,bool>>(body, expr1.Parameters[0]);
Funziona bene anche per annullare una singola operazione:
static Expression<Func<T, bool>> Not<T>(
this Expression<Func<T, bool>> expr)
{
return Expression.Lambda<Func<T, bool>>(
Expression.Not(expr.Body), expr.Parameters[0]);
}
Altrimenti, a seconda del provider LINQ, potresti essere in grado di combinarli con Invoke
:
// OrElse is very similar...
static Expression<Func<T, bool>> AndAlso<T>(
this Expression<Func<T, bool>> left,
Expression<Func<T, bool>> right)
{
var param = Expression.Parameter(typeof(T), "x");
var body = Expression.AndAlso(
Expression.Invoke(left, param),
Expression.Invoke(right, param)
);
var lambda = Expression.Lambda<Func<T, bool>>(body, param);
return lambda;
}
Da qualche parte, ho del codice che riscrive un albero delle espressioni sostituendo i nodi per rimuovere la necessità di Invoke
, ma è piuttosto lungo (e non ricordo dove l'ho lasciato ...)
Versione generalizzata che seleziona il percorso più semplice:
static Expression<Func<T, bool>> AndAlso<T>(
this Expression<Func<T, bool>> expr1,
Expression<Func<T, bool>> expr2)
{
// need to detect whether they use the same
// parameter instance; if not, they need fixing
ParameterExpression param = expr1.Parameters[0];
if (ReferenceEquals(param, expr2.Parameters[0]))
{
// simple version
return Expression.Lambda<Func<T, bool>>(
Expression.AndAlso(expr1.Body, expr2.Body), param);
}
// otherwise, keep expr1 "as is" and invoke expr2
return Expression.Lambda<Func<T, bool>>(
Expression.AndAlso(
expr1.Body,
Expression.Invoke(expr2, param)), param);
}
A partire da .net 4.0. Esiste la classe ExpressionVistor che consente di creare espressioni sicure per EF.
public static Expression<Func<T, bool>> AndAlso<T>(
this Expression<Func<T, bool>> expr1,
Expression<Func<T, bool>> expr2)
{
var parameter = Expression.Parameter(typeof (T));
var leftVisitor = new ReplaceExpressionVisitor(expr1.Parameters[0], parameter);
var left = leftVisitor.Visit(expr1.Body);
var rightVisitor = new ReplaceExpressionVisitor(expr2.Parameters[0], parameter);
var right = rightVisitor.Visit(expr2.Body);
return Expression.Lambda<Func<T, bool>>(
Expression.AndAlso(left, right), parameter);
}
private class ReplaceExpressionVisitor
: ExpressionVisitor
{
private readonly Expression _oldValue;
private readonly Expression _newValue;
public ReplaceExpressionVisitor(Expression oldValue, Expression newValue)
{
_oldValue = oldValue;
_newValue = newValue;
}
public override Expression Visit(Expression node)
{
if (node == _oldValue)
return _newValue;
return base.Visit(node);
}
}
Altri suggerimenti
Puoi usare Expression.AndAlso / OrElse per combinare espressioni logiche, ma devi assicurarti che ParameterExpressions sia uguale.
Ho avuto problemi con EF e PredicateBuilder , quindi ho creato il mio senza ricorrere a Invoke, che ho potrebbe usare in questo modo:
var filterC = filterA.And(filterb);
Codice sorgente per il mio PredicateBuilder:
public static class PredicateBuilder {
public static Expression<Func<T, bool>> And<T>(this Expression<Func<T, bool>> a, Expression<Func<T, bool>> b) {
ParameterExpression p = a.Parameters[0];
SubstExpressionVisitor visitor = new SubstExpressionVisitor();
visitor.subst[b.Parameters[0]] = p;
Expression body = Expression.AndAlso(a.Body, visitor.Visit(b.Body));
return Expression.Lambda<Func<T, bool>>(body, p);
}
public static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> a, Expression<Func<T, bool>> b) {
ParameterExpression p = a.Parameters[0];
SubstExpressionVisitor visitor = new SubstExpressionVisitor();
visitor.subst[b.Parameters[0]] = p;
Expression body = Expression.OrElse(a.Body, visitor.Visit(b.Body));
return Expression.Lambda<Func<T, bool>>(body, p);
}
}
E la classe di utilità per sostituire i parametri in un lambda:
internal class SubstExpressionVisitor : System.Linq.Expressions.ExpressionVisitor {
public Dictionary<Expression, Expression> subst = new Dictionary<Expression, Expression>();
protected override Expression VisitParameter(ParameterExpression node) {
Expression newValue;
if (subst.TryGetValue(node, out newValue)) {
return newValue;
}
return node;
}
}
Joe Albahari (autore di C # 3.0 in breve e LINQPad) ha scritto un'utilità chiamata PredicateBuilder che può essere utilizzata insieme alle funzioni AND e OR.
http://www.albahari.com/nutshell/predicatebuilder.aspx
Mentre funziona su funzioni è open source, quindi puoi verificarlo e vedere come funziona.
Se il tuo provider non supporta Invoke e devi combinare due espressioni, puoi usare un ExpressionVisitor per sostituire il parametro nella seconda espressione con il parametro nella prima espressione.
class ParameterUpdateVisitor : ExpressionVisitor
{
private ParameterExpression _oldParameter;
private ParameterExpression _newParameter;
public ParameterUpdateVisitor(ParameterExpression oldParameter, ParameterExpression newParameter)
{
_oldParameter = oldParameter;
_newParameter = newParameter;
}
protected override Expression VisitParameter(ParameterExpression node)
{
if (object.ReferenceEquals(node, _oldParameter))
return _newParameter;
return base.VisitParameter(node);
}
}
static Expression<Func<T, bool>> UpdateParameter<T>(
Expression<Func<T, bool>> expr,
ParameterExpression newParameter)
{
var visitor = new ParameterUpdateVisitor(expr.Parameters[0], newParameter);
var body = visitor.Visit(expr.Body);
return Expression.Lambda<Func<T, bool>>(body, newParameter);
}
[TestMethod]
public void ExpressionText()
{
string text = "test";
Expression<Func<Coco, bool>> expr1 = p => p.Item1.Contains(text);
Expression<Func<Coco, bool>> expr2 = q => q.Item2.Contains(text);
Expression<Func<Coco, bool>> expr3 = UpdateParameter(expr2, expr1.Parameters[0]);
var expr4 = Expression.Lambda<Func<Recording, bool>>(
Expression.OrElse(expr1.Body, expr3.Body), expr1.Parameters[0]);
var func = expr4.Compile();
Assert.IsTrue(func(new Coco { Item1 = "caca", Item2 = "test pipi" }));
}
Suggerisco un ulteriore miglioramento alle soluzioni PredicateBuilder e ExpressionVisitor
. L'ho chiamato UnifyParametersByName
e puoi trovarlo nella mia libreria MIT: LinqExprHelper . Permette di combinare espressioni arbitrarie lambda. Di solito vengono poste domande sull'espressione predicata, ma questa idea si estende anche alle espressioni di proiezione.
Il codice seguente utilizza un metodo ExprAdres
che crea un'espressione parametrica complicata, usando lambda inline. Questa espressione complicata viene codificata una sola volta e quindi riutilizzata, grazie alla mini-libreria LinqExprHelper
.
public IQueryable<UbezpExt> UbezpFull
{
get
{
System.Linq.Expressions.Expression<
Func<UBEZPIECZONY, UBEZP_ADRES, UBEZP_ADRES, UbezpExt>> expr =
(u, parAdrM, parAdrZ) => new UbezpExt
{
Ub = u,
AdrM = parAdrM,
AdrZ = parAdrZ,
};
// From here an expression builder ExprAdres is called.
var expr2 = expr
.ReplacePar("parAdrM", ExprAdres("M").Body)
.ReplacePar("parAdrZ", ExprAdres("Z").Body);
return UBEZPIECZONY.Select((Expression<Func<UBEZPIECZONY, UbezpExt>>)expr2);
}
}
E questo è il codice di costruzione della sottoespressione:
public static Expression<Func<UBEZPIECZONY, UBEZP_ADRES>> ExprAdres(string sTyp)
{
return u => u.UBEZP_ADRES.Where(a => a.TYP_ADRESU == sTyp)
.OrderByDescending(a => a.DATAOD).FirstOrDefault();
}
Quello che ho cercato di ottenere è stato quello di eseguire query parametrizzate senza bisogno di copiare e incollare e con la possibilità di usare lambda in linea, che sono così carini. Senza tutte queste cose di espressione di aiuto, sarei costretto a creare un'intera query in una volta sola.
Avevo bisogno di ottenere gli stessi risultati, ma usando qualcosa di più generico (dato che il tipo non era noto). Grazie alla risposta di Marc ho finalmente capito cosa stavo cercando di ottenere:
public static LambdaExpression CombineOr(Type sourceType, LambdaExpression exp, LambdaExpression newExp)
{
var parameter = Expression.Parameter(sourceType);
var leftVisitor = new ReplaceExpressionVisitor(exp.Parameters[0], parameter);
var left = leftVisitor.Visit(exp.Body);
var rightVisitor = new ReplaceExpressionVisitor(newExp.Parameters[0], parameter);
var right = rightVisitor.Visit(newExp.Body);
var delegateType = typeof(Func<,>).MakeGenericType(sourceType, typeof(bool));
return Expression.Lambda(delegateType, Expression.Or(left, right), parameter);
}
Penso che funzioni bene, vero?
Func<T, bool> expr1 = (x => x.Att1 == "a");
Func<T, bool> expr2 = (x => x.Att2 == "b");
Func<T, bool> expr1ANDexpr2 = (x => expr1(x) && expr2(x));
Func<T, bool> expr1ORexpr2 = (x => expr1(x) || expr2(x));
Func<T, bool> NOTexpr1 = (x => !expr1(x));