我正在尝试为Entity Framework Core 2.0编写一个存储库方法,该方法可以使用.ThenInclude处理返回属性的子集合,但我遇到了第二个表达式的问题。这是.Include的一种工作方法,它将返回你的实体的子属性(你提供一个lambdas列表)。
public T GetSingle(Expression<Func<T, bool>> predicate, params Expression<Func<T, object>>[] includeProperties)
{
IQueryable<T> query = _context.Set<T>();
foreach (var includeProperty in includeProperties)
{
query = query.Include(includeProperty);
}
return query.Where(predicate).FirstOrDefault();
}
现在,我尝试编写一个方法,该方法将采用两个表达式的元组并将其提供给.Include(a =&gt; a.someChild).ThenInclude(b =&gt; b.aChildOfSomeChild)链。这不是一个完美的解决方案,因为它只处理一个孩子的一个孩子,但它是一个开始。
public T GetSingle(Expression<Func<T, bool>> predicate, params Tuple<Expression<Func<T, object>>, Expression<Func<T, object>>>[] includeProperties)
{
IQueryable<T> query = _context.Set<T>();
foreach (var includeProperty in includeProperties)
{
query = query.Include(includeProperty.Item1).ThenInclude(includeProperty.Item2);
}
return query.Where(predicate).FirstOrDefault();
}
Intellisense返回错误说&#34;无法根据用法推断出类型,请尝试明确指定类型&#34;。我有一种感觉,因为Item2中的表达式需要被分类为与Item1有某种关系,因为它需要知道它具有的子关系。
编写这样的方法有什么想法或更好的技巧吗?
答案 0 :(得分:19)
我在网上找到了这个存储库方法,它完全符合我的要求。 Yared的回答很好,但并不是一直都有。
/// <summary>
/// Gets the first or default entity based on a predicate, orderby delegate and include delegate. This method default no-tracking query.
/// </summary>
/// <param name="selector">The selector for projection.</param>
/// <param name="predicate">A function to test each element for a condition.</param>
/// <param name="orderBy">A function to order elements.</param>
/// <param name="include">A function to include navigation properties</param>
/// <param name="disableTracking"><c>True</c> to disable changing tracking; otherwise, <c>false</c>. Default to <c>true</c>.</param>
/// <returns>An <see cref="IPagedList{TEntity}"/> that contains elements that satisfy the condition specified by <paramref name="predicate"/>.</returns>
/// <remarks>This method default no-tracking query.</remarks>
public TResult GetFirstOrDefault<TResult>(Expression<Func<TEntity, TResult>> selector,
Expression<Func<TEntity, bool>> predicate = null,
Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
Func<IQueryable<TEntity>, IIncludableQueryable<TEntity, object>> include = null,
bool disableTracking = true)
{
IQueryable<TEntity> query = _dbSet;
if (disableTracking)
{
query = query.AsNoTracking();
}
if (include != null)
{
query = include(query);
}
if (predicate != null)
{
query = query.Where(predicate);
}
if (orderBy != null)
{
return orderBy(query).Select(selector).FirstOrDefault();
}
else
{
return query.Select(selector).FirstOrDefault();
}
}
用法:
var affiliate = await affiliateRepository.GetFirstOrDefaultAsync(
predicate: b => b.Id == id,
include: source => source
.Include(a => a.Branches)
.ThenInclude(a => a.Emails)
.Include(a => a.Branches)
.ThenInclude(a => a.Phones));
答案 1 :(得分:5)
我遇到了同样的问题,因为EF Core不支持延迟加载,但我试图通过以下方式获得解决方法:
首先创建一个属性类,以便从给定类的其他属性中标记所需的导航属性。
[AttributeUsage(AttributeTargets.Property, Inherited = false)]
public class NavigationPropertyAttribute : Attribute
{
public NavigationPropertyAttribute()
{
}
}
过滤掉导航属性的扩展方法,并使用基于字符串的预先加载来应用Include / ThenInclude。
public static class DbContextHelper
{
public static Func<IQueryable<T>, IQueryable<T>> GetNavigations<T>() where T : BaseEntity
{
var type = typeof(T);
var navigationProperties = new List<string>();
//get navigation properties
GetNavigationProperties(type, type, string.Empty, navigationProperties);
Func<IQueryable<T>, IQueryable<T>> includes = ( query => {
return navigationProperties.Aggregate(query, (current, inc) => current.Include(inc));
});
return includes;
}
private static void GetNavigationProperties(Type baseType, Type type, string parentPropertyName, IList<string> accumulator)
{
//get navigation properties
var properties = type.GetProperties();
var navigationPropertyInfoList = properties.Where(prop => prop.IsDefined(typeof(NavigationPropertyAttribute)));
foreach (PropertyInfo prop in navigationPropertyInfoList)
{
var propertyType = prop.PropertyType;
var elementType = propertyType.GetTypeInfo().IsGenericType ? propertyType.GetGenericArguments()[0] : propertyType;
//Prepare navigation property in {parentPropertyName}.{propertyName} format and push into accumulator
var properyName = string.Format("{0}{1}{2}", parentPropertyName, string.IsNullOrEmpty(parentPropertyName) ? string.Empty : ".", prop.Name);
accumulator.Add(properyName);
//Skip recursion of propert has JsonIgnore attribute or current property type is the same as baseType
var isJsonIgnored = prop.IsDefined(typeof(JsonIgnoreAttribute));
if(!isJsonIgnored && elementType != baseType){
GetNavigationProperties(baseType, elementType, properyName, accumulator);
}
}
}
}
实施NavigationPropertyAttribute
public class A : BaseEntity{
public string Prop{ get; set; }
}
public class B : BaseEntity{
[NavigationProperty]
public virtual A A{ get; set; }
}
public class C : BaseEntity{
[NavigationProperty]
public virtual B B{ get; set; }
}
存储库中的用法
public async Task<T> GetAsync(Expression<Func<T, bool>> predicate)
{
Func<IQueryable<T>, IQueryable<T>> includes = DbContextHelper.GetNavigations<T>();
IQueryable<T> query = _context.Set<T>();
if (includes != null)
{
query = includes(query);
}
var entity = await query.FirstOrDefaultAsync(predicate);
return entity;
}
样本类C的Json结果将是:
{
"B" : {
"A" : {
"Prop" : "SOME_VALUE"
}
}
}
答案 2 :(得分:0)
在EF6中,我们可以这样写:
query.Include(t => t.Navigation1, t => t.Navigation2.Select(x => x.Child1));
这是完美而简单的。我们可以将其公开在存储库中,而无需将引用从EF程序集拖到其他项目中。
此功能已从EF Core中删除,但是由于EF6是开源的,因此可以轻松提取用于转换路径中的lambda表达式的方法,以便在EF Core中使用。
这是完整的扩展方法。
/// <summary>
/// Provides extension methods to the <see cref="Expression" /> class.
/// </summary>
public static class ExpressionExtensions
{
/// <summary>
/// Converts the property accessor lambda expression to a textual representation of it's path. <br />
/// The textual representation consists of the properties that the expression access flattened and separated by a dot character (".").
/// </summary>
/// <param name="expression">The property selector expression.</param>
/// <returns>The extracted textual representation of the expression's path.</returns>
public static string AsPath(this LambdaExpression expression)
{
if (expression == null)
return null;
TryParsePath(expression.Body, out var path);
return path;
}
/// <summary>
/// Recursively parses an expression tree representing a property accessor to extract a textual representation of it's path. <br />
/// The textual representation consists of the properties accessed by the expression tree flattened and separated by a dot character (".").
/// </summary>
/// <param name="expression">The expression tree to parse.</param>
/// <param name="path">The extracted textual representation of the expression's path.</param>
/// <returns>True if the parse operation succeeds; otherwise, false.</returns>
private static bool TryParsePath(Expression expression, out string path)
{
var noConvertExp = RemoveConvertOperations(expression);
path = null;
switch (noConvertExp)
{
case MemberExpression memberExpression:
{
var currentPart = memberExpression.Member.Name;
if (!TryParsePath(memberExpression.Expression, out var parentPart))
return false;
path = string.IsNullOrEmpty(parentPart) ? currentPart : string.Concat(parentPart, ".", currentPart);
break;
}
case MethodCallExpression callExpression:
switch (callExpression.Method.Name)
{
case nameof(Queryable.Select) when callExpression.Arguments.Count == 2:
{
if (!TryParsePath(callExpression.Arguments[0], out var parentPart))
return false;
if (string.IsNullOrEmpty(parentPart))
return false;
if (!(callExpression.Arguments[1] is LambdaExpression subExpression))
return false;
if (!TryParsePath(subExpression.Body, out var currentPart))
return false;
if (string.IsNullOrEmpty(parentPart))
return false;
path = string.Concat(parentPart, ".", currentPart);
return true;
}
case nameof(Queryable.Where):
throw new NotSupportedException("Filtering an Include expression is not supported");
case nameof(Queryable.OrderBy):
case nameof(Queryable.OrderByDescending):
throw new NotSupportedException("Ordering an Include expression is not supported");
default:
return false;
}
}
return true;
}
/// <summary>
/// Removes all casts or conversion operations from the nodes of the provided <see cref="Expression" />.
/// Used to prevent type boxing when manipulating expression trees.
/// </summary>
/// <param name="expression">The expression to remove the conversion operations.</param>
/// <returns>The expression without conversion or cast operations.</returns>
private static Expression RemoveConvertOperations(Expression expression)
{
while (expression.NodeType == ExpressionType.Convert || expression.NodeType == ExpressionType.ConvertChecked)
expression = ((UnaryExpression)expression).Operand;
return expression;
}
}
然后您可以像这样使用它(将其放入QueryableExtensions
类或类似的东西中):
/// <summary>
/// Specifies related entities to include in the query result.
/// </summary>
/// <typeparam name="T">The type of entity being queried.</typeparam>
/// <param name="source">The source <see cref="IQueryable{T}" /> on which to call Include.</param>
/// <param name="paths">The lambda expressions representing the paths to include.</param>
/// <returns>A new <see cref="IQueryable{T}" /> with the defined query path.</returns>
internal static IQueryable<T> Include<T>(this IQueryable<T> source, params Expression<Func<T, object>>[] paths)
{
if (paths != null)
source = paths.Aggregate(source, (current, include) => current.Include(include.AsPath()));
return source;
}
然后通常像在EF6中一样在存储库中调用它:
query.Include(t => t.Navigation1, t => t.Navigation2.Select(x => x.Child1));
参考文献:
How to pass lambda 'include' with multiple levels in Entity Framework Core?