How can I transform this linq expression?

牧云@^-^@ 提交于 2020-01-13 03:40:08

问题


Say I have an entity that I want to query with ranking applied:

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

In my query I have the following:

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();

I have tried ordering the results before selecting the anonymous object with the Rank property, but the ordering ends up getting lost. It seems that linq to entities discards the ordering of the separate sets and converts back to natural ordering during both Concat and Union.

What I think I may be able to do is dynamically transform the expression defined in the orderBy variable from x => x.Name to x => x.Entity.Name, but I'm not sure how:

if (orderBy != null)
{
    var transformedExpression = ???
    rankedResults = rankedResults.ThenBy(transformedExpression);
}

How might I be able to use Expression.Lambda to wrap x => x.Name into x => x.Entity.Name? When I hard code x => x.Entity.Name into the ThenBy I get the ordering that I want, but the orderBy is provided by the calling class of the query, so I don't want to hard-code it in. I have it hardcoded in the example above for simplicity of explanation only.


回答1:


This should help. However you are going to have to concrete up the Anonymous type for this to work. My LinqPropertyChain will not work with it, since its going to be difficult to create the Expression<Func<Anonymous, Person>> whilst its still Anonymous.

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;
            }
        }
    }
}

Figured out a way to do this with less Explicite Generics.

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;
            }
        }
    }
}



回答2:


Since you're ordering by Rank first, and the Rank values are identical within each sequence, you should be able to just sort independently and then concatenate. It sounds like the hiccup here would be that, according to your post, Entity Framework isn't maintaining sorting across Concat or Union operations. You should be able to get around this by forcing the concatenation to happen client-side:

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

This should render the Rank property unnecessary and probably simplify the SQL queries being executed against your database, and it doesn't require mucking about with expression trees.

The downside is that, once you call AsEnumerable(), you no longer have the option of appending additional database-side operations (i.e., if you chain additional LINQ operators after Concat, they will use the LINQ-to-collections implementations). Looking at your code, I don't think this would be a problem for you, but it's worth mentioning.



来源:https://stackoverflow.com/questions/19031493/how-can-i-transform-this-linq-expression

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!