Может ли альтернатива String based Include быть создана в Entity Framework Core?

В API мне нужен динамический include, но EF Core не поддерживает String.

Из-за этого я создал mapper, который отображает строки в лямбда-выражения, добавленные в список, как:

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? Кто-нибудь нашел решение для динамического включения?

Включить и ThenIncludeAfterReference и ThenIncludeAfterCollection являются частью EntityFrameworkQueryableExtensions класса в репозитории EntityFramework Github.

Ответы

Ответ 1

Обновление:

Начиная с v1.1.0, основанный на строках, теперь является частью 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");

Как это работает:

Учитывая тип TEntity и путь свойства строки формы Prop1.Prop2...PropN, мы разделим путь и сделаем следующее:

Для первого свойства мы просто вызываем через отражение метод EntityFrameworkQueryableExtensions.Include:

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

и сохраните результат. Мы знаем, что TEntity и TProperty - тип свойства.

Для следующих свойств это немного сложнее. Нам нужно вызвать одну из следующих перегрузок 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, извлеченным из общих аргументов типа коллекции, иначе просто вызываем вторую перегрузку с этим типом.

И это все. Ничего необычного, просто эмулируя явные цепочки вызовов Include/ThenInclude через отражение.

Ответ 2

String-based Include() поставляется в EF Core 1.1. Я предлагаю вам попробовать обновить и устранить любые обходные пути, которые вы должны добавить в свой код, чтобы устранить это ограничение.

Ответ 3

Создание расширения "IncludeAll" для запроса потребует другого подхода от того, что вы изначально сделали.

EF Core выполняет интерпретацию выражений. Когда он видит метод .Include, он интерпретирует это выражение для создания дополнительных запросов. (См. RelationalQueryModelVisitor.cs и IncludeExpressionVisitor.cs).

Один из подходов состоит в том, чтобы добавить дополнительного посетителя выражения, который обрабатывает расширение IncludeAll. Другой (и, вероятно, лучший) подход заключался бы в том, чтобы интерпретировать дерево выражений от .IncludeAll до соответствующего .Includes, а затем позволить EF-дескриптору включить нормально. Реализация либо нетривиальна, либо выходит за рамки ответа SO.

Ответ 4

String-based Include(), поставляемый в EF Core 1.1. Если вы сохраните это расширение, вы получите сообщение об ошибке "Незначительное совпадение найдено". Я потратил полдня на поиск решения этой ошибки. Наконец, я удалился над расширением, и ошибка была решена.