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