Sử dụng lại logic đặt hàng
Tôi có một enum mô tả một thứ tự sắp xếp nhất định cho một bài đăng:
enum PostOrder
{
TitleAsc,
TitleDesc,
ScoreAsc,
ScoreDesc,
}
và một phương thức mở rộng để sử dụng lại logic sắp xếp:
static class IQueryableExtensions
{
public static IOrderedQueryable<Post> OrderByCommon(this IQueryable<Post> queryable, PostOrder orderBy)
=> orderBy switch
{
PostOrder.TitleAsc => queryable.OrderBy(x => x.Title),
PostOrder.TitleDesc => queryable.OrderByDescending(x => x.Title),
PostOrder.ScoreAsc => queryable.OrderBy(x => x.Score).ThenBy(x => x.Title),
PostOrder.ScoreDesc => queryable.OrderByDescending(x => x.Score).ThenBy(x => x.Title),
_ => throw new NotSupportedException(),
};
}
Phương thức mở rộng hoạt động khi được sử dụng trong ngữ cảnh bình thường nhưng không thành công ở đây:
var input = PostOrder.ScoreDesc;
var dbContext = new QuestionContext();
var users = dbContext.Users
.Select(x => new
{
User = x,
Top3Posts = x.Posts.AsQueryable()
.OrderByCommon(input)
.Take(3)
.ToList()
}).ToList();
với lỗi này:
The LINQ expression 'MaterializeCollectionNavigation(
Navigation: User.Posts,
subquery: NavigationExpansionExpression
Source: DbSet<Post>()
.Where(p => EF.Property<Nullable<int>>(u, "Id") != null && object.Equals(
objA: (object)EF.Property<Nullable<int>>(u, "Id"),
objB: (object)EF.Property<Nullable<int>>(p, "AuthorId")))
PendingSelector: p => NavigationTreeExpression
Value: EntityReference: Post
Expression: p
.Where(i => EF.Property<Nullable<int>>(NavigationTreeExpression
Value: EntityReference: User
Expression: u, "Id") != null && object.Equals(
objA: (object)EF.Property<Nullable<int>>(NavigationTreeExpression
Value: EntityReference: User
Expression: u, "Id"),
objB: (object)EF.Property<Nullable<int>>(i, "AuthorId")))
.AsQueryable()
.OrderByCommon(__input_0)
.Take(3)' could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to either AsEnumerable(), AsAsyncEnumerable(), ToList(), or ToListAsync(). See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.
có lẽ vì nó đang được sử dụng trong một Expression<>ngữ cảnh.
Làm thế nào tôi có thể làm cho nó hoạt động ở đó?
Một dự án có thể tái tạo có thể được tìm thấy trong kho này .
Trả lời
Đây là vấn đề nổi tiếng không có giải pháp chung.
Vấn đề chung với dịch cây biểu thức là nó hoàn toàn dựa trên kiến thức - không có phương thức nào thực sự được gọi, các phương thức đã biết được xác định bằng chữ ký và được dịch theo ngữ nghĩa đã biết của chúng. Đó là lý do tại sao không thể dịch các phương thức / thuộc tính / đại biểu tùy chỉnh.
Vấn đề thường được giải quyết bằng cách sử dụng một số thư viện thao tác biểu thức. Trong quá trình làm việc với EF6 / EF Core, tôi đã thử nhiều - LinqKit, NeinLinq, AutoMapper, gần đây là DelegateDecompiler. Tất cả chúng cho phép thay thế (mở rộng) các phần của cây biểu thức bằng các biểu thức thô tương ứng của chúng như thể bạn đã viết chúng theo cách thủ công.
Vấn đề trong trường hợp cụ thể này phức tạp hơn vì để được dịch, phương thức tùy chỉnh thực sự phải được gọi. Nhưng bằng cách nào? Đặc biệt, IQueryblelập luận sẽ là gì? Lưu ý rằng ở đây
x.Posts.AsQueryable()
bạn không có xcá thể, do đó không có Postscá thể tập hợp nào để gọi AsQueryable()và chuyển nó vào phương thức tùy chỉnh.
Một giải pháp khả thi là gọi phương thức truyền LINQ giả đến các Đối tượng IQueryable, sau đó tìm và thay thế nó trong cây biểu thức truy vấn kết quả bằng biểu thức thực.
Sau đây là việc thực hiện ý tưởng trên:
partial class IQueryableExtensions
{
public static IQueryable<T> Transform<T>(this IQueryable<T> source)
{
var expression = new QueryableMethodTransformer().Visit(source.Expression);
return expression == source.Expression ? source : source.Provider.CreateQuery<T>(expression);
}
class QueryableMethodTransformer : ExpressionVisitor
{
protected override Expression VisitMethodCall(MethodCallExpression node)
{
if (node.Method.DeclaringType == typeof(IQueryableExtensions) &&
node.Method.IsStatic &&
typeof(IQueryable).IsAssignableFrom(node.Method.ReturnType) &&
node.Arguments.Count > 1 &&
node.Arguments[0].Type.IsGenericType &&
node.Arguments[0].Type.GetGenericTypeDefinition() == typeof(IQueryable<>))
{
// Extract arguments
var args = new object[node.Arguments.Count];
int index = 1;
while (index < args.Length && TryExtractValue(Visit(node.Arguments[index]), out args[index]))
index++;
if (index == args.Length)
{
var source = node.Arguments[0];
var elementType = source.Type.GetGenericArguments()[0];
// Create fake queryable instance
var fakeSource = args[0] = EmptyQueryableMethod
.MakeGenericMethod(elementType)
.Invoke(null, null);
// Invoke the method with it
var result = (IQueryable)node.Method.Invoke(null, args);
// Replace it with the actual queryable expression
return new ConstValueReplacer
{
From = fakeSource,
To = source
}.Visit(result.Expression);
}
}
return base.VisitMethodCall(node);
}
static IQueryable<T> EmptyQueryable<T>() => Enumerable.Empty<T>().AsQueryable();
static readonly MethodInfo EmptyQueryableMethod = typeof(QueryableMethodTransformer)
.GetMethod(nameof(EmptyQueryable), BindingFlags.NonPublic | BindingFlags.Static);
static bool TryExtractValue(Expression source, out object value)
{
if (source is ConstantExpression constExpr)
{
value = constExpr.Value;
return true;
}
if (source is MemberExpression memberExpr && TryExtractValue(memberExpr.Expression, out var instance))
{
value = memberExpr.Member is FieldInfo field ? field.GetValue(instance) :
((PropertyInfo)memberExpr.Member).GetValue(instance);
return true;
}
value = null;
return source == null;
}
}
class ConstValueReplacer : ExpressionVisitor
{
public object From;
public Expression To;
protected override Expression VisitConstant(ConstantExpression node) =>
node.Value == From ? To : base.VisitConstant(node);
}
}
Như người ta có thể thấy, nó không chung chung lắm vì nó có rất nhiều giả định - tìm một phương thức tĩnh lấy IQueryable<T>đối số đầu tiên và các đối số khác là có thể đánh giá được (giá trị không đổi hoặc trường / thuộc tính của giá trị không đổi, trường hợp này xảy ra với các bao đóng) và thực hiện hành động nói trên.
Nhưng nó giải quyết vấn đề cụ thể. Tất cả những gì bạn cần là gọi Transformvào cuối truy vấn của bạn (trước khi cụ thể hóa):
var users = dbContext.Users
.Select(x => new
{
User = x,
Top3Posts = x.Posts.AsQueryable()
.OrderByCommon(input)
.Take(3)
.ToList()
})
.Transform() // <--
.ToList();
Giờ đây, bạn có thể tránh phải Transformgọi bằng cách cắm QueryableMethodTransformervào đường dẫn dịch truy vấn EF Core, nhưng nó yêu cầu rất nhiều mã đường ống dẫn nước chỉ để gọi một phương thức duy nhất. Lưu ý rằng nó phải được cắm vào trình dịch trước truy vấn, vì IMethodCallTranslatorkhông thể xử lý IQueryable(và nói chung IEnumerable) các đối số. Nếu bạn quan tâm, câu trả lời của tôi cho truy vấn EF Core tất cả các cột trong SQL khi ánh xạ tới đối tượng trong Select cho thấy cách bạn có thể cắm DelegateDecompiler vào EF Core, cùng một mã có thể được sử dụng để cắm aby khác (bao gồm cả mã được trình bày ở đây) tùy chỉnh bộ xử lý trước dựa trên khách truy cập biểu thức.