Update:
Starting with v1.1.0, the string based include is now part of EF Core, so the issue and the below solution are obsolete.
Original answer:
Interesting exercise for the weekend.
Solution:
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;
}
}
Test:
Model:
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);
}
}
Usage:
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");
How it works:
Given a type TEntity
and a string property path of the form Prop1.Prop2...PropN
, we split the path and do the following:
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
)
and store the result. We know TEntity
and TProperty
is the type of the property.
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
)
and
public static IIncludableQueryable<TEntity, TProperty>
ThenInclude<TEntity, TPreviousProperty, TProperty>
(
this IIncludableQueryable<TEntity, TPreviousProperty> source,
Expression<Func<TPreviousProperty, TProperty>> navigationPropertyPath
)
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.
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.
And that's all. Nothing fancy, just emulating an explicit Include
/ ThenInclude
call chains via reflection.