Peut-on créer une alternative d’inclusion basée sur chaîne dans Entity Framework Core?

Sur une API, j’ai besoin d’inclure dynamic, mais EF Core ne prend pas en charge l’inclusion basée sur chaîne.

Pour cette raison, j’ai créé un mappeur qui mappe Ssortingngs aux expressions lambda ajoutées à une liste sous la forme suivante:

List<List> expressions = new List<List>(); 

Considérez les types spécifiques suivants:

 public class EFContext { public DbSet P1s { get; set; } public DbSet P2s { get; set; } public DbSet P3s { get; set; } } public class P1 { public P2 P2 { get; set; } public P3 P3 { get; set; } } public class P2 { public P3 P3 { get; set; } } public class P3 { } 

Include et ThenInclude sont normalement utilisés comme suit:

  EFContext efcontext = new EFContext(); IQueryable result = efcontext.P1s.Include(p1 => p1.P2).ThenInclude(p2 => p2.P3).Include(p1 => p1.P3); 

Ils peuvent également être utilisés de la manière suivante:

  Expression<Func> p1p2 = p1 => p1.P2; Expression<Func> p1p3 = p1 => p1.P3; Expression<Func> p2p3 = p2 => p2.P3; List<List> expressions = new List<List> { new List { p1p2, p1p3 }, new List { p2p3 } }; EFContext efcontext = new EFContext(); IIncludableQueryable q1 = EntityFrameworkQueryableExtensions.Include(efcontext.P1s, p1p2); IIncludableQueryable q2 = EntityFrameworkQueryableExtensions.ThenInclude(q1, p2p3); IIncludableQueryable q3 = EntityFrameworkQueryableExtensions.Include(q2, p1p3); result = q3.AsQueryable(); 

Le problème est que ma méthode reçoit une liste de liste d’expressions et que je n’ai que le type de base dans T:

 public static class IncludeExtensions { public static IQueryable IncludeAll(this IQueryable collection, List<List> expressions) { MethodInfo include = typeof(EntityFrameworkQueryableExtensions).GetTypeInfo().GetDeclaredMethods(nameof(EntityFrameworkQueryableExtensions.Include)).Single(mi => mi.GetParameters().Any(pi => pi.Name == "navigationPropertyPath")); MethodInfo includeAfterCollection = typeof(EntityFrameworkQueryableExtensions).GetTypeInfo().GetDeclaredMethods(nameof(EntityFrameworkQueryableExtensions.ThenInclude)).Single(mi => !mi.GetParameters()[0].ParameterType.GenericTypeArguments[1].IsGenericParameter); MethodInfo includeAfterReference = typeof(EntityFrameworkQueryableExtensions).GetTypeInfo().GetDeclaredMethods(nameof(EntityFrameworkQueryableExtensions.ThenInclude)).Single(mi => mi.GetParameters()[0].ParameterType.GenericTypeArguments[1].IsGenericParameter); foreach (List path in expressions) { Boolean start = true; foreach (Expression expression in path) { if (start) { MethodInfo method = include.MakeGenericMethod(typeof(T), ((LambdaExpression)expression).ReturnType); IIncludableQueryable result = method.Invoke(null, new Object[] { collection, expression }); start = false; } else { MethodInfo method = includeAfterReference.MakeGenericMethod(typeof(T), typeof(?), ((LambdaExpression)expression).ReturnType); IIncludableQueryable  result = method.Invoke(null, new Object[] { collection, expression }); } } } return collection; // (to be replaced by final as Queryable) } } 

Le problème principal a été la résolution des types corrects pour chacune des étapes Include et ThenInclude, ainsi que celle à utiliser par ThenInclude …

Est-ce même possible avec l’EF7 Core actuel? Quelqu’un a-t-il trouvé une solution pour inclure dynamic?

Les méthodes Include and ThenIncludeAfterReference et ThenIncludeAfterCollection font partie de la classe EntityFrameworkQueryableExtensions du référentiel EntityFramework Github .

Mettre à jour:

Depuis la version 1.1.0, l’inclusion basée sur une chaîne fait maintenant partie d’EF Core. Le problème et la solution ci-dessous sont donc obsolètes.

Réponse originale:

Exercice intéressant pour le week-end.

Solution:

J’ai fini avec la méthode d’extension suivante:

 public static class IncludeExtensions { private static readonly MethodInfo IncludeMethodInfo = typeof(EntityFrameworkQueryableExtensions).GetTypeInfo() .GetDeclaredMethods(nameof(EntityFrameworkQueryableExtensions.Include)).Single(mi => mi.GetParameters().Any(pi => pi.Name == "navigationPropertyPath")); private static readonly MethodInfo IncludeAfterCollectionMethodInfo = typeof(EntityFrameworkQueryableExtensions).GetTypeInfo() .GetDeclaredMethods(nameof(EntityFrameworkQueryableExtensions.ThenInclude)).Single(mi => !mi.GetParameters()[0].ParameterType.GenericTypeArguments[1].IsGenericParameter); private static readonly MethodInfo IncludeAfterReferenceMethodInfo = typeof(EntityFrameworkQueryableExtensions).GetTypeInfo() .GetDeclaredMethods(nameof(EntityFrameworkQueryableExtensions.ThenInclude)).Single(mi => mi.GetParameters()[0].ParameterType.GenericTypeArguments[1].IsGenericParameter); public static IQueryable Include(this IQueryable source, params ssortingng[] propertyPaths) where TEntity : class { var entityType = typeof(TEntity); object query = source; foreach (var propertyPath in propertyPaths) { Type prevPropertyType = null; foreach (var propertyName in propertyPath.Split('.')) { Type parameterType; MethodInfo method; if (prevPropertyType == null) { parameterType = entityType; method = IncludeMethodInfo; } else { parameterType = prevPropertyType; method = IncludeAfterReferenceMethodInfo; if (parameterType.IsConstructedGenericType && parameterType.GenericTypeArguments.Length == 1) { var elementType = parameterType.GenericTypeArguments[0]; var collectionType = typeof(ICollection<>).MakeGenericType(elementType); if (collectionType.IsAssignableFrom(parameterType)) { parameterType = elementType; method = IncludeAfterCollectionMethodInfo; } } } var parameter = Expression.Parameter(parameterType, "e"); var property = Expression.PropertyOrField(parameter, propertyName); if (prevPropertyType == null) method = method.MakeGenericMethod(entityType, property.Type); else method = method.MakeGenericMethod(entityType, parameter.Type, property.Type); query = method.Invoke(null, new object[] { query, Expression.Lambda(property, parameter) }); prevPropertyType = property.Type; } } return (IQueryable)query; } } 

Tester:

Modèle:

 public class P { public int Id { get; set; } public ssortingng Info { get; set; } } public class P1 : P { public P2 P2 { get; set; } public P3 P3 { get; set; } } public class P2 : P { public P4 P4 { get; set; } public ICollection P1s { get; set; } } public class P3 : P { public ICollection P1s { get; set; } } public class P4 : P { public ICollection P2s { get; set; } } public class MyDbContext : DbContext { public DbSet P1s { get; set; } public DbSet P2s { get; set; } public DbSet P3s { get; set; } public DbSet P4s { get; set; } // ... protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity().HasOne(e => e.P2).WithMany(e => e.P1s).HasForeignKey("P2Id").IsRequired(); modelBuilder.Entity().HasOne(e => e.P3).WithMany(e => e.P1s).HasForeignKey("P3Id").IsRequired(); modelBuilder.Entity().HasOne(e => e.P4).WithMany(e => e.P2s).HasForeignKey("P4Id").IsRequired(); base.OnModelCreating(modelBuilder); } } 

Usage:

 var db = new MyDbContext(); // Sample query using Include/ThenInclude var queryA = db.P3s .Include(e => e.P1s) .ThenInclude(e => e.P2) .ThenInclude(e => e.P4) .Include(e => e.P1s) .ThenInclude(e => e.P3); // The same query using ssortingng Includes var queryB = db.P3s .Include("P1s.P2.P4", "P1s.P3"); 

Comment ça marche:

Étant donné un type TEntity et un chemin de propriété de chaîne de la forme Prop1.Prop2...PropN , nous Prop1.Prop2...PropN le chemin et procédons comme suit:

Pour la première propriété, nous appelons simplement par reflection la méthode EntityFrameworkQueryableExtensions.Include :

 public static IIncludableQueryable Include ( this IQueryable source, Expression> navigationPropertyPath ) 

et stocker le résultat. Nous soaps que TEntity et TProperty sont le type de la propriété.

Pour les propriétés suivantes, c’est un peu plus complexe. Nous devons appeler l’une des surcharges ThenInclude suivantes:

 public static IIncludableQueryable ThenInclude ( this IIncludableQueryable> source, Expression> navigationPropertyPath ) 

et

 public static IIncludableQueryable ThenInclude ( this IIncludableQueryable source, Expression> navigationPropertyPath ) 

source est le résultat actuel. TEntity est la même pour tous les appels. Mais qu’est-ce que TPreviousProperty et comment décider de la méthode à appeler.

D’abord, nous utilisons une variable pour nous rappeler quelle était la TProperty dans l’appel précédent. Ensuite, nous vérifions s’il s’agit d’un type de propriété de collection et, dans l’affirmative, nous appelons le premier type de surcharge avec TPreviousProperty extrait des arguments génériques du type de collection. Sinon, appelez simplement le second type de surcharge avec ce type.

Et c’est tout. Rien d’extraordinaire, il suffit d’émuler des chaînes d’appel explicites Include / ThenInclude via reflection.

Include() basé sur des chaînes fourni avec EF Core 1.1. Je vous suggérerais d’essayer de mettre à niveau et de supprimer toutes les solutions de contournement que vous deviez append à votre code pour résoudre cette limitation.

Créer une extension “IncludeAll” sur une requête nécessitera une approche différente de celle que vous avez initialement effectuée.

EF Core interprète les expressions . Lorsqu’il voit la méthode .Include , il interprète cette expression pour créer des requêtes supplémentaires. (Voir RelationalQueryModelVisitor.cs et IncludeExpressionVisitor.cs ).

Une approche consiste à append un visiteur d’expression supplémentaire qui gère votre extension IncludeAll. Une autre approche (et probablement meilleure) consisterait à interpréter l’arbre d’expression de .IncludeAll au fichier .Includes approprié, puis à laisser EF le gérer normalement. Une implémentation de l’un ou l’autre n’est pas sortingviale et dépasse le cadre d’une réponse SO

Include () basé sur des chaînes fourni avec EF Core 1.1. Si vous conservez cette extension, vous obtiendrez l’erreur “Match ambigu trouvé”. J’ai passé une demi-journée à chercher une solution à cette erreur. Enfin, j’ai supprimé l’extension ci-dessus et l’erreur a été résolue.