2016-07-11 1 views
16

В API мне нужно динамическое включение, но EF Core не поддерживает String.Можно ли создать альтернативный вариант String в Entity Framework Core?

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

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

Включать и 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) 

    } 
} 

Основной проблемой было разрешения правильных типов для каждого Включать и ThenInclude, а также, что ThenInclude для использования ...

Возможно ли это с нынешним ядром EF7? Кто-нибудь нашел решение для динамического включения?

В Include и ThenIncludeAfterReference и ThenIncludeAfterCollection методы являются частью класса EntityFrameworkQueryableExtensions в EntityFramework Github's хранилище.

+0

Можете ли вы предоставить больше контекста? Как вы строите эти списки выражений, почему они являются списком списков, всегда ли они являются одиночными атрибутами доступа к ресурсам lambdas и т. Д. Или лучше какой-то образец строк, которые вы обрабатываете, например «P2.P3» и «P3» или? –

ответ

15

Update:

Начиная с v1.1.0, строка на основе включают теперь является частью EF Ядра, поэтому вопрос и ниже решения устарели.

Оригинальный ответ:

Интересное упражнение на выходные.

Решение:

Я закончил с следующим методом расширения:

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 через отражение.

+0

Спасибо ... Это отлично работает. По крайней мере, для тестов, которые я делал. Просто отмечен как ответ. –

0

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

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

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

+0

Я пошел на этот подход как самый простой способ сделать эту работу. В принципе, я определяю «mapper», который говорит: p1 => p1.P2 для «P2», p1 => p1.P3 для «P3» ... Итак, когда я получаю строку expand = «P2, P3», я могу получить выражения p1 => p1.P2 и p1 = > p1.P3 ... Тогда мне просто нужно запускать с ними вместе ... Но это была моя проблема ... –

3

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

0

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

Смежные вопросы