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 は、個別のセットの順序付けを破棄し、 と の両方Concatで自然な順序付けに戻すようUnionです。

私ができると思うのは、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ますか? にハードコードx => x.Entity.NameするThenByと、必要な順序が得orderByられますが、クエリの呼び出しクラスによって提供されるため、ハードコードしたくありません。上記の例では、簡単にするためにハードコードしています。説明のみ。

4

2 に答える 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;
            }
        }
    }
}
于 2013-09-26T15:16:55.317 に答える
1

Rank最初に並べ替えており、Rank値は各シーケンス内で同一であるため、個別に並べ替えてから連結できるはずです。ここでの問題は、あなたの投稿によると、Entity Framework が並べ替えConcatUnion操作を維持していないことです。連結をクライアント側で強制的に行うことで、これを回避できるはずです。

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

これにより、Rankプロパティが不要になり、おそらくデータベースに対して実行される SQL クエリが簡素化され、式ツリーをいじる必要がなくなります。

欠点は、 を呼び出すとAsEnumerable()、追加のデータベース側の操作を追加するオプションがなくなることです (つまり、 の後Concatに追加の LINQ 演算子をチェーンすると、LINQ-to-collections 実装が使用されます)。あなたのコードを見ると、これが問題になるとは思いませんが、言及する価値があります。

于 2013-10-09T17:52:51.107 に答える