web-dev-qa-db-fra.com

Requête dynamique avec OR conditions dans Entity Framework

Je crée une application créant une recherche dans la base de données et permettant à l'utilisateur d'ajouter dynamiquement tout critère (environ 50 possibles), à l'instar de la question SO suivante: Création de requêtes dynamiques avec un cadre d'entité . Je travaille actuellement sur une recherche qui vérifie chaque critère et, s'il n'est pas vide, il l'ajoute à la requête. 

C #

var query = Db.Names.AsQueryable();
  if (!string.IsNullOrWhiteSpace(first))
      query = query.Where(q => q.first.Contains(first));
  if (!string.IsNullOrWhiteSpace(last))
      query = query.Where(q => q.last.Contains(last));
  //.. around 50 additional criteria
  return query.ToList();

Ce code produit quelque chose de similaire au suivant dans le serveur SQL (j'ai simplifié pour une compréhension plus facile)

SQL

SELECT
    [Id],
    [FirstName],
    [LastName],
    ...etc
FROM [dbo].[Names]
WHERE [FirstName] LIKE '%first%'
  AND [LastName] LIKE '%last%'

J'essaie maintenant d'ajouter un moyen de générer le code SQL suivant avec C # via un cadre d'entité, mais avec un OU au lieu d'un ET, tout en conservant la possibilité d'ajouter des critères de manière dynamique.

SQL

SELECT
    [Id],
    [FirstName],
    [LastName],
    ...etc
  FROM [dbo].[Names]
WHERE [FirstName] LIKE '%first%'
  OR [LastName] LIKE '%last%' <-- NOTICE THE "OR"

Habituellement, les critères ne sont pas supérieurs à deux ou trois éléments pour une requête, mais leur combinaison en une requête gigantesque n’est pas une option. J'ai essayé concat, union et intersection et ils dupliquent tous la requête et les joignent à UNION.

Existe-t-il un moyen simple et propre d’ajouter des conditions "OU" à une requête générée dynamiquement à l’aide du framework entity?

Editer avec ma solution - 29/09/2015

Depuis que j'ai posté ceci, j'ai remarqué que cela avait reçu un peu d'attention, alors j'ai décidé de poster ma solution

// Make sure to add required nuget
// PM> Install-Package LinqKit

var searchCriteria = new 
{
    FirstName = "sha",
    LastName = "hill",
    Address = string.Empty,
    Dob = (DateTime?)new DateTime(1970, 1, 1),
    MaritalStatus = "S",
    HireDate = (DateTime?)null,
    LoginId = string.Empty,
};

var predicate = PredicateBuilder.False<Person>();
if (!string.IsNullOrWhiteSpace(searchCriteria.FirstName))
{
    predicate = predicate.Or(p => p.FirstName.Contains(searchCriteria.FirstName));
}

if (!string.IsNullOrWhiteSpace(searchCriteria.LastName))
{
    predicate = predicate.Or(p => p.LastName.Contains(searchCriteria.LastName));
}

// Quite a few more conditions...

foreach(var person in this.Persons.Where(predicate.Compile()))
{
    Console.WriteLine("First: {0} Last: {1}", person.FirstName, person.LastName);
}
30
Ben Anderson

Vous cherchez probablement quelque chose comme Predicate Builder qui vous permet de contrôler plus facilement les AND et les OR de la déclaration where.

Il y a aussi Dynamic Linq qui vous permet de soumettre la clause WHERE comme une chaîne SQL et de l'analyser dans le prédicat correct d'un WHERE.

18
Steven V

Bien que LINQKit et son PredicateBuilder soient assez polyvalents, il est possible de le faire plus directement avec quelques utilitaires simples (chacun pouvant servir de base à d’autres opérations de manipulation d’expression):

Tout d'abord, un substitut d'expression polyvalent:

public class ExpressionReplacer : ExpressionVisitor
{
    private readonly Func<Expression, Expression> replacer;

    public ExpressionReplacer(Func<Expression, Expression> replacer)
    {
        this.replacer = replacer;
    }

    public override Expression Visit(Expression node)
    {
        return base.Visit(replacer(node));
    }
}

Ensuite, une méthode utilitaire simple pour remplacer l’utilisation d’un paramètre par un autre paramètre dans une expression donnée:

public static T ReplaceParameter<T>(T expr, ParameterExpression toReplace, ParameterExpression replacement)
    where T : Expression
{
    var replacer = new ExpressionReplacer(e => e == toReplace ? replacement : e);
    return (T)replacer.Visit(expr);
}

Cela est nécessaire car les paramètres lambda dans deux expressions différentes sont en réalité des paramètres différents, même s'ils portent le même nom. Par exemple, si vous voulez vous retrouver avec q => q.first.Contains(first) || q.last.Contains(last), la q dans q.last.Contains(last) doit être le exact même q fourni au début de l'expression lambda.

Ensuite, nous avons besoin d’une méthode polyvalente Join capable de joindre des expressions lambda de style Func<T, TReturn> avec un générateur d’expression binaire donné.

public static Expression<Func<T, TReturn>> Join<T, TReturn>(Func<Expression, Expression, BinaryExpression> joiner, IReadOnlyCollection<Expression<Func<T, TReturn>>> expressions)
{
    if (!expressions.Any())
    {
        throw new ArgumentException("No expressions were provided");
    }
    var firstExpression = expressions.First();
    var otherExpressions = expressions.Skip(1);
    var firstParameter = firstExpression.Parameters.Single();
    var otherExpressionsWithParameterReplaced = otherExpressions.Select(e => ReplaceParameter(e.Body, e.Parameters.Single(), firstParameter));
    var bodies = new[] { firstExpression.Body }.Concat(otherExpressionsWithParameterReplaced);
    var joinedBodies = bodies.Aggregate(joiner);
    return Expression.Lambda<Func<T, TReturn>>(joinedBodies, firstParameter);
}

Nous allons utiliser cela avec Expression.Or, mais vous pouvez utiliser la même méthode à diverses fins, comme la combinaison d'expressions numériques avec Expression.Add.

Enfin, en réunissant tout cela, vous pouvez avoir quelque chose comme ceci:

var searchCriteria = new List<Expression<Func<Name, bool>>();

  if (!string.IsNullOrWhiteSpace(first))
      searchCriteria.Add(q => q.first.Contains(first));
  if (!string.IsNullOrWhiteSpace(last))
      searchCriteria.Add(q => q.last.Contains(last));
  //.. around 50 additional criteria
var query = Db.Names.AsQueryable();
if(searchCriteria.Any())
{
    var joinedSearchCriteria = Join(Expression.Or, searchCriteria);
    query = query.Where(joinedSearchCriteria);
}
  return query.ToList();
4
StriplingWarrior