Une chaîne de caractères basée sur Include alternative peut-elle être créée dans Entity Framework Core?

sur une API j'ai besoin de dynamic include mais EF Core ne supporte pas les chaînes basées sur include.

A cause de cela j'ai créé un mapper qui mappe les chaînes à des expressions lambda ajoutées à une liste comme:

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

Considérer les types spécifiques suivants:

public class EFContext {
  public DbSet<P1> P1s { get; set; }
  public DbSet<P1> P2s { get; set; }
  public DbSet<P1> 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<P1> result = efcontext.P1s.Include(p1 => p1.P2).ThenInclude(p2 => p2.P3).Include(p1 => p1.P3);

ils peuvent aussi être utilisés de la manière suivante:

  Expression<Func<P1, P2>> p1p2 = p1 => p1.P2;
  Expression<Func<P1, P3>> p1p3 = p1 => p1.P3;
  Expression<Func<P2, P3>> p2p3 = p2 => p2.P3;

  List<List<Expression>> expressions = new List<List<Expression>> {
    new List<Expression> { p1p2, p1p3 },
    new List<Expression> { p2p3 }
  };

  EFContext efcontext = new EFContext();

  IIncludableQueryable<P1, P2> q1 = EntityFrameworkQueryableExtensions.Include(efcontext.P1s, p1p2);
  IIncludableQueryable<P1, P3> q2 = EntityFrameworkQueryableExtensions.ThenInclude(q1, p2p3);
  IIncludableQueryable<P1, P3> q3 = EntityFrameworkQueryableExtensions.Include(q2, p1p3);

  result = q3.AsQueryable();

le problème est que ma méthode reçoit une liste de des Expressions et je n'ai que le type de base T:

public static class IncludeExtensions<T> {

  public static IQueryable<T> IncludeAll(this IQueryable<T> collection, List<List<Expression>> 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<Expression> path in expressions) {

      Boolean start = true;

      foreach (Expression expression in path) {

        if (start) {

          MethodInfo method = include.MakeGenericMethod(typeof(T), ((LambdaExpression)expression).ReturnType);

          IIncludableQueryable<T,?> result = method.Invoke(null, new Object[] { collection, expression });

          start = false;

        } else {

          MethodInfo method = includeAfterReference.MakeGenericMethod(typeof(T), typeof(?), ((LambdaExpression)expression).ReturnType);

          IIncludableQueryable <T,?> result = method.Invoke(null, new Object[] { collection, expression });

        }           
      }
    }

    return collection; // (to be replaced by final as Queryable)

  }
}

le problème principal a été de résoudre les types corrects pour chaque inclure et puis inclure des étapes et aussi qui ensuite inclure à utiliser ...

Est-ce même possible avec le noyau EF7 actuel? Quelqu'un a trouvé une solution pour dynamique?

Include et then Includeafterreference et ThenIncludeAfterCollection méthodes font partie de Entiteframeworkqueryableextensions classe Entitefram Framework Github's référentiel.

17
demandé sur Miguel Moura 2016-07-11 20:04:31

4 réponses

mise à Jour:

à partir de v1.1.0, la chaîne basée include fait maintenant partie du noyau EF, donc la question et la solution ci-dessous sont obsolètes.

réponse Originale à cette question:

exercice Intéressant pour le week-end.

Solution:

j'ai fini avec l'extension suivante méthode:

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<TEntity> Include<TEntity>(this IQueryable<TEntity> source, params string[] 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<TEntity>)query;
    }
}

Test:

Modèle:

public class P
{
    public int Id { get; set; }
    public string 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<P1> P1s { get; set; }
}

public class P3 : P
{
    public ICollection<P1> P1s { get; set; }
}

public class P4 : P
{
    public ICollection<P2> P2s { get; set; }
}

public class MyDbContext : DbContext
{
    public DbSet<P1> P1s { get; set; }
    public DbSet<P2> P2s { get; set; }
    public DbSet<P3> P3s { get; set; }
    public DbSet<P4> P4s { get; set; }

