Combiner des Expressions Trees
Il y a tout juste quelques jours, on pair programmait avec Anthony sur un projet. Nous nous sommes retrouvés a vouloir combiner au runtime des Expressions trees en fonction de parametres évalués au runtime.
En gros on avait envie d’écrire un truc du genre
Expression predicate = x => x.Id > 1;
puis un peu plus loin
predicate += x => x.Name.Length sexy
Pour faire plaisir à [maitre Etienne](https://twitter.com/EjDel?lang=fr) on va travailler a base de tests unitaires.
Commençons par la version naïve; En cherchant un peu sur la msdn on tombera facilement sur [Expression.AndAlso](https://msdn.microsoft.com/fr-fr/library/bb382914(v=vs.110).aspx) permettant de créer une nouvelle expression qui combine deux Expressions left et right
[Fact]
public void SimpleCombining_Fails()
{
// Arrange
Expression firstPredicate = x => x.Id > 1;
Expression secondPredicate = x => x.Name.Length lambda = Expression.Lambda(binaryexp, parameters);
// Act
Action filter = () => {
var combinedPredicate = lambda.Compile();
var result = _persons.Where(combinedPredicate);
};
// Assert
Assert.Throws(() => filter());
}
On commence par définir nos deux prédicats (left & right) que l'on combine dans une expression binaire AndAlso (et logique) en utilisant comme paramètre générique T le paramètre utilisé dans la premiere expression initiale.
On essaye par la suite d'utiliser la lambda créée pour l'occasion dans une instruction [Where](https://msdn.microsoft.com/fr-fr/library/bb549418(v=vs.110).aspx) que vous connaissez tous en Linq.
Bon, j'avais déjà vendu la méche en disant que c'était une version naïve; effectivement a l'execution vous allez avoir une [InvalidOperationException](https://msdn.microsoft.com/fr-fr/library/system.invalidoperationexception(v=vs.110).aspx)
System.InvalidOperationException: ‘variable ‘x’ of type ‘AndOrExpressions.Person’ referenced from scope ‘’, but it is not defined’
Pour faire simple, dans nos deux expressions initiales, le paramètre x bien qu'il se nomme de la même maniere et soit du même type, il ne s'agit pas du meme objet. x est donc inconnu dans la seconde expression.
Etant donné que les Expressions sont des objets [immutables](https://en.wikipedia.org/wiki/Immutable_object) il va nous falloir un moyen de créer/réécrire une expression donnée.
C'est pour ce genre de cas qu'MS a mis à notre disposition la classe [ExpressionVisitor](https://msdn.microsoft.com/fr-fr/library/system.linq.expressions.expressionvisitor(v=vs.110).aspx). Celle-ci nous permet de traverser l'expression tree et d'apporter des modifications. Rappelez vous que chaque modification produira une nouvelle expression, c'est le principe même de l'immutabilité.
Ok, commencons par créer une visiteur qui remplacera un paramètre T par un autre passé en paramètre.
public class ReplaceExpressionVisitor : ExpressionVisitor { private readonly Expression _searched; private readonly Expression _replacement;
public ReplaceExpressionVisitor(Expression searched, Expression replacement)
{
_searched = searched;
_replacement = replacement;
}
public override Expression Visit(Expression node)
{
if (node == _searched)
{
return _replacement;
}
return base.Visit(node);
}
}
Simple, non?
Du coup si on réécrivait notre test de la facon suivante en imaginant une methode And qui permet de combiner des Expressions
[Fact] public void ReplacingType_Succeed() { // Arrange Expression firstPredicate = x => x.Id > 1; Expression secondPredicate = x => x.Name.Length Assert.Equal(people, Anto)); }
Il ne nous reste qu'à définir une methode d'extension qui répond a cette signature, comme ceci :
public static class ExpressionsExtensions { public static Func And(this Expression> firstOperand, Expression> secondOperand) { var replacement = Expression.Parameter(typeof(T));
var leftVisitor = new ReplaceExpressionVisitor(firstOperand.Parameters[0], replacement);
var left = leftVisitor.Visit(firstOperand.Body);
var rightVisitor = new ReplaceExpressionVisitor(secondOperand.Parameters[0], replacement);
var right = rightVisitor.Visit(secondOperand.Body);
return Expression.Lambda>(Expression.AndAlso(left, right), replacement)
.Compile();
}
}
Cette methode va combiner avec Expression.AndAlso nos deux expressions initiales, mais cette fois ci on aura prit soin de les réécrire en utilisant le même paramètre générique. Et évidemment cette fois-ci, le test est vert :)