webentwicklung-frage-antwort-db.com.de

Fügen Sie dynamisch neue Lambda-Ausdrücke hinzu, um einen Filter zu erstellen

Ich muss eine Filterung für ein ObjectSet durchführen, um die benötigten Entitäten zu erhalten:

query = this.ObjectSet.Where(x => x.TypeId == 3); // this is just an example;

Später im Code (und vor dem Start der verzögerten Ausführung) filtere ich die Abfrage erneut wie folgt:

query = query.Where(<another lambda here ...>);

Das funktioniert soweit ganz gut.

Hier ist mein Problem:

Die Entitäten enthalten eine DateFrom - Eigenschaft und eine DateTo - Eigenschaft, die beide DataTime Typen sind. Sie repräsentieren eine Zeitspanne .

Ich muss die Entitäten filtern, um nur diejenigen zu erhalten, die Teil einer collection of Zeiträume sind. Die Perioden in der Sammlung sind nicht notwendigerweise zusammenhängend , daher sieht die Logik zum Abrufen der Entitäten so aus:

entities.Where(x => x.DateFrom >= Period1.DateFrom and x.DateTo <= Period1.DateTo)
||
entities.Where(x => x.DateFrom >= Period2.DateFrom and x.DateTo <= Period2.DateTo)
||

... und so weiter und so weiter für alle Zeiträume in der Sammlung.

Ich habe das versucht:

foreach (var ratePeriod in ratePeriods)
{
    var period = ratePeriod;

    query = query.Where(de =>
        de.Date >= period.DateFrom && de.Date <= period.DateTo);
}

Wenn ich jedoch die verzögerte Ausführung starte, wird diese in SQL genau so konvertiert, wie ich es möchte (ein Filter für jeden der Zeiträume für so viele Zeiträume in der Auflistung). ABER, er übersetzt AND-Vergleiche anstelle von OR Vergleiche, bei denen überhaupt keine Entitäten zurückgegeben werden, da eine Entität offensichtlich nicht mehr als einen Zeitraum umfasst.

Ich muss hier eine Art dynamisches Linq erstellen, um die Periodenfilter zu aggregieren.


Update

Basierend auf unserer Antwort habe ich folgendes Mitglied hinzugefügt:

private Expression<Func<T, bool>> CombineWithOr<T>(Expression<Func<T, bool>> firstExpression, Expression<Func<T, bool>> secondExpression)
{
    // Create a parameter to use for both of the expression bodies.
    var parameter = Expression.Parameter(typeof(T), "x");
    // Invoke each expression with the new parameter, and combine the expression bodies with OR.
    var resultBody = Expression.Or(Expression.Invoke(firstExpression, parameter), Expression.Invoke(secondExpression, parameter));
    // Combine the parameter with the resulting expression body to create a new lambda expression.
    return Expression.Lambda<Func<T, bool>>(resultBody, parameter);
}

Deklarierte einen neuen CombineWithOr-Ausdruck:

Expression<Func<DocumentEntry, bool>> resultExpression = n => false;

Und benutzte es in meiner Periodensammlung wie folgt:

foreach (var ratePeriod in ratePeriods)
{
    var period = ratePeriod;
    Expression<Func<DocumentEntry, bool>> expression = de => de.Date >= period.DateFrom && de.Date <= period.DateTo;
    resultExpression = this.CombineWithOr(resultExpression, expression);
}

var documentEntries = query.Where(resultExpression.Compile()).ToList();

Ich habe mir die resultierende SQL angesehen und es ist, als ob der Ausdruck überhaupt keine Wirkung hat. Das resultierende SQL gibt die zuvor programmierten Filter zurück, nicht jedoch die kombinierten Filter. Warum ?


Update 2

Ich wollte den Vorschlag von feO2x ausprobieren, deshalb habe ich meine Filterabfrage wie folgt umgeschrieben:

query = query.AsEnumerable()
    .Where(de => ratePeriods
        .Any(rp => rp.DateFrom <= de.Date && rp.DateTo >= de.Date))

Wie Sie sehen, habe ich AsEnumerable() hinzugefügt, aber der Compiler gab mir einen Fehler, dass er IEnumerable nicht zurück in IQueryable konvertieren kann. Ich habe also ToQueryable() am Ende meiner Abfrage hinzugefügt:

