2016-07-11 23 views
16

Su un'API ho bisogno di includere dinamico ma EF Core non supporta l'inclusione basata su stringhe.È possibile creare un'alternativa basata su stringa basata su Entity Framework Core?

A causa di questo ho creato un mapper che mappa le stringhe di lambda espressioni aggiunti a un elenco come:

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

considerare i seguenti tipi specifici:

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 { } 

includere e ThenInclude sono normalmente utilizzati come segue:

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

Possono essere utilizzati anche i seguenti modo:

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(); 

Il problema è il mio metodo riceve un elenco di elenco di espressioni e ho solo il tipo di base a 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) 

    } 
} 

Il problema principale è stato risolvendo i tipi corretti per ogni Includi e poiIncludere i passaggi e anche quale ThenInclude utilizzare ...

E 'anche possibile con l'attuale core EF7? Qualcuno ha trovato una soluzione per l'inclusione dinamica?

I Include e ThenIncludeAfterReference e ThenIncludeAfterCollection metodi sono parte di EntityFrameworkQueryableExtensions classe EntityFramework Github's repository.

+0

Potete fornire più contesto? Come si costruiscono questi elenchi di espressioni, perché sono elenchi di elenchi, sono sempre singoli accessor di proprietà lambda ecc. O meglio alcuni esempi di stringhe che si stanno elaborando, come "P2.P3" e "P3" o? –

risposta

15

Aggiornamento:

Partendo v1.1.0, la corda a base di includere è ora parte di EF core, in modo che il problema e la soluzione qui di seguito sono obsolete.

risposta originale:

esercizio interessante per il fine settimana.

Soluzione:

ho finito con il seguente metodo di estensione:

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; 
    } 
} 

prova:

Modello:

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); 
    } 
} 

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

Come funziona:

dato un tipo TEntity e un percorso di proprietà stringa della forma Prop1.Prop2...PropN, abbiamo diviso il percorso e procedere come segue:

Per la prima proprietà chiamiamo semplicemente tramite riflessione il metodo EntityFrameworkQueryableExtensions.Include:

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

e memorizzare il risultato. Sappiamo che TEntity e TProperty è il tipo di proprietà.

Per le proprietà successive è un po 'più complesso. Abbiamo bisogno di chiamare uno dei seguenti ThenInclude sovraccarichi:

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

e

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

source è il risultato corrente. TEntity è lo stesso per tutte le chiamate. Ma cos'è TPreviousProperty e come decidiamo quale metodo chiamare.

Bene, per prima cosa usiamo una variabile per ricordare quale era il TProperty nella precedente chiamata. Quindi controlliamo se si tratta di un tipo di proprietà della raccolta e, in caso affermativo, chiamiamo il primo overload con tipo TPreviousProperty estratto dagli argomenti generici del tipo di raccolta, altrimenti chiamiamo semplicemente il secondo overload con quel tipo.

E questo è tutto. Niente di speciale, basta emulare una catena di chiamate esplicita Include/ThenInclude tramite riflessione.

+0

Grazie ... Funziona bene. Almeno per i test che ho fatto. Appena segnato come risposta. –

0

La creazione di un'estensione "IncludeAll" sulla query richiede un approccio diverso da quello che è stato inizialmente fatto.

EF Core expression interpretation. Quando vede il metodo .Include, interpreta questa espressione nella creazione di ulteriori query. (Vedi RelationalQueryModelVisitor.cs e IncludeExpressionVisitor.cs).

Un approccio potrebbe essere quello di aggiungere un visitatore di espressioni aggiuntive che gestisca l'estensione IncludeAll. Un altro (e probabilmente migliore) approccio sarebbe interpretare l'albero delle espressioni da .IncludeAll all'appropriato .Includes e quindi lasciare che EF gestisca normalmente gli include. Un'implementazione di entrambi è non banale e va oltre lo scopo di una risposta SO.

+0

Ho scelto questo approccio come il modo più semplice per farlo funzionare. Fondamentalmente, sto definendo un "mapper" che dice: p1 => p1.P2 è per "P2", p1 => p1.P3 è per "P3" ... Quindi quando ricevo una stringa expand = "P2, P3" sono in grado di ottenere le espressioni p1 => p1.P2 e p1 = > p1.P3 ... Quindi ho solo bisogno di eseguire gli include con loro ... Ma quello è stato il mio problema ... –

3

Include() basato su stringa fornito in EF Core 1.1. Ti suggerirei di provare ad aggiornare e rimuovere qualsiasi soluzione alternativa che dovevi aggiungere al tuo codice per risolvere questa limitazione.

0

Includi basati su stringa() forniti in EF Core 1.1. Se mantieni questa estensione riceverai l'errore "Trovata corrispondenza ambigua". Ho trascorso mezza giornata a cercare la soluzione per questo errore. Alla fine ho rimosso l'estensione precedente e l'errore è stato risolto.

Problemi correlati