可以在Entity Framework Core中创建基于String的Include替代方案吗?
在API上我需要动态包含但是EF Core不支持基于String的包含。
因此我创建了一个映射器,它将字符串映射到添加到列表中的lambda表达式:
List<List> expressions = new List<List>();
请考虑以下特定类型:
public class EFContext { public DbSet P1s { get; set; } public DbSet P2s { get; set; } public DbSet 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 result = efcontext.P1s.Include(p1 => p1.P2).ThenInclude(p2 => p2.P3).Include(p1 => p1.P3);
它们也可以通过以下方式使用:
Expression<Func> p1p2 = p1 => p1.P2; Expression<Func> p1p3 = p1 => p1.P3; Expression<Func> p2p3 = p2 => p2.P3; List<List> expressions = new List<List> { new List { p1p2, p1p3 }, new List { p2p3 } }; EFContext efcontext = new EFContext(); IIncludableQueryable q1 = EntityFrameworkQueryableExtensions.Include(efcontext.P1s, p1p2); IIncludableQueryable q2 = EntityFrameworkQueryableExtensions.ThenInclude(q1, p2p3); IIncludableQueryable q3 = EntityFrameworkQueryableExtensions.Include(q2, p1p3); result = q3.AsQueryable();
问题是我的方法收到一个表达式列表列表,我只有T中的基类型:
public static class IncludeExtensions { public static IQueryable IncludeAll(this IQueryable collection, List<List> 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 path in expressions) { Boolean start = true; foreach (Expression expression in path) { if (start) { MethodInfo method = include.MakeGenericMethod(typeof(T), ((LambdaExpression)expression).ReturnType); IIncludableQueryable result = method.Invoke(null, new Object[] { collection, expression }); start = false; } else { MethodInfo method = includeAfterReference.MakeGenericMethod(typeof(T), typeof(?), ((LambdaExpression)expression).ReturnType); IIncludableQueryable result = method.Invoke(null, new Object[] { collection, expression }); } } } return collection; // (to be replaced by final as Queryable) } }
主要问题是解决每个Include和ThenInclude步骤的正确类型,以及ThenInclude使用哪个…
目前的EF7 Core甚至可以实现这一点吗? 有人找到了动态包含的解决方案吗?
Include和ThenIncludeAfterReference以及ThenIncludeAfterCollection方法是EntityFramework Github存储库中EntityFrameworkQueryableExtensions类的一部分。
更新:
从v1.1.0开始,基于字符串的include现在是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 Include (this IQueryable 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 )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 P1s { get; set; } } public class P3 : P { public ICollection P1s { get; set; } } public class P4 : P { public ICollection P2s { get; set; } } public class MyDbContext : DbContext { public DbSet P1s { get; set; } public DbSet P2s { get; set; } public DbSet P3s { get; set; } public DbSet P4s { get; set; } // ... protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity().HasOne(e => e.P2).WithMany(e => e.P1s).HasForeignKey("P2Id").IsRequired(); modelBuilder.Entity ().HasOne(e => e.P3).WithMany(e => e.P1s).HasForeignKey("P3Id").IsRequired(); modelBuilder.Entity().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
forms的字符串属性路径,我们分割路径并执行以下操作:
对于第一个属性,我们只需通过reflection调用EntityFrameworkQueryableExtensions.Include
方法:
public static IIncludableQueryable Include ( this IQueryable source, Expression> navigationPropertyPath )
并存储结果。 我们知道TEntity
和TProperty
是该物业的类型。
对于下一个属性,它有点复杂。 我们需要调用以下ThenInclude
重载之一:
public static IIncludableQueryable ThenInclude ( this IIncludableQueryable> source, Expression> navigationPropertyPath )
和
public static IIncludableQueryable ThenInclude ( this IIncludableQueryable source, Expression> navigationPropertyPath )
source
是当前的结果。 所有呼叫的TEntity
都是一样的。 但是什么是TPreviousProperty
以及我们如何决定调用哪种方法。
好吧,首先我们使用变量来记住前一次调用中的TProperty
。 然后我们检查它是否是一个集合属性类型,如果是,我们调用从集合类型的generics参数中提取的TPreviousProperty
类型的第一个重载,否则只需用该类型调用第二个重载。
就这样。 没什么ThenInclude
,只是通过reflection模拟显式的Include
/ ThenInclude
调用链。
EF Core 1.1中附带的基于字符串的Include()
。 我建议您尝试升级并删除必须添加到代码中的任何变通方法以解决此限制。
在查询中创建“IncludeAll”扩展名需要采用与您最初完成的方法不同的方法。
EF Core进行表达式解释 。 当它看到.Include
方法时,它会将此表达式解释为创建其他查询。 (请参阅RelationalQueryModelVisitor.cs和IncludeExpressionVisitor.cs )。
一种方法是添加一个处理IncludeAll扩展的附加表达式访问者。 另一种(可能更好的)方法是将.IncludeAll中的表达式树解释为适当的.Includes
,然后让EF正常处理包含。 任何一种实现都是非平凡的,超出了SO答案的范围。
EF Core 1.1中附带的基于字符串的Include()。 如果您保留此扩展程序,则会收到错误“找到模糊匹配”。 我花了半天时间来搜索此错误的解决方案。 最后我删除了上面的扩展名并且错误已解决。