query = query.AsEnumerable()
    .Where(de => ratePeriods
        .Any(rp => rp.DateFrom <= de.Date && rp.DateTo >= de.Date))
            .ToQueryable();

Alles funktioniert gut. Ich kann den Code kompilieren und diese Abfrage starten. Es passt jedoch nicht zu meinen Bedürfnissen.

Während der Erstellung des SQL-Profils kann ich feststellen, dass die Filterung nicht Teil der SQL-Abfrage ist , da die Daten während des Vorgangs im Speicher gefiltert werden. Ich schätze, dass Sie das bereits wissen und das ist, was Sie vorschlagen wollten.

Ihr Vorschlag funktioniert, ABER, da er alle Entitäten aus der Datenbank abruft (und es Tausende und Abertausende von ihnen gibt), bevor er sie im Arbeitsspeicher filtert, ist es wirklich langsam, diese riesige Menge aus der Datenbank zurückzuholen.

Was ich wirklich möchte, ist das Senden der Periodenfilterung als Teil der resultierenden SQL-Abfrage , damit keine große Anzahl von Entitäten zurückgegeben wird, bevor der Filterungsprozess abgeschlossen ist.

12
user2324540

Trotz der guten Vorschläge musste ich mit dem LinqKit gehen. Einer der Gründe ist, dass ich dieselbe Art der Prädikatsaggregation an vielen anderen Stellen im Code wiederholen muss. LinqKit zu verwenden ist am einfachsten, ganz zu schweigen davon, dass ich es schaffen kann, indem ich nur ein paar Zeilen Code schreibe.

So habe ich mein Problem mit LinqKit gelöst:

var predicate = PredicateBuilder.False<Document>();
foreach (var submittedPeriod in submittedPeriods)
{
    var period = period;
    predicate = predicate.Or(d =>
        d.Date >= period.DateFrom && d.Date <= period.DateTo);
}

Und ich starte die verzögerte Ausführung (beachten Sie, dass ich kurz vorher AsExpandable() aufrufe):

var documents = this.ObjectSet.AsExpandable().Where(predicate).ToList();

Ich habe mir das resultierende SQL angesehen und es ist ein guter Job, um meine Prädikate in SQL zu übersetzen.

8
user2324540

Sie können eine Methode wie die folgende verwenden:

Expression<Func<T, bool>> CombineWithOr<T>(Expression<Func<T, bool>> firstExpression, Expression<Func<T, bool>> secondExpression)
{
    // Create a parameter to use for both of the expression bodies.
    var parameter = Expression.Parameter(typeof(T), "x");
    // Invoke each expression with the new parameter, and combine the expression bodies with OR.
    var resultBody = Expression.Or(Expression.Invoke(firstExpression, parameter), Expression.Invoke(secondExpression, parameter));
    // Combine the parameter with the resulting expression body to create a new lambda expression.
    return Expression.Lambda<Func<T, bool>>(resultBody, parameter);
}

Und dann:

Expression<Func<T, bool>> resultExpression = n => false; // Always false, so that it won't affect the OR.
foreach (var ratePeriod in ratePeriods)
{
    var period = ratePeriod;
    Expression<Func<T, bool>> expression = (de => de.Date >= period.DateFrom && de.Date <= period.DateTo);
    resultExpression = CombineWithOr(resultExpression, expression);
}

// Don't forget to compile the expression in the end.
query = query.Where(resultExpression.Compile());

Für weitere Informationen möchten Sie vielleicht Folgendes überprüfen:

Kombination zweier Ausdrücke (Ausdruck <Func <T, bool >>)

http://www.albahari.com/nutshell/predicatebuilder.aspx

Edit: Die Zeile Expression<Func<DocumentEntry, bool>> resultExpression = n => false; ist nur ein Platzhalter. Für die CombineWithOr-Methode sind zwei Methoden zu kombinieren, wenn Sie die Expression<Func<DocumentEntry, bool>> resultExpression;', you can't use it in the call toCombineWithOrfor the first time in yourforeach`-Schleife schreiben. Es ist genau wie der folgende Code:

int resultOfMultiplications = 1;
for (int i = 0; i < 10; i++)
    resultOfMultiplications = resultOfMultiplications * i;

Wenn resultOfMultiplications zunächst nichts enthält, können Sie es nicht in Ihrer Schleife verwenden.

Warum ist das Lambda n => false. Weil es in einer OR-Anweisung keine Auswirkung hat. Zum Beispiel ist false OR someExpression OR someExpression gleich someExpression OR someExpression. Dass false keine Wirkung hat.

4
hattenn

Wie wäre es mit diesem Code:

var targets = query.Where(de => 
    ratePeriods.Any(period => 
        de.Date >= period.DateFrom && de.Date <= period.DateTo));

Ich verwende den LINQ-Operator Any, um zu bestimmen, ob ein Tarifzeitraum vorliegt, der mit de.Date übereinstimmt. Ich bin mir allerdings nicht ganz sicher, wie dies von Entität in effiziente SQL-Anweisungen übersetzt wird. Wenn Sie die resultierende SQL posten könnten, wäre das für mich sehr interessant.

Hoffe das hilft.

UPDATE nach der Antwort vonnust:

Ich glaube nicht, dass die Lösung von unserer Lösung funktionieren würde, da Entity Framework LINQ-Ausdrücke verwendet, um die SQL- oder DML-Datenbank zu erzeugen, die für die Datenbank ausgeführt wird. Daher basiert Entity Framework auf der IQueryable<T>-Schnittstelle und nicht auf IEnumerable<T>. Die standardmäßigen LINQ-Operatoren (wie Where, Any, OrderBy, FirstOrDefault usw.) sind jetzt auf beiden Schnittstellen implementiert. Daher ist der Unterschied manchmal schwer zu erkennen. Der Hauptunterschied dieser Schnittstellen besteht darin, dass bei den Erweiterungsmethoden IEnumerable<T> die zurückgegebenen Enumerables fortlaufend ohne Nebenwirkungen aktualisiert werden, während bei IQueryable<T> der tatsächliche Ausdruck neu zusammengesetzt wird, der nicht nebenwirkungsfrei ist (dh Sie ändern) die Ausdrucksbaumstruktur, die schließlich zum Erstellen der SQL-Abfrage verwendet wird).

Jetzt unterstützt das Entity Framework die ca. Wenn Sie jedoch eigene Methoden schreiben, die einen IQueryable<T> (wie die Methode von hatenn) manipulieren, würde dies zu einem Ausdrucksbaum führen, den Entity Framework möglicherweise nicht analysieren kann, da er die neue Erweiterung einfach nicht kennt Methode. Dies kann die Ursache dafür sein, dass Sie die kombinierten Filter nicht sehen können, nachdem Sie sie erstellt haben (obwohl ich eine Ausnahme erwarten würde).

Wann funktioniert die Lösung mit dem Any-Operator:

In den Kommentaren haben Sie mitgeteilt, dass Sie einen System.NotSupportedException: Konstantenwert des Typs 'RatePeriod' nicht erstellen konnten. In diesem Zusammenhang werden nur Grundtypen oder Aufzählungstypen unterstützt. Dies ist der Fall, wenn die RatePeriod-Objekte In-Memory-Objekte sind und nicht von Entity Framework ObjectContext oder DbContext verfolgt werden. Ich habe eine kleine Testlösung erstellt, die hier heruntergeladen werden kann: https://dl.dropboxusercontent.com/u/14810011/LinqToEntitiesOrOperator.Zip

Ich habe Visual Studio 2012 mit LocalDB und Entity Framework 5 verwendet. Um die Ergebnisse anzuzeigen, öffnen Sie die Klasse LinqToEntitiesOrOperatorTest, öffnen Sie den Test Explorer, erstellen Sie die Lösung und führen Sie alle Tests aus. Sie werden erkennen, dass ComplexOrOperatorTestWithInMemoryObjects fehlschlagen wird, alle anderen sollten bestehen.

Der Kontext, den ich verwendet habe, sieht so aus:

public class DatabaseContext : DbContext
{
    public DbSet<Post> Posts { get; set; }
    public DbSet<RatePeriod> RatePeriods { get; set; }
}
public class Post
{
    public int ID { get; set; }
    public DateTime PostDate { get; set; }
}
public class RatePeriod
{
    public int ID { get; set; }
    public DateTime From { get; set; }
    public DateTime To { get; set; }
}

Nun, es ist so einfach wie es nur geht :-). Im Testprojekt gibt es zwei wichtige Unit-Testmethoden:

    [TestMethod]
    public void ComplexOrOperatorDBTest()
    {
        var allAffectedPosts =
            DatabaseContext.Posts.Where(
                post =>
                DatabaseContext.RatePeriods.Any(period => period.From < post.PostDate && period.To > post.PostDate));

        Assert.AreEqual(3, allAffectedPosts.Count());
    }

    [TestMethod]
    public void ComplexOrOperatorTestWithInMemoryObjects()
    {
        var inMemoryRatePeriods = new List<RatePeriod>
            {
                new RatePeriod {ID = 1000, From = new DateTime(2002, 01, 01), To = new DateTime(2006, 01, 01)},
                new RatePeriod {ID = 1001, From = new DateTime(1963, 01, 01), To = new DateTime(1967, 01, 01)}
            };

        var allAffectedPosts =
            DatabaseContext.Posts.Where(
                post => inMemoryRatePeriods.Any(period => period.From < post.PostDate && period.To > post.PostDate));
        Assert.AreEqual(3, allAffectedPosts.Count());
    }

Beachten Sie, dass die erste Methode erfolgreich ist, während die zweite mit der oben genannten Ausnahme fehlschlägt, obwohl beide Methoden genau dasselbe tun, mit der Ausnahme, dass im zweiten Fall Ratenperiodenobjekte im Speicher erstellt wurden, über die DatabaseContext nichts weiß.

Was können Sie tun, um dieses Problem zu lösen?

  1. Befinden sich Ihre RatePeriod-Objekte in derselben ObjectContext oder DbContext? Dann verwenden Sie sie wie im ersten oben genannten Unit-Test.

  2. Wenn nicht, können Sie alle Beiträge gleichzeitig laden oder würde dies zu einer OutOfMemoryException führen? Wenn nicht, können Sie den folgenden Code verwenden. Beachten Sie den Aufruf von AsEnumerable(), der dazu führt, dass der Operator Where für die Schnittstelle IEnumerable<T> anstelle von IQueryable<T> verwendet wird. Dies führt dazu, dass alle Beiträge in den Speicher geladen und dann gefiltert werden:

    [TestMethod]
    public void CorrectComplexOrOperatorTestWithInMemoryObjects()
    {
        var inMemoryRatePeriods = new List<RatePeriod>
            {
                new RatePeriod {ID = 1000, From = new DateTime(2002, 01, 01), To = new DateTime(2006, 01, 01)},
                new RatePeriod {ID = 1001, From = new DateTime(1963, 01, 01), To = new DateTime(1967, 01, 01)}
            };
    
        var allAffectedPosts =
            DatabaseContext.Posts.AsEnumerable()
                           .Where(
                               post =>
                               inMemoryRatePeriods.Any(
                                   period => period.From < post.PostDate && period.To > post.PostDate));
        Assert.AreEqual(3, allAffectedPosts.Count());
    }
    
  3. Wenn die zweite Lösung nicht möglich ist, würde ich empfehlen, eine gespeicherte TSQL-Prozedur zu schreiben, in der Sie Ihre Tarifperioden übergeben und die korrekte SQL-Anweisung bildet. Diese Lösung ist auch die leistungsfähigste.

1
feO2x

Ich denke jedoch, dass dynamische LINQ-Abfrageerstellung nicht so einfach war, wie ich dachte. Versuchen Sie es mit Entity SQL, ähnlich wie unten beschrieben:

var filters = new List<string>();
foreach (var ratePeriod in ratePeriods)
{
    filters.Add(string.Format("(it.Date >= {0} AND it.Date <= {1})", ratePeriod.DateFrom, ratePeriod.DateTo));
}

var filter = string.Join(" OR ", filters);
var result = query.Where(filter);

Dies ist möglicherweise nicht genau richtig (ich habe es nicht versucht), aber es sollte etwas Ähnliches sein.

0
hattenn