可以在 Entity Framework Core 中创建基于字符串的 Include 替代方案吗? [英] Can a String based Include alternative be created in Entity Framework Core?

查看:22
本文介绍了可以在 Entity Framework Core 中创建基于字符串的 Include 替代方案吗?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

在 API 上,我需要动态包含,但 EF Core 不支持基于字符串的包含.

On an API I need dynamic include but EF Core does not support String based include.

因此,我创建了一个映射器,它将字符串映射到添加到列表中的 lambda 表达式:

Because of this I created a mapper which maps Strings to lambda expressions added to a list as:

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

Include and ThenInclude are normally used as follows:

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

它们也可以通过以下方式使用:

They can also be used the following way:

  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 中只有基本类型:

The problem is my method receives a List of List of Expressions and I only have the base type in 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 ...

The main problem has been resolving the correct types for each Include and ThenInclude steps and also which ThenInclude to use ...

这在当前的 EF7 Core 中是否可行?有人找到了动态包含的解决方案吗?

Is this even possible with current EF7 Core? Did someone found a solution for dynamic Include?

包括ThenIncludeAfterReferenceThenIncludeAfterCollection 方法是EntityFramework Github 的 存储库中的 Microsoft.EntityFrameworkCore/EntityFrameworkQueryableExtensions.cs">EntityFrameworkQueryableExtensions 类.

The Include and ThenIncludeAfterReference and ThenIncludeAfterCollection methods are part of EntityFrameworkQueryableExtensions class in EntityFramework Github's repository.

推荐答案

更新:

从 v1.1.0 开始,基于字符串的包含现在是 EF Core 的一部分,因此该问题和以下解决方案已过时.

Starting with v1.1.0, the string based include is now part of EF Core, so the issue and the below solution are obsolete.

原答案:

周末有趣的练习.

解决方案:

我最终得到了以下扩展方法:

I've ended up with the following extension method:

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

工作原理:

给定类型 TEntity 和形式为 Prop1.Prop2...PropN 的字符串属性路径,我们拆分路径并执行以下操作:

Given a type TEntity and a string property path of the form Prop1.Prop2...PropN, we split the path and do the following:

对于第一个属性,我们只是通过反射调用 EntityFrameworkQueryableExtensions.Include 方法:

For the first property we just call via reflection the EntityFrameworkQueryableExtensions.Include method:

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

并存储结果.我们知道 TEntityTProperty 是属性的类型.

and store the result. We know TEntity and TProperty is the type of the property.

对于下一个属性,它有点复杂.我们需要调用以下 ThenInclude 重载之一:

For the next properties it's a bit more complex. We need to call one of the following ThenInclude overloads:

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 以及我们如何决定调用哪个方法.

source is the current result. TEntity is one and the same for all calls. But what is TPreviousProperty and how we decide which method to call.

好吧,首先我们使用一个变量来记住上一次调用中的 TProperty 是什么.然后我们检查它是否是集合属性类型,如果是,我们使用从集合类型的泛型参数中提取的 TPreviousProperty 类型调用第一个重载,否则简单地调用该类型的第二个重载.

Well, first we use a variable to remember what was the TProperty in the previous call. Then we check if it is a collection property type, and if yes, we call the first overload with TPreviousProperty type extracted from the generic arguments of the collection type, otherwise simply call the second overload with that type.

仅此而已.没什么特别的,只是通过反射模拟显式的 Include/ThenInclude 调用链.

And that's all. Nothing fancy, just emulating an explicit Include / ThenInclude call chains via reflection.

这篇关于可以在 Entity Framework Core 中创建基于字符串的 Include 替代方案吗?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
相关文章
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