Frédéric Mélantois
Requêtes dynamiques sur les IEnumerable
A partir d'un exemple fourni avec Visual Studio 2008, initialement prévu pour tout objet Queryable, nous allons présenter comment en ajoutant très peu de code rendre disponible aux IEnumerable un requêteur dynamique.
Par Frédéric Mélantois publié le 24/04/2008 à 15:03, lu 8413 fois,
Si vous avez installé Visual Studio 2008, vous pouvez consulter les nombreux exemples fournis avec cet outil de développement. Ils sont très riches pour appréhender les nouveautés de cette version.
 
/content/422cd3ca-1f76-40b2-a664-8bad94d1f129/image1.JPG
 
L'exemple que nous allons exploiter se trouve dans l'archive CSharpSamples.zip.
 
/content/422cd3ca-1f76-40b2-a664-8bad94d1f129/image2.JPG
 
La localisation exacte de la solution est précisée par les copies d'écran ci-dessus. Ce qui nous intéresse pour cet article se trouve dans le fichier « Dynamic.cs ». Mais avant de décortiquer ce fichier, examinons l'exemple présenté dans « program.cs ». A partir d'une base de données d'exemple : « Northwind » mappée pour fonctionner avec Linq To Sql, un mode de requête inhabituel est utilisé. Celui-ci intègre des données dynamiques :

string dbPath = Path.GetFullPath(Path.Combine(Application.StartupPath, @"..\..\..\..\Data\NORTHWND.MDF"));

string sqlServerInstance = @".\SQLEXPRESS";

string connString = "AttachDBFileName='" +

    dbPath + "';Server='" +

    sqlServerInstance + "';user instance=true;Integrated Security=SSPI;Connection Timeout=60";

 

Northwind db = new Northwind(connString);

db.Log = Console.Out;

 

var query =

    db.Customers.Where("City == @0 and Orders.Count >= @1", "London", 10).

    OrderBy("CompanyName").

    Select("New(CompanyName as Name, Phone)");

 

Console.WriteLine(query);

Console.ReadLine();

Une recherche rapide nous montre que des méthodes d'extension sont utilisées. L'utilisation du namespace « System.Linq.Dynamic » nous précise la location de ces dernières. Elles se trouvent dans le fichier « Dynamic.cs » en particulier dans la classe « DynamicQueryable » donc voici les signatures des différentes méthodes :
 
/content/422cd3ca-1f76-40b2-a664-8bad94d1f129/image3.JPG
 
Si nous prenons par exemple la méthode d'extension « Where », nous pouvons facilement analyser le mécanisme en regardant le code :

public static IQueryable Where(this IQueryable source, string predicate, params object[] values) {

    if (source == null) throw new ArgumentNullException("source");

    if (predicate == null) throw new ArgumentNullException("predicate");

    LambdaExpression lambda = DynamicExpression.ParseLambda(source.ElementType, typeof(bool), predicate, values);

    return source.Provider.CreateQuery(

        Expression.Call(

            typeof(Queryable), "Where",

            new Type[] { source.ElementType },

            source.Expression, Expression.Quote(lambda)));

}

La chaîne « predicate » ainsi que les éventuelles valeurs de paramètres « values » permettent de fabriquer une expression lambda qui sera pris en charge par la mécanique de Linq. Une classe statique « DynamicExpression » sert de boîte à outil :
 
/content/422cd3ca-1f76-40b2-a664-8bad94d1f129/image4.JPG
 
Voici le détail de la méthode « ParseLambda » :

public static LambdaExpression ParseLambda(ParameterExpression[] parameters, Type resultType,

                                            string expression, params object[] values)

{

    ExpressionParser parser = new ExpressionParser(parameters, expression, values);

    return Expression.Lambda(parser.Parse(resultType), parameters);

}

