Include typé et Entity Framework
Par défaut, lorsque l’on fait une requête Entity Framework, les objets connexes ne sont pas chargés, il faut explicitement indiquer que l’on souhaite charger ces objets.
Il existe 2 solutions pour charger les objets connexes. Soit on active le lazy-loading, ainsi les objets seront chargés lors de l’accès à la propriété. Soit on indique à Entity Framework quelles sont les objets connexes à charger, cela se fait via la méthode Include de ObjectQuery. Cette méthode prend un paramètre de type String indiquant quelles sont les propriétés à charger en même temps que l’objet.
Ainsi, si l’on souhaite charger les clients en même temps qu’une commande, il faudra inclure la propriété Customer, le code ci-dessous illustre mon propos.
var q = entities.Orders.Include("Customer").Where(o => o.CustomerId == 3);
foreach(var order in q){
// order.Customer ne sera ici pas null
}
Le problème de ce bout de code est que si un jour on change le nom de la propriété Customer et que l’on oublie de changer le code du Include, il n’y aura aucune erreur de compilation, le code plantera avec une exception de type “InvalidOperationException” lors de l’exécution :
System.InvalidOperationException: A specified Include path is not valid. The EntityType 'XXX.Data.Entities.Order' does not declare a navigation property with the name 'Cuuustomer'.
Afin de palier à ce problème, il est possible de créer sa propre méthode d’extension Include qui prend non pas une chaine de caractère mais une lambda expression permettant de spécifier la propriété que l’on veut inclure. Le code ci-dessus devient alors :
var q = entities.Orders.Include(o => o.Customer).Where(o => o.CustomerId == 3);
foreach(var order in q){
// order.Customer ne sera ici pas null
}
Ainsi, le nom de la propriété
Customer sera vérifié lors de la compilation, ce qui limite donc grandement les erreurs.
Si l’on veut inclure plus d’une profondeur d’objet, par exemple, si l’on souhaite inclure les clients d’une commande ainsi que ses adresses, Entity Framework propose de séparer les propriétés par des points. Le bout de code suivant fonctionnera :
var q = entities.Orders.Include("Customer.Addresses").Where(o => o.CustomerId == 3);
foreach(var order in q){
// order.Customer.Addresses ne sera ici pas null
}
Dans ce cas, il est assez simple de faire une méthode d’extension pour ce cas particulier, on pourrait alors écrire ceci :
var q = entities.Orders.Include(o => o.Customer.Addresses).Where(o => o.CustomerId == 3);
foreach(var order in q){
// order.Customer.Addresses ne sera ici pas null
}
Le problème se pose lorsque l’on veut inclure une propriété après une collection, par exemple lorsque l’on veut inclure les pays de toutes les adresses du client d’une commande. Dans ce cas, puisqu’on travaille sur une collection d’adresses, il n’est pas possible de retomber sur un élément pour accéder à une propriété précise. Une solution courante est d’utiliser la méthode First, lors de la conversion de l’expression en String, les appels à la méthode First ne seront pas pris en compte. Exemple :
var q = entities.Orders.Include(o => o.Customer.Addresses.First().Country).Where(o => o.CustomerId == 3);
foreach(var order in q){
foreach(var address in order.Customer.Addresses){
// address.Coutnry ne sera ici pas null
}
}
Une autre solution possible serait d’avoir plusieurs paramètre sur notre include et que chaque paramètre soit une lambda expression qui prend un entrée soit le type de l’argument précédent, soit le type de l’élément de la collection si l’élément précédent était une collection. Ainsi, on pourrait écrire notre include de cette façon :
var q = entities.Orders.Include(o => o.Customer, c => Addresses, a => a.Country).Where(o => o.CustomerId == 3);
foreach(var order in q){
foreach(var address in order.Customer.Addresses){
// address.Coutnry ne sera ici pas null
}
}
Avec cette solution, il existe une multitude de signature possible. Pour générer ces différentes signatures, j’ai utilisé un fichier T4.
Mon fichier T4 permet de générer des signatures avec une profondeur de 6, cela veut dire qu’il sera possible de mettre 6 paramètres à notre fonction Include. Une profondeur de 6 nous génère 64 signatures différents, au-delà, Visual Studio a beaucoup de mal à afficher l’intellisense … Cette limitation est acceptable, avoir des includes a plus de 6 niveaux de profondeur est déjà important, le SQL généré risque d’être déjà très complexe. De plus, lorsque l’on ne travaille pas sur une collection, on peut réunir les paramètres, l’include ci-dessus peut donc s’écrire ainsi :
var q = entities.Orders.Include(o => o.Customer.Addresses, a => a.Country).Where(o => o.CustomerId == 3);
foreach(var order in q){
foreach(var address in order.Customer.Addresses){
// address.Coutnry ne sera ici pas null
}
}
Le code de mon fichier T4 se trouve sur csharpfr : Entity Framework : Avoir un includé typé. Pour l’utiliser, il suffit d’inclure le fichier T4 et le fichier ExpressionExtensions.cs dans votre solution.
Pour ma part, je trouve cette solution assez élégante, le seul problème est le nombre de signature assez importante que cela génère. Et vous, qu’en pensez-vous ? Préférez-vous cette solution à la solution du .First() ? Avez-vous une autre solution ? Comment faites-vous vos includes ?