4

Dire que j'ai une entité que je veux interroger avec le classement appliqué:Comment puis-je transformer cette expression linq?

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

Dans ma requête, je donne les résultats suivants:

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

J'ai essayé de commander les résultats avant de sélectionner l'objet anonyme la propriété Rank, mais la commande finit par se perdre. Il semble que linq aux entités rejette l'ordre des ensembles séparés et les convertit à l'ordre naturel pendant les deux et Union.

Ce que je pense que je peux être en mesure de faire est de transformer dynamiquement l'expression définie dans la variable de x => x.Name à x => x.Entity.NameorderBy, mais je ne suis pas sûr de savoir comment:

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

Comment pourrais-je être en mesure d'utiliser Expression.Lambda pour envelopper x => x.Name dans x => x.Entity.Name? Lorsque je code dur x => x.Entity.Name dans le ThenBy je reçois la commande que je veux, mais le orderBy est fourni par la classe appelante de la requête, donc je ne veux pas le coder en dur. Je l'ai codé en dur dans l'exemple ci-dessus pour la simplicité de l'explication seulement.

+1

J'ai supprimé ma réponse pour essayer de récupérer la prime. Si vous publiez un commentaire de suivi une fois que vous l'aurez remis à Aron, je le re-posterai au cas où cela aiderait quelqu'un dans le futur. BTW, avez-vous essayé ma solution, et si oui, cela a-t-il fonctionné? –

+0

@MikeStrobel merci j'apprécie vraiment cela. Vous pouvez aller de l'avant et republier votre réponse en tant que séparé, juste ne pas supprimer celui que vous avez supprimé s'il vous plaît. J'attends toujours que la prime soit remboursée pour que je puisse la récompenser. – danludwig

+0

L'utilisation d'une classe d'espace réservé fortement typée ici aurait sauvé beaucoup d'ennuis. –

Répondre

4

Cela devrait aider. Cependant, vous devrez concrétiser le type anonyme pour que cela fonctionne. Mon LinqPropertyChain ne fonctionnera pas avec lui, car il va être difficile de créer le Expression<Func<Anonymous, Person>> alors qu'il est encore anonyme.

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

Imaginé un moyen de le faire avec moins 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; 
      } 
     } 
    } 
} 
+0

Juste édité, puisque je viens d'écrire quelques tests pour cela ... – Aron

+0

Génial, cela fonctionne définitivement. En plus de bétonner le type anonyme, j'ai aussi dû épeler explicitement les arguments génériques à la méthode Chain. Cela ne fonctionnait que lorsque je l'ai fait '.ThenBy (LinqPropertyChain.Chain (x => x.Entity, orderBy))' – danludwig

1

Puisque vous commandez par Rank d'abord, et les valeurs Rank sont identiques dans chaque séquence, vous devriez être en mesure de quelque sorte indépendamment puis concaténer. Il semble que le hiccup ici serait que, selon votre message, Entity Framework ne maintient pas le tri à travers Concat ou Union opérations. Vous devriez être en mesure de contourner ce problème en forçant la concaténation à se produire du côté client:

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

Cela devrait rendre la Rank propriété inutile et simplifier probablement les requêtes SQL en cours d'exécution sur votre base de données, et il ne nécessite pas déblayer avec des arbres d'expression. L'inconvénient est que, une fois que vous avez appelé AsEnumerable(), vous n'avez plus la possibilité d'ajouter des opérations supplémentaires côté base de données (si vous enchaînez des opérateurs LINQ supplémentaires après Concat, ils utiliseront les implémentations LINQ-to-collections) . En regardant votre code, je ne pense pas que ce serait un problème pour vous, mais il vaut la peine de le mentionner.

+0

Pour le compte rendu, je sais que j'aurais pu le faire en tapant la base de données puis concerter les résultats. Il y a des opérations additionnelles de db-side qui se passent après le tri - à savoir, la pagination ('.Skip' et' .Take'), ainsi, je dois nettoyer avec l'arbre d'expression avant de frapper le db. – danludwig

Questions connexes