Nous voyons très facilement qu'une classe interne se charge de parser la chaîne d'expression et prend en charge les valeurs dynamiques fournies. Nous ne détaillerons pas le contenu de la classe « DynamicParser » mais je vous recommande de l'étudier car elle est très riche en enseignement.
Grâce à l'exemple fourni, nous pouvons constater que la classe « DynamicQueryable » regroupant toutes les méthodes d'extension permettant un requêtage dynamique est très pratique pour tous les objets implémentant l'interface « IQueryable ». Mais malheureusement, elle ne prend pas en charge les objets implémentant uniquement « IEnumerable<T> ».
Dans le cas où votre système a été mis en place avant l'arrivée de Linq, la couche d'accès ne renvoie pas d'« IQueryable<T> » mais plus vraisemblablement des IEnumerable<T>. Si vous souhaitez intégrer pour vos utilisateurs une possibilité de calcul dynamique sans à avoir à retoucher votre couche d'accès aux données et encore moins votre couche métier, il vous faudra développer un parseur. Ce dernier devra intégrer inévitablement de la génération de code donc de l'IL (intermediate language). Outre la gestion des éléments de la pile de chaque méthode en IL que vous développerez, il vous faudra aussi gérer une pile d'expression en fonction de la formule de calcul de l'utilisateur, identifier les propriétés de type valeur de votre objet, gérer le type decimal qui demande un traitement particulier. Bref, ce n'est pas infaisable mais ça risque de vous demander pas mal de temps pour mettre au point ce parseur.
Il serait dommage de ne pas utiliser le parseur fourni en exemple avec Visual Studio 2008 présenté ci-dessus. Seulement ce dernier a été écrit pour prendre en charge les « IQueryable » et non pas les IEnumerable<T>. Il y a sans doute un moyen de détourner le code pour que celui-ci puisse prendre en charge ces « IEnumerable<T> ».
En fait, le principe est simple : pour pouvoir utiliser le requêteur dynamique fourni en exemple dans Visual Studio 2008, il faut transformer en entrée le IEnumerable<T> en IQueryable<T>.
Prenons un exemple. Imaginons que nous ayons une liste de contrats (Police) que vos commerciaux ont vendus. Chaque client s'engage à verser un montant (Versement) suivant une périodicité définie (Periodicite 12=mensuel, 4=trimestriel). Sur ces contrats, vous percevez de la part de la compagnie qui vous demande de les vendre, un pourcentage (Taux). Vous souhaitez rémunérer vos commerciaux en fonction de leur vente par un calcul (Volume). Vous pouvez définir une entité Contrat qui peut ressembler à ceci :

public class ContratENT

{

    public int Periodicite {get; set;}

    public decimal Versement {get; set;}

    public decimal Taux {get; set;}

    public string Police { get; set; }

}

Nous pouvons simuler une initialisation pour notre exemple à défaut d'avoir de vrais données :

//Initialisation simulée d'un ensemble de 8000 contrats

List<ContratENT> c = new List<ContratENT>();

int cpt = 0;

do

{

    tabSource[cpt] = cpt1;

    c.Add(new ContratENT { Police = "num" + cpt.ToString(), Taux = 5.5M, Periodicite = 12, Versement = cpt * 10 });

 

}

while (cpt++ < 7999);

A partir des données, nous pouvons bénéficier du requêteur dynamique à condition de fournir un objet implémentant « IQueryable ». « List<ContratENT> » n'implémente pas cette interface. La méthode d'extension « AsQueryable() » fournie par Linq To Objects peut venir à notre secours :

int montantMin = 100;

 

var req = c.AsQueryable()

        .Where("Versement >= @0", montantMin)

        .OrderBy("Versement desc")

        .Select("new(police, Taux, Periodicite,Versement, (Versement * Periodicite) * (Taux / 10) as Volume )");

Ce code fonctionne très bien. Par contre, il impose à chacun de vos développeurs de connaître cette astuce. De plus, si vous souhaitez effectuer un « ToList() », il vous faudra caster pour passer d'un « IQueryable » à un « IQueryable<object> » ou plus généralement à un « IEnumerable<object> » puisque « DynamicClassN » (la classe générée par le « new » dans le « select ») n'est pas disponible puisque générée lors de l'exécution.

dataGridView1.DataSource = req.Cast<object>().ToList();

Si on souhaite fournir la prise en charge intégrale sous forme de méthodes d'extension, on ajoutera les méthodes suivantes à la classe « DynamicQueryable » :

public static IEnumerable<T> OrderBy<T>(this IEnumerable<T> source, string ordering, params object[] values)

{

    if (source == null) throw new ArgumentNullException("source");

    if (ordering == null) throw new ArgumentNullException("ordering");

    return OrderBy(source.AsQueryable(), ordering, values).AsEnumerable();

}

 

public static IEnumerable<T> Where<T>(this IEnumerable<T> source, string predicate, params object[] values)

{

    if (source == null) throw new ArgumentNullException("source");

    if (predicate == null) throw new ArgumentNullException("predicate");

    return Where(source.AsQueryable(), predicate, values).AsEnumerable();

}

 

 

 

public static IEnumerable<Object> Select<T>(this IEnumerable<T> source, string selector, params object[] values)

{

    if (source == null) throw new ArgumentNullException("source");

    if (selector == null) throw new ArgumentNullException("selector");

    return Select((IQueryable)source.AsQueryable(), selector, values).Cast<Object>();

}

Il ne sera plus nécessaire alors de faire appel au préalable à la méthode d'extension « AsQueryable() » et de caster par « Cast<object>() ».
J'espère que cette astuce vous invitera à découvrir les exemples fournis avec Visual Studio 2008, vous permettra d'effectuer des requêtes dynamiques sur vos listes d'objets et vous épargnera le développement d'un parseur d'expressions.
Je remercie Matthieu Mezil, Thomas Lebrun, Fabrice Romelard, Laurent Kempé pour la relecture de cet article.
Matthieu Mezil, un de nos spécialistes de Linq to Entities, MVP C# et auteur de Tech Head Brothers, a fait mention de possibles requêtes sous forme de chaîne de caractères. Je vous invite vivement à consulter ce post : Pourquoi ESQL, c'est bien
 
» Démarrer une discussion