wake-up-neo.net

Dynamische Abfrage mit OR Bedingungen in Entity Framework

Ich erstelle eine Anwendung, die die Datenbank durchsucht und dem Benutzer das dynamische Hinzufügen beliebiger Kriterien ermöglicht (etwa 50 möglich), ähnlich der folgenden SO - Frage: Erstellen dynamischer Abfragen mit Entity Framework . Ich arbeite derzeit an einer Suche, die die einzelnen Kriterien überprüft. Wenn sie nicht leer ist, wird sie der Abfrage hinzugefügt. 

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

Dieser Code erzeugt etwas Ähnliches in SQL-Server (ich habe es für ein besseres Verständnis vereinfacht)

SQL

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

Ich versuche jetzt, eine Möglichkeit hinzuzufügen, die folgende SQL mit C # durch das Entity-Framework zu generieren, jedoch mit einem ODER anstelle von UND, wobei die Fähigkeit zum dynamischen Hinzufügen von Kriterien erhalten bleibt.

SQL

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

Normalerweise sind die Kriterien für eine Abfrage nicht größer als zwei oder drei Elemente. Sie können jedoch zu einer riesigen Abfrage zusammengefasst werden. Ich habe concat, union und intersect ausprobiert und sie alle kopieren nur die Abfrage und schließen sich ihnen mit UNION an.

Gibt es eine einfache und saubere Möglichkeit, einer dynamisch generierten Abfrage mithilfe des Entity-Frameworks "ODER" -Bedingungen hinzuzufügen?

Bearbeiten mit meiner Lösung - 29.09.2015

Seit ich dies geschrieben habe, habe ich festgestellt, dass dies ein wenig Aufmerksamkeit erhalten hat, also habe ich mich entschlossen, meine Lösung zu posten

// 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

Sie suchen wahrscheinlich nach etwas wie Predicate Builder , mit dem Sie die ANDs und ORs der where-Anweisung leichter steuern können.

Es gibt auch Dynamic Linq , mit dem Sie die WHERE-Klausel wie eine SQL-Zeichenfolge übergeben können, und sie wird in das richtige Prädikat für ein WHERE geparst.

18
Steven V

Obwohl LINQKit und sein PredicateBuilder recht vielseitig sind, ist es möglich, dies mit einigen einfachen Dienstprogrammen (die jeweils als Grundlage für andere Operationen zur Bearbeitung von Ausdrücken dienen) direkter auszuführen:

Erstens ein allgemeiner Ausdrucksersatz:

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

Als Nächstes eine einfache Utility-Methode, um die Verwendung eines Parameters durch einen anderen Parameter in einem bestimmten Ausdruck zu ersetzen:

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

Dies ist notwendig, da die Lambda-Parameter in zwei verschiedenen Ausdrücken tatsächlich unterschiedliche Parameter sind, selbst wenn sie denselben Namen haben. Wenn Sie beispielsweise mit q => q.first.Contains(first) || q.last.Contains(last) enden möchten, muss die q in q.last.Contains(last) die exakt gleiche q sein, die am Anfang des Lambda-Ausdrucks angegeben ist.

Als Nächstes benötigen wir eine allgemein verwendbare Join-Methode, die in der Lage ist, Lambda-Ausdrücke im Func<T, TReturn>-Stil mit einem bestimmten Generator für binäre Ausdrücke zu verbinden.

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

Wir werden dies mit Expression.Or verwenden, aber Sie können dieselbe Methode für verschiedene Zwecke verwenden, beispielsweise für die Kombination numerischer Ausdrücke mit Expression.Add.

Zum Schluss kann man so etwas zusammen haben:

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

Gibt es eine einfache und saubere Möglichkeit, einer dynamisch generierten Abfrage mithilfe des Entity-Frameworks "ODER" -Bedingungen hinzuzufügen?

Ja, Sie können dies erreichen, indem Sie sich einfach auf eine einzige where-Klausel verlassen, die einen einzigen booleschen Ausdruck enthält, dessen OR-Teile zur Laufzeit dynamisch deaktiviert oder aktiviert sind. So müssen Sie nicht LINQKit installieren oder einen benutzerdefinierten Prädikat-Generator schreiben.

In Bezug auf Ihr Beispiel:

var isFirstValid = !string.IsNullOrWhiteSpace(first);
var isLastValid = !string.IsNullOrWhiteSpace(last);

var query = db.Names
  .AsQueryable()
  .Where(name =>
    (isFirstValid && name.first.Contains(first)) ||
    (isLastValid && name.last.Contains(last))
  )
  .ToList();

Wie Sie im obigen Beispiel sehen können, schalten wir die ODER-Teile des where-Filterausdrucks dynamisch ein oder aus, basierend auf zuvor bewerteten Voraussetzungen (z. B. isFirstValid). 

Wenn zum Beispiel isFirstValid nicht true ist, ist name.first.Contains(first)short-circuit und wird weder ausgeführt noch beeinflusst das Resultset. Darüber hinaus wird DefaultQuerySqlGenerator von EF Core den booleschen Ausdruck in where vor der Ausführung weiter optimieren und reduzieren (z. B. kann false && x || true && y || false && z durch einfache statische Analyse auf y reduziert werden).

Bitte beachten Sie: Wenn keine der Prämissen true ist, ist die Ergebnismenge leer - was in Ihrem Fall das gewünschte Verhalten ist. Wenn Sie jedoch aus irgendeinem Grund es vorziehen, alle Elemente aus Ihrer IQueryable-Quelle auszuwählen, können Sie dem Ausdruck true eine abschließende Variable hinzufügen (z. B. .Where( ... || shouldReturnAll) mit var shouldReturnAll = !(isFirstValid || isLastValid) oder etwas Ähnlichem).

Eine letzte Bemerkung: Der Nachteil dieser Technik besteht darin, dass Sie dazu gezwungen werden, einen "zentralisierten" booleschen Ausdruck zu erstellen, der sich in demselben Methodenkörper befindet, in dem Ihre Abfrage liegt (genauer gesagt der where-Teil der Abfrage). Wenn Sie aus irgendeinem Grund den Build-Prozess Ihrer Vergleichselemente dezentralisieren und als Argumente einfügen oder über den Query Builder verketten möchten, sollten Sie sich besser an einen Vergleichselement-Builder halten, wie in den anderen Antworten vorgeschlagen. Ansonsten genieße diese einfache Technik :)

1
B12Toaster