    // ...

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<P1>().HasOne(e => e.P2).WithMany(e => e.P1s).HasForeignKey("P2Id").IsRequired();
        modelBuilder.Entity<P1>().HasOne(e => e.P3).WithMany(e => e.P1s).HasForeignKey("P3Id").IsRequired();
        modelBuilder.Entity<P2>().HasOne(e => e.P4).WithMany(e => e.P2s).HasForeignKey("P4Id").IsRequired();
        base.OnModelCreating(modelBuilder);
    }
}

Utilisation:

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 string Includes
var queryB = db.P3s
    .Include("P1s.P2.P4", "P1s.P3");

Comment cela fonctionne:

avec un type TEntity et un chemin de propriété string de la forme Prop1.Prop2...PropN, nous avons divisé le chemin d'accès et procédez de la manière suivante:

pour la première propriété nous appelons simplement par réflexion le EntityFrameworkQueryableExtensions.Include méthode:

public static IIncludableQueryable<TEntity, TProperty>
Include<TEntity, TProperty>
(
    this IQueryable<TEntity> source,
    Expression<Func<TEntity, TProperty>> navigationPropertyPath
)

et stockez le résultat. Nous savoir TEntity et TProperty est le type de la propriété.

Pour la prochaine propriétés c'est un peu plus complexe. Nous avons besoin à appeler un des éléments suivants ThenInclude surcharges:

public static IIncludableQueryable<TEntity, TProperty>
ThenInclude<TEntity, TPreviousProperty, TProperty>
(
    this IIncludableQueryable<TEntity, ICollection<TPreviousProperty>> source,
    Expression<Func<TPreviousProperty, TProperty>> navigationPropertyPath
)

et

public static IIncludableQueryable<TEntity, TProperty>
ThenInclude<TEntity, TPreviousProperty, TProperty>
(
    this IIncludableQueryable<TEntity, TPreviousProperty> source,
    Expression<Func<TPreviousProperty, TProperty>> navigationPropertyPath
)

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

Eh bien, nous utilisons d'abord une variable pour nous rappeler ce qu'était le TProperty dans l'appel précédent. Puis nous vérifions si c'est un type de propriété collection, et si oui, nous appelons la première surcharge avec TPreviousProperty type Extrait des arguments génériques du type collection, sinon appelez simplement la seconde surcharge avec ce type.

Et c'est tout. Rien d'extravagant, il suffit d'imiter un explicite Include/ThenInclude chaînes d'appel par réflexion.

16
répondu Ivan Stoev 2017-09-01 13:39:22

Chaîne Include() expédiées dans le cœur EF 1.1. Je vous suggère d'essayer de mettre à jour et de supprimer toutes les solutions de rechange que vous avez dû ajouter à votre code pour remédier à cette limitation.

3
répondu divega 2016-11-28 21:29:03

créer une extension" IncludeAll " sur la requête exigera une approche différente de ce que vous avez fait initialement.

EF Core does interprétation de l'expression. Quand il voit le .Include méthode, il interprète cette expression en créant des requêtes supplémentaires. (Voir RelationalQueryModelVisitor.cs et y compris Visiteurexpressionnel.cs).

une approche serait d'ajouter une expression visiteur supplémentaire qui gère votre IncludeAll extension. Un autre (et probablement la meilleure approche serait d'interpréter l'expression de l'arbre .IncludeAll approprié .Includes et ensuite laisser EF Gérer les inclusions normalement. La mise en œuvre de l'un ou l'autre de ces éléments n'est pas anodine et dépasse le cadre d'une réponse SO.

0
répondu natemcmaster 2016-07-14 15:54:44

String-fonction Include() est livré en EF de Base 1.1. Si vous gardez cette extension, vous obtiendrez une erreur Ambiguë "match". J'ai passé une demi-journée à chercher une solution à cette erreur. Finalement, j'ai supprimé la prolongation ci-dessus et l'erreur a été résolue.

0
répondu Sujan Kumar Kolli 2017-02-12 13:59:23