可以在Entity Framework Core中創建基於String的Include替代方案嗎?

c# entity-framework-core

在API上我需要動態包含但是EF Core不支持基於String的包含。

因此我創建了一個映射器,它將字符串映射到添加到列表中的lambda表達式:

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

請考慮以下特定類型:

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和ThenInclude通常使用如下:

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

它們也可以通過以下方式使用:

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

問題是我的方法收到一個表達式列表列表,我只有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)

  }
}

主要問題是解決每個Include和ThenInclude步驟的正確類型,以及ThenInclude使用哪個...

目前的EF7 Core甚至可以實現這一點嗎?有人找到了動態包含的解決方案嗎?

IncludeThenIncludeAfterReference以及ThenIncludeAfterCollection方法是EntityFramework Github存儲庫中EntityFrameworkQueryableExtensions類的一部分。

一般承認的答案

更新:

從v1.1.0開始,基於字符串的include現在是EF Core的一部分,因此問題和以下解決方案已過時。

原始答案:

週末有趣的運動。

解:

我最終得到了以下擴展方法:

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

測試:

模型:

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

用法:

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

怎麼運行的:

給定類型為TEntityProp1.Prop2...PropN形式的字符串屬性路徑,我們分割路徑並執行以下操作:

對於第一個屬性,我們只需通過反射調用EntityFrameworkQueryableExtensions.Include方法:

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

並存儲結果。我們知道TEntityTProperty是該物業的類型。

對於下一個屬性,它有點複雜。我們需要調用以下ThenInclude重載之一:

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

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

source是當前的結果。所有呼叫的TEntity都是一樣的。但是什麼是TPreviousProperty以及我們如何決定調用哪種方法。

好吧,首先我們使用變量來記住前一次調用中的TProperty 。然後我們檢查它是否是一個集合屬性類型,如果是,我們調用從集合類型的泛型參數中提取的TPreviousProperty類型的第一個重載,否則只需用該類型調用第二個重載。

就這樣。沒什麼ThenInclude ,只是通過反射模擬顯式的Include / ThenInclude調用鏈。


熱門答案

EF Core 1.1中附帶的基於字符串的Include() 。我建議您嘗試升級並刪除必須添加到代碼中的任何變通方法以解決此限制。



Related

許可下: CC-BY-SA with attribution
不隸屬於 Stack Overflow
許可下: CC-BY-SA with attribution
不隸屬於 Stack Overflow