如何转换此LINQ表达式?

4

假设我有一个实体,我想查询并应用排名:

public class Person: Entity
{
    public int Id { get; protected set; }
    public string Name { get; set; }
    public DateTime Birthday { get; set; }
}

在我的查询中,我有以下内容:

Expression<Func<Person, object>> orderBy = x => x.Name;

var dbContext = new MyDbContext();

var keyword = "term";
var startsWithResults = dbContext.People
    .Where(x => x.Name.StartsWith(keyword))
    .Select(x => new {
        Rank = 1,
        Entity = x,
    });
var containsResults = dbContext.People
    .Where(x => !startsWithResults.Select(y => y.Entity.Id).Contains(x.Id))
    .Where(x => x.Name.Contains(keyword))
    .Select(x => new {
        Rank = 2,
        Entity = x,
    });

var rankedResults = startsWithResults.Concat(containsResults)
    .OrderBy(x => x.Rank);

// TODO: apply thenby ordering here based on the orderBy expression above

dbContext.Dispose();

我尝试在选择具有 Rank 属性的匿名对象之前对结果进行排序,但排序结果最终丢失了。似乎实体框架中的linq会在 ConcatUnion 期间丢弃单独集合的排序,并转换回自然排序。
我认为我可以动态地将 orderBy 变量中定义的表达式从 x => x.Name 转换成 x => x.Entity.Name,但我不确定如何做到这一点。
if (orderBy != null)
{
    var transformedExpression = ???
    rankedResults = rankedResults.ThenBy(transformedExpression);
}

我如何使用`Expression.Lambda`将`x => x.Name`封装为`x => x.Entity.Name`? 当我在`ThenBy`中硬编码`x => x.Entity.Name`时,我得到了想要的排序结果。但是,查询的`orderBy`由调用类提供,因此我不希望在其中硬编码。上面的示例中仅出于解释简单性而硬编码。

1
我删除了我的答案,这样你就可以尝试重新获得赏金。如果你在将赏金授予Aron后发表跟进评论,我会重新发布我的答案,以便帮助未来的人。顺便问一下,你尝试过我的解决方案吗?如果尝试了,它是否有效? - Mike Strobel
@MikeStrobel 谢谢,我非常感激。你可以将你的答案重新发布为一个独立的帖子,但请不要恢复你删除的那个。我仍在等待赏金退还,以便我可以重新授予它。 - danludwig
在这里使用一个强类型的占位符类会节省很多麻烦。 - Travis J
2个回答

4

这应该有所帮助。但是,为了使其正常工作,您需要定义匿名类型。由于在类型仍然是匿名的情况下创建Expression<Func<Anonymous, Person>> 将会很困难,所以我的LinqPropertyChain将无法与其配合使用。

Expression<Func<Person, object>> orderBy = x => x.Name;

using(var dbContext = new MyDbContext())
{
var keyword = "term";
var startsWithResults = dbContext.People
    .Where(x => x.Name.StartsWith(keyword))
    .Select(x => new {
        Rank = 1,
        Entity = x,
    });
var containsResults = dbContext.People
    .Where(x => !startsWithResults.Select(y => y.Entity.Id).Contains(x.Id))
    .Where(x => x.Name.Contains(keyword))
    .Select(x => new {
        Rank = 2,
        Entity = x,
    });


var rankedResults = startsWithResults.Concat(containsResults)
    .OrderBy(x => x.Rank)
    .ThenBy(LinqPropertyChain.Chain(x => x.Entity, orderBy));

// TODO: apply thenby ordering here based on the orderBy expression above

}

public static class LinqPropertyChain 
{

    public static Expression<Func<TInput, TOutput>> Chain<TInput, TOutput, TIntermediate>(
        Expression<Func<TInput, TIntermediate>> outter,
        Expression<Func<TIntermediate, TOutput>> inner
        )
    {

        Console.WriteLine(inner);
        Console.WriteLine(outter);
        var visitor = new Visitor(new Dictionary<ParameterExpression, Expression>
        {
            {inner.Parameters[0], outter.Body}
        });

        var newBody = visitor.Visit(inner.Body);
        Console.WriteLine(newBody);
        return Expression.Lambda<Func<TInput, TOutput>>(newBody, outter.Parameters);
    }

    private class Visitor : ExpressionVisitor
    {
        private readonly Dictionary<ParameterExpression, Expression> _replacement;

        public Visitor(Dictionary<ParameterExpression, Expression> replacement)
        {
            _replacement = replacement;
        }

        protected override Expression VisitParameter(ParameterExpression node)
        {
            if (_replacement.ContainsKey(node))
                return _replacement[node];
            else
            {
                return node;
            }
        }
    }
}

找到了一种使用较少明确泛型的方法来实现这个问题。

Expression<Func<Person, object>> orderBy = x => x.Name;
Expression<Func<Foo, Person>> personExpression = x => x.Person;

var helper = new ExpressionChain(personExpression);
var chained = helper.Chain(orderBy).Expression;


// Define other methods and classes here
public class ExpressionChain<TInput, TOutput>
{
    private readonly Expression<Func<TInput, TOutput>> _expression; 
    public ExpressionChain(Expression<Func<TInput, TOutput>> expression)
    {
        _expression = expression;
    }

    public Expression<Func<TInput, TOutput>> Expression { get { return _expression; } }

    public ExpressionChain<TInput, TChained> Chain<TChained>
        (Expression<Func<TOutput, TChained>> chainedExpression)
    {
        var visitor = new Visitor(new Dictionary<ParameterExpression, Expression>
        {
            {_expression.Parameters[0], chainedExpression.Body}
        });
        var lambda = Expression.Lambda<Func<TInput, TOutput>>(newBody, outter.Parameters);
        return new ExpressionChain(lambda);
    }

    private class Visitor : ExpressionVisitor
    {
        private readonly Dictionary<ParameterExpression, Expression> _replacement;

        public Visitor(Dictionary<ParameterExpression, Expression> replacement)
        {
            _replacement = replacement;
        }

        protected override Expression VisitParameter(ParameterExpression node)
        {
            if (_replacement.ContainsKey(node))
                return _replacement[node];
            else
            {
                return node;
            }
        }
    }
}

太棒了,这绝对有效。除了具体化匿名类型之外,我还必须明确拼写 Chain 方法的泛型参数。只有当我这样做时才起作用:.ThenBy(LinqPropertyChain.Chain<ConcreteRanker, object, Person>(x => x.Entity, orderBy)) - danludwig

1

由于您首先按Rank排序,并且每个序列内的Rank值相同,因此您应该能够独立进行排序,然后进行连接。根据您的帖子,似乎这里的问题是Entity Framework在ConcatUnion操作中没有维护排序。您可以通过强制在客户端执行连接来解决这个问题:

var rankedResults = startsWithResults.OrderBy(orderBy)
                                     .AsEnumerable()
                                     .Concat(containsResults.OrderBy(orderBy));

这将使Rank属性变得不必要,可能简化针对数据库执行的SQL查询,并且不需要深入研究表达式树。
缺点是一旦调用AsEnumerable(),您将不再有在数据库端附加其他操作的选项(即,如果您在Concat之后链接其他LINQ运算符,它们将使用LINQ-to-collections实现)。从您的代码来看,我认为这不会成为问题,但值得一提。

记录一下,我知道我本可以通过连接数据库并合并结果来完成这个任务。但是在排序后还进行了其他的数据库操作-- 分页(.Skip.Take),因此在查询数据库之前必须修改表达式树。 - danludwig

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接