juin
2009
Entity Framework peut s’utiliser de différentes façon. La méthode la plus connue est certainement l’utilisation de Linq To Entities qui permet d’écrire des requêtes Linq sur les collections d’entités.
Le problème c’est que cette méthode est « statique » dans le sens où une fois que la requête est écrite seul les paramètres varient, il est impossible de modifier, par exemple, l’opérateur. Or ce genre de chose peut être intéressant lorsque, toujours par exemple, on écrit un composant de recherche.
Dans ce cas il y a deux façons de procéder :
1°) Utilisation de l' »ESql » : il s’agit d’un Sql adapté à EntityFramework. Cette solution est intéressante à bien des égards, et surtout elle est simple. Mais ce n’est pas le sujet de ce post aussi si vous souhaitez en savoir plus sur cette méthode je vous invite à consulter l’excellent blog de Matthieu Mézil qui le traite
2°) Créer une requête Linq de façon dynamique. Et oui vous ne le savez peut être pas mais il est possible de construire une requête Linq de façon dynamique en utilisant les classes de définition du « langage ». Afin de comprendre comment tout ça fonctionne il faut il aller mettre les main dans le cambouis.
Aller c’est partie. Pour commencer il faut bien comprendre que quand vous écrivez ce code :
var results = from user in context.Users where user.Username == "admin" select user;
le compilateur ne va pas laisser le code « dans cet état ». Cette expression Linq va être transformé en appel à des classes et fonctions permettant une exécution « normale ». Le moyen le plus simple pour le vérifier c’est encore d’aller voir le code généré par le compilateur à l’aide de Reflector :
IQueryable<Users> results = context.Users.Where<Users>( Expression.Lambda<Func<Users, bool>>(Expression.Equal( Expression.Property(CS$0$0000 = Expression.Parameter(typeof(Users), "user"), (MethodInfo) methodof(Users.get_Username)), Expression.Constant("admin", typeof(string)), false, (MethodInfo) methodof(string.op_Equality)), new ParameterExpression[] { CS$0$0000 }));
Ouf. Okay, on prend deux aspirines, et on commence à analyser tout ça :
- Notre where a été « traduit » en en un appel à la méthode d’extension Where<T> . ça c’est la partie simple :d
- Ensuite notre simple user.Username == « admin » a été « traduit » en un charabia gigantesque que je ne vais pas détailler. Il y a deux choses qui m’intéresse dedans : l’utilisation de la classe Expression qui regroupe toutes les méthodes permettant de créer des requètes Linq « exécutable » et plus particulièrement les méthodes Equal, Parameter, et Constant
Revenons maintenant à notre problématique : Comment créer une requète Linq de manière dynamique ?
En se basant sur le code généré et sur la Msdn on arrive à « deviner » comment procéder. Il va falloir utiliser la classe Expression et construire la requète « à la main » avant de la faire s’exécuter dans Linq.
Pour cet exemple le but est d’avoir une méthode qui exécute une requète Linq sur une propriété dont le nom est passé en paramètre. Cela doit donner cela lors de l’utilisation :
context.Users.Search("Username", val);
Pour cela nous allons commencer par créer une méthode d’extension qui va étendre le type IQueryable et prendre en paramètre le nom de la propriété sur laquelle la requête doit être effectuée ainsi que la valeur recherché :
public static IQueryable<T> Search<T>(this IQueryable<T> query, string propertyName, string value) { throw new NotImplementedException(); }
La première étape va être de mettre en place le code nécessaire à exécuter une requète Linq simple sans nous préoccuper de faire changer le nom de la propriété. Toutefois il va falloir faire ça avec les méthodes de la classe Expression. Pour cela nous allons procéder dans le sens inverse de l’exécution : nous allons partir de la fin pour remonter aux différents éléments dont nous avons besoin. Première question : Que cherche t-on à faire au final ? La réponse est simple : Rien de plus qu’un Where sur la source de données. A partir de là voyons de quoi nous avons besoin pour faire un Where sur un IQueryable<T> ? Nous avons besoin d’un Expression<Func<T, bool>> . En gros nous avons d’un Expression qui sache exécuter une méthode prenant en paramètre une instance de T et qui renvoi un bool permettant de savoir si l’instance de T doit être incluse ou non dans le jeu de résultat. Or cet Expression<Func<T,bool>> n’est rien d’autre qu’une expression Lambda. Et comme la chose est bien faite, il existe dans la classe Expression une méthode statique nommée Lambda qui prend en paramètre les instructions de la méthode Lambda à créer ainsi que les paramètres. Ce qui nous donne ceci :
Expression<Func<T,bool>> filter = Expression.Lambda<Func<T, bool>>(comparaison, param);
Il nous reste maintenant à écrire le contenu des variables comparaison et param. La variable comparaison est du type Expression et va contenir le code permettant d’effectuer la comparaison entre la valeur de l’instance de T passée en argument et la valeur recherchée. La variable param est du type ParamExpression et va contenir l’instance de T à comparer. Commençons par le plus simple c’est à dire par créer notre variable param :
ParameterExpression param = Expression.Parameter(typeof(T), "$$param$$");
Ce code permet d’indiquer que le paramètre sera du type de T (le type utilisé dans la collection d’objet sur laquelle nous effectuerons la recherche) et aura pour nom $$param$$. Dans le cas présent, le nom de nous interesse pas car nous ne l’utiliserons pas directement. En effet nous allons manipuler l’objet param plutôt que de faire des références textuelles.
Passons maintenant au code de la variable comparaison. Cette variable va contenir le code permettant de déterminer si une instance de T (passée en argument par la variable param) doit faire partie du jeu de résultat ou non. Dans notre cas nous voulons comparer la valeur d’une propriété de l’instance de T à une valeur passée en argument. Pour savoir si deux membres sont égaux, la classe Expression nous fourni déjà une aide. En effet elle possède la méthode Equal qui permet de comparer deux Expressions. Il ne reste donc plus qu’à déterminer les deux Expressions qui seront utilisées dans cette méthode :
Expression comparaison = Expression.Equal(left, right);
Il faut donc déterminer ce qu’est l’expression de gauche puis l’expression de droite. Dans notre cas, l’expression de gauche sera la valeur de la propriété recherché. Il nous faut donc pouvoir appeler la propriété que nous voulons sur l’instance de T sur laquelle nous travaillons. Pour cela gardez à l’esprit que nous avons créé une ParameterExpression qui justement reférence l’instance de T. Reste qu’il faut trouver un moyen d’obtenir la valeur de la propriété qui nous intéresse et dont le nom est passé en paramètre. La encore la classe Expression va nous aider car elle possède une méthode nommée Property permettant de faire ce que nous souhaitons. Au final notre membre gauche de notre égalité se forme comme cela :
Expression left = Expression.Property(param, propertyName);
Simple non ?
Il ne nous reste plus qu’à écrire le membre droit de notre égalité et c’est gagné. Ce membre droit est égale à la valeur cherchée. C’est donc une constante du point de vue de la requête Linq. Là encore la classe Expression dispose de tout ce dont nous avons besoin :
Expression right = Expression.Constant(value);
Et voila nous avons toute les pièces pour notre requête Linq dynamique ! Voici le code complet :
public static IQueryable<T> Search<T>(this IQueryable<T> query, string propertyName, string value) { <font color="#000000"> </font>ParameterExpression param = Expression.Parameter(typeof(T), "$$param$$"); Expression left = Expression.Property(param, propertyName); Expression right = Expression.Constant(value); Expression comparaison = Expression.Equal(left, right); Expression<Func<T,bool>> filter = Expression.Lambda<Func<T, bool>>(comparaison, param); return query.Where(filter); }
et l’utilisation de notre méthode Search<T> :
string val = "admin"; var results = context.Users.Search<Users>("Username", val);
Une fois le principe compris, il est possible de quasiment tout faire à l’aide cette méthode. Toutefois ne perdez pas de vue qu’introduire de la réflexion, des tests sur les objets ou autres vous fait perdre une des grandes forces de Linq : La vérification de la syntaxe de la requête à la compilation. de plus en fonction des objets sur lesquels vous travaillez certaine méthodes seront acceptées alors que le même code ne fonctionnera pas car venant d’une source de données d’un autre type.
Archives
- juillet 2012
- mars 2012
- février 2012
- novembre 2011
- octobre 2011
- mars 2011
- novembre 2010
- octobre 2010
- septembre 2010
- août 2010
- avril 2010
- février 2010
- janvier 2010
- novembre 2009
- octobre 2009
- septembre 2009
- juin 2009
- mai 2009
- avril 2009
- mars 2009
- février 2009
- janvier 2009
- décembre 2008
- novembre 2008
- octobre 2008
- septembre 2008
- août 2008
- juin 2008
- mai 2008
- avril 2008
- février 2008
- mai 2007
- avril 2007
- mars 2007
- février 2007
- janvier 2007