如何在ef core的子列表对象上创建动态order语句



我正在尝试根据排序方向动态创建顺序

public class Item
{   
public Guid ItemId {get; set;}
public string ItemName { get; set; }
public List<ItemProductType> ItemProductTypes {get; set;}
}
public class ItemProductType
{
public Guid ItemId {get; set;}
public Guid ProductTypeId {get; set; }
public Item Item { get; set; }
public ProductType ProductType { get; set; }
}
public class ProductType
{   
public Guid ProductTypeId {get; set;}
public string ProductTypeName { get; set; }
public List<ItemProductType> ItemProductTypes {get; set;}
}

我需要在item表上创建一个order语句,如

_context.Items.Include(a => a.ItemProductTypes)
.ThenInclude(a => a.ProductType)        
.OrderBy(a => 
a.ItemProductTypes.OrderBy(b => b.ProductType.ProductTypeName)
.Select(c => c.ProductType.ProductTypeName)
.FirstOrDefault()
).ToListAsync();

上面的代码是工作的,但我需要改变顺序升序或降序基于Sortdirection从ColumnHeader点击。

我尝试在Queryable上使用OrderBySortDynamic扩展方法来动态获取顺序或有序降序

public static IQueryable<t> OrderBySortDynamic<t, TKey>(this IQueryable<t> query, Expression<Func<t, TKey>> keySelector, bool sortDescending) 
{ 
string command = "OrderBy"; 
if (sortDescending) 
{ 
command = "OrderByDescending"; 
} 
Expression resultExpression = Expression.Call( 
typeof(Queryable), 
command, 
new[] { typeof(t), keySelector.ReturnType },
query.Expression, 
Expression.Quote(keySelector));
return query.Provider.CreateQuery<t>(resultExpression); 
} 

我如何创建一个扩展方法来动态执行orderderby的子导航属性,其中a.t itemproducttypes .orderderby(b =祝辞b.ProductType.ProductTypeName)。我尝试用Ienumerable更新上面的扩展方法,但它不起作用。

首先要考虑的是自定义扩展方法不能在表达式树中使用。如这里

.OrderBy(a => 
a.ItemProductTypes.OrderBy(b => b.ProductType.ProductTypeName)
.Select(c => c.ProductType.ProductTypeName)
.FirstOrDefault()
)

你可以用自定义方法代替外部的OrderBy,但不能用内部的-a =>之后的所有东西都是表达式树的一部分,必须是众所周知的方法/属性,因为表达式树中的方法/属性通常不会被调用,而是由查询表达式转换器处理。

话虽如此,让我们看看我们能做些什么来处理你的问题。外部OrderBy可通过自定义扩展方式替换。但你是过于复杂并不是真正的动态——它所做的是调用OrderByOrderByDescending传递准备提供参数。因此,它可以简化为(基本上与OrderBy相同的签名,但增加了bool参数):

public static IOrderedQueryable<T> OrderBy<T, TKey>(
this IQueryable<T> source,
Expression<Func<T, TKey>> keySelector,
bool descending
) => descending ? source.OrderByDescending(keySelector) : source.OrderBy(keySelector);

如果选择器使用单个值(直接或间接通过引用导航属性),这将工作。为了处理内部的OrderBy,您需要另一个自定义方法来接收必要的部分作为参数。

在这个表达式中

a => a.ItemProductTypes
.OrderBy(b => b.ProductType.ProductTypeName)
.Select(c => c.ProductType.ProductTypeName)
.FirstOrDefault()
)

可自定义部分为a.ItemProductTypes,用于从外部"对象"中选择内部可枚举对象,b => b.ProductType.ProductTypeName用于选择内部键进行排序。

所以第二个自定义方法是这样的:
public static IOrderedQueryable<TOuter> OrderBy<TOuter, TInner, TKey>(
this IQueryable<TOuter> source,
Expression<Func<TOuter, IEnumerable<TInner>>> innerSelector,
Expression<Func<TInner, TKey>> innerKeySelector,
bool descending
)
{
var innerOrderBy = Expression.Call(
typeof(Enumerable),
descending ? nameof(Enumerable.OrderByDescending) : nameof(Enumerable.OrderBy),
new[] { typeof(TInner), typeof(TKey) },
innerSelector.Body,
innerKeySelector
);
var innerSelect = Expression.Call(
typeof(Enumerable),
nameof(Enumerable.Select),
new[] { typeof(TInner), typeof(TKey) },
innerOrderBy,
innerKeySelector
);
var innerFirstOrDefault = Expression.Call(
typeof(Enumerable),
nameof(Enumerable.FirstOrDefault),
new[] { typeof(TKey) },
innerSelect
);
var outerKeySelector = Expression.Lambda<Func<TOuter, TKey>>(
innerFirstOrDefault,
innerSelector.Parameters
);
return source.OrderBy(outerKeySelector, descending);
}

这里的实现必须使用Expression类方法来组成一个键选择器,以传递给上一个自定义扩展方法。您只需要知道这些方法是静态的,包括包含类、方法名、泛型类型参数和形参。并且它们的调用顺序与c#扩展方法语法糖(即

)所提供的顺序相反。
OrderBy(...).Select(...).FirstOrDefault()

FirstOrDefault(Select(OrderBy(...), ...))

现在您可以在查询中使用第二个扩展方法:

var query = _context.Items
.Include(a => a.ItemProductTypes)
.ThenInclude(a => a.ProductType)
.OrderBy(
a => a.ItemProductTypes,
b => b.ProductType.ProductTypeName,
descending: true // pass your criteria here
);

的另一个可能的解决方案是编写查询升序(如示例中),然后使用自定义ExpressionVisitor替换所有OrderBy调用OrderByDescendingstring.Replace之类,但表情。

下面是上面的一个示例实现:

public static IQueryable<T> SwitchOrderBy<T>(
this IQueryable<T> source,
bool descending
)
{
if (!descending) return source;
var expression = new OrderBySwitcher().Visit(source.Expression);
if (source.Expression == expression) return source;
return source.Provider.CreateQuery<T>(expression);
}
class OrderBySwitcher : ExpressionVisitor
{
protected override Expression VisitMethodCall(MethodCallExpression node)
{
if ((node.Method.DeclaringType == typeof(Enumerable)
|| node.Method.DeclaringType == typeof(Queryable))
&& node.Method.Name == nameof(Enumerable.OrderBy))
{
var args = new Expression[node.Arguments.Count];
for (int i = 0; i < args.Length; i++)
args[i] = Visit(node.Arguments[i]);
return Expression.Call(
node.Method.DeclaringType,
nameof(Enumerable.OrderByDescending),
node.Method.GetGenericArguments(),
args
);
}
return base.VisitMethodCall(node);
}
}

和示例用法


var query = _context.Items
.Include(a => a.ItemProductTypes)
.ThenInclude(a => a.ProductType)
.OrderBy(a => a.ItemProductTypes
.OrderBy(b => b.ProductType.ProductTypeName)
.Select(c => c.ProductType.ProductTypeName)
.FirstOrDefault()
)
// everything down to here is same as in the original query
.SwitchOrderBy(descending: true); // pass your criteria here