Dynamische LINQ Abfrage (gestaffelte Abfragen mit wechselndem SearchTerm)

  • C#
  • .NET (FX) 4.5–4.8

Es gibt 15 Antworten in diesem Thema. Der letzte Beitrag () ist von ErfinderDesRades.

    Dynamische LINQ Abfrage (gestaffelte Abfragen mit wechselndem SearchTerm)

    Hi,
    ich hoffe ich hab den Titel nicht ganz falsch gewählt.

    Ich lese eine Csv-Datei ein und parse jede Zeile/Datensatz in ein eigenes Klassenmodel und erstelle eine Liste der Obj. ca. 4000 Stk.
    Die einzelnen Klassenmodele haben unter anderem folgende Propertys:

    ​ID, Typ, Herstellername, Modelfamilie, Modelname, ...

    Ich lass mir die Gesamtliste nun in einem Datagriedview anzeigen. Alles i.O.

    Der Benutzer soll nun in der Lage sein, einzelne Teile aus der Liste raussuchen zulassen.
    Für jedes Property nach dem gesucht werden soll, erstelle ich eine Txtbox.
    User kann also alle Teile vom Hersteller "Metzler" anzeigen lassen, die in der Modelfamlie "Masterlock" sind.
    Diese neue kleinere Ergebnisliste wird unter dem ersten Datagridview in einem zweiten Dgv angezeigt.

    Zum Bsp. so:

    C#-Quellcode

    1. ​List<MeList> match = new List<MeList>();
    2. //erste Eingrenzung
    3. match = LstAllObjCsvs.Where(x => Regex.IsMatch(x.Hersteller, SearchTermHersteller)).ToList();
    4. //zweite Eingrenzung
    5. match = match.Where(x => Regex.IsMatch(x.Familie, SearchTermFamilie)).ToList();


    Hier wäre das Problem, dass der User zwangsweise den Hersteller kennen muss damit so gesucht werden kann.

    Wie mache ich es, dass der User mal nur nach einem Property suchen kann oder nach beliebigen Konstellationen?
    z.B. Hersteller, ModelName oder Typ, ModelFamilie und Modelname
    Der User soll dazu noch in der Lage sein, die Suche nach einer Suche noch zu verfeinern, in dem er noch ein SuchProperty eintippt.
    z.B. erste Suche nur Hersteller. Dann danach noch die Modelfamilie ...


    Ich kann im Moment nur auf Lösungen kommen, die mal wieder nach feinstem SpaghettiCode aussehen.
    Was übersehe ich da?
    codewars.com Rank: 4 kyu
    Versuch einfacher zu beschreiben was ich machen möchte:
    Beispiel Schrottplatz 4000 Autos.
    Alle Autos bei Eingang in Computer eingetragen mit Merkmalen z.B. Hersteller, Farbe, Sitzplätze, PS,..

    Jetzt will ich sehen: "Hab ich rote Autos?" -> Liste wird nach rot durchsucht und von 4000 Stk. sind 300 rot
    jetzt will ich wissen wieviel von den roten Autos haben 120 Ps -> nur noch 20 Stk. gefunden die rot sind und 120 Ps haben.

    Und nun würde ich die Suche gerne so programmieren, dass verschiedene Suchreihenfolgen möglich sind.
    Mal nach Farbe mal Gewicht mal nach Sitzplätzen und Farbe etc.

    ----------
    Ich hab jetzt einen Ansatz gefunden, der nicht ganz schlecht ist.
    Kurzfassung:
    Ich erzeuge eine Liste aus Suchwortobjekten bestehend, aus dem TextString aus der Txtbx und je nach Txtbx (Herstellertxbx, Modeltxbx) dann noch das Property (nameOf(ModelProperty)) als String.
    Nur für das erste Suchwort such ich die Komplette Liste an Teilen durch. Für die weiteren Suchen nehme ich dann jeweils schon die "feinere" Liste.

    Ob das nun schlauch ist !?!? Es schein das zu machen was ich wollte und ist verständlich jedenfalls für mich ... in diesem Moment. Frag mich bloß nicht nach Sachen die ich vor 2 Wochen gecodet habe :)


    C#-Quellcode

    1. for (int i = 0; i < searchTerms.Count; i++)
    2. {
    3. if (i==0)
    4. {
    5. match = LstAllCsvs.Where(x => AddIfMatch(x, searchTerms[i].PropertyNameTooSearch, searchTerms[i].SearchTermString)).ToList();
    6. }
    7. match = match.Where(x => AddIfMatch(x, searchTerms[i].PropertyNameTooSearch, searchTerms[i].SearchTermString)).ToList();
    8. }


    Das echte "matchen" der Treffer erfolgt in einer extra Methode

    C#-Quellcode

    1. private bool AddIfMatch(MeList entrie, string propertyName, string searchTerm)
    2. {
    3. bool result = false;
    4. switch (true)
    5. {
    6. case true when propertyName == nameof(MeList.Hersteller):
    7. result = Regex.IsMatch(entrie.Hersteller, searchTerm, RegexOptions.IgnoreCase);
    8. break;
    9. case true when propertyName == nameof(MeList.Familie):
    10. result = Regex.IsMatch(entrie.Familie, searchTerm, RegexOptions.IgnoreCase);
    11. break;
    12. case true when propertyName == nameof(MeList.Modell):
    13. result = Regex.IsMatch(entrie.Modell, searchTerm, RegexOptions.IgnoreCase);
    14. break;
    15. case true when propertyName == nameof(MeList.Bauform):
    16. result = Regex.IsMatch(entrie.Bauform, searchTerm, RegexOptions.IgnoreCase);
    17. break;
    18. default:
    19. break;
    20. }
    21. return result;
    22. }
    codewars.com Rank: 4 kyu

    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „nogood“ () aus folgendem Grund: Lösungsansatz

    Ich glaube eine Lösung über LINQ zu suchen, würde mich auch gleich ein bisschen überfordern. Ich würde da eher eine statische SUCH-Klasse erstellen, die eine Einstiegs-Methode besitzt, in der die Liste und ein ganz spezifisches SuchObjekt eingegeben werden müssen. Dieses Suchobjekt ist ein Objekt (eine Klasse oder Struktur) die einerseits alle Eigenschaften auch enthält die deine Objekte in der Liste haben, aber anderseits auch noch genauere Definitionen wie ">", "<", "=" etc. aber auch Arrays {color1,color2} etc. zulässt. Und mit diesem SuchObjekt durchläufts du alle deine Objekte in der Liste, und prüfst, ob die Bedingungen erfüllt werden. Erfüllt ein Objekt in der Liste die gewünschten Bedingungen, so wird die Instanz des Objektes in einer Return-Liste gesammelt, und am Schluss zurückgegeben.

    Ich glaube das wäre meine erste Vorgehensweise die ich prüfen würde, ob sie in allem funkst.

    Freundliche Grüsse

    exc-jdbi
    eine möglichkeit wäre mit OLEDB und einer Schema.ini die
    CSV Datei direkt auszulesen. Mit Sql kannst du deine Filter angeben.

    habe eine einfache Datei erstellt mit zwei filter

    VB.NET-Quellcode

    1. Private Sub Button1_Click(sender As System.Object, e As System.EventArgs) Handles Button1.Click
    2. Dim dt As Data.DataTable = Nothing
    3. Dim FileName As String = "Users.txt"
    4. Dim sCon As String = "Provider=Microsoft.Jet.OleDb.4.0;Data Source=D:\TestFolder;Extended Properties='Text;HDR=Yes;Schema=schema.ini;';"
    5. Dim Cn As OleDb.OleDbConnection = New OleDb.OleDbConnection(sCon)
    6. Cn.Open()
    7. Dim SchemeFile As String = "D:\TestFolder\Schema.ini"
    8. If File.Exists(SchemeFile) Then File.Delete(SchemeFile)
    9. Using SchemaStream As FileStream = New FileStream(SchemeFile, FileMode.CreateNew)
    10. Using writer As StreamWriter = New StreamWriter(SchemaStream)
    11. writer.AutoFlush = True
    12. writer.WriteLine("[" & System.IO.Path.GetFileName(FileName) & "]")
    13. writer.WriteLine("ColNameHeader=True")
    14. writer.WriteLine("Format=Delimited(;)")
    15. writer.WriteLine("DecimalSymbol=,")
    16. writer.WriteLine("CharacterSet=ANSI")
    17. writer.WriteLine("Col1=Firstname Text Width 25")
    18. writer.WriteLine("Col2=Lastname Text Width 25")
    19. writer.WriteLine("Col3=City Text Width 65")
    20. Debug.Print("[" & System.IO.Path.GetFileName(FileName) & "]")
    21. Debug.Print("ColNameHeader=True")
    22. Debug.Print("Format=Delimited(;)")
    23. Debug.Print("CharacterSet=ANSI")
    24. Debug.WriteLine("Col1=Firstname Text Width 25")
    25. Debug.WriteLine("Col2=Lastname Text Width 25")
    26. Debug.WriteLine("Col3=City Text Width 65")
    27. End Using
    28. SchemaStream.Close()
    29. End Using
    30. Dim sSql As String = "SELECT Firstname,Lastname,City FROM " & FileName
    31. Dim sWhere As String = Nothing
    32. If TextBox1.Text <> Nothing Then
    33. sWhere &= " And (Lastname Like '" & TextBox1.Text & "%')"
    34. End If
    35. If TextBox2.Text <> Nothing Then
    36. sWhere &= " And (City Like '" & TextBox2.Text & "%')"
    37. End If
    38. If sWhere <> Nothing Then
    39. sWhere = " Where " & sWhere.Substring(4)
    40. End If
    41. sSql &= sWhere & " Order by LastName"
    42. Debug.Print(sSql)
    43. Dim Cmd As New OleDb.OleDbCommand(sSql, Cn)
    44. Dim Dr As OleDb.OleDbDataReader
    45. Dr = Cmd.ExecuteReader
    46. dt = New Data.DataTable
    47. dt.Load(Dr)
    48. Me.DataGridView1.DataSource = dt
    49. End Sub


    wenn du keine Filter in den Textboxen setzt, sieht deine SQL so aus

    VB.NET-Quellcode

    1. SELECT Firstname,Lastname,City FROM Users.txt Order by LastName


    mit 1 filter (Textbox1)

    VB.NET-Quellcode

    1. SELECT Firstname,Lastname,City FROM Users.txt Where (Lastname Like 'J%') Order by LastName


    mit 2 filter (Textbox1 und Textbox2)

    VB.NET-Quellcode

    1. SELECT Firstname,Lastname,City FROM Users.txt Where (Lastname Like 'J%') And (City Like 'L%') Order by LastName


    Die SQL passt sich an wie du siehst
    Vielleicht bringt es Dir was: In einem Privatprojekt habe ich eine List(Of Spezialobjekt). Ein Spezialobjekt hat mehrere Properties. Ich lasse die Property-1-Werte aller Objekte in CheckListBox1 eintragen, die Property-2-Werte aller Objekte in CheckListBox2, usw.
    Dann soll der User sich aus den CheckListBoxen raussuchen, welche Propertywerte zutreffen sollten.
    Dann sage ich: Gib mir alle Prozesse, bei denen:
    • der Property-1-Wert mit der Auswahl 1 zusammenpasst UND
    • der Property-2-Wert mit der Auswahl 2 zusammenpasst
    • usw.
    Also: alle möglichen Werte auflisten und vom User aussuchen lassen. Der Aufwand dafür ist gering.
    Dieser Beitrag wurde bereits 5 mal editiert, zuletzt von „VaporiZed“, mal wieder aus Grammatikgründen.

    Aufgrund spontaner Selbsteintrübung sind all meine Glaskugeln beim Hersteller. Lasst mich daher bitte nicht den Spekulatiusbackmodus wechseln.
    Hi,
    vielleicht noch so als Zusatz: das, was du suchst, nennt sich PredicateBuilder. Da gibt es diverse Nuget-Pakete, bzw. so etwas ist schnell selbst implementiert albahari.com/nutshell/predicatebuilder.aspx. Ich habe zusätlich ein Beispiel angehängt, welches die Idee dahinter verdeutlicht. Ein Vorteil dieser Methode ist auch, dass "Where" nur ein einziges Mal auf deine Daten angewandt wird und nicht mehrmals, wie in deinem Beispiel. Evtl. hilft dir das in irgendeiner Weise :)
    Dateien
    • PredicateTest.zip

      (196,14 kB, 88 mal heruntergeladen, zuletzt: )
    Man kann Linq-Suchen eiglich problemlos miteinander verknüpfen. PseudoCode:

    VB.NET-Quellcode

    1. Dim SucheRoteAutos as Func(of IEnumerable(Of Auto), IEnumerable(Of Auto)) =Function(autos) from a in autos Where rot '(so ähnlich)
    2. Dim Suche180Ps as Func(of IEnumerable(Of Auto), IEnumerable(Of Auto)) =Function(autos) from a in autos Where 180PS '(so ähnlich)
    3. Dim SucheRot_180Ps = Autos.Where(SucheRoteAutos).Where(Suche180Ps)
    Man kann auch eine Methode schreiben, der man beliebig viele solche Such-Filter übergeben kann, und die in einer Schleife alle Suchen anwendet auf das Ergebnis der jeweils vorherigen Suche.

    Edit: Der PredicateBuilder find ich aber eiglich sogar hübscher. :thumbup:

    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „ErfinderDesRades“ ()

    Danke an euch alle @exc-jdbi @Kasi @VaporiZed @ISliceUrPanties

    Im Einzelnen
    @exc-jdbi Klasse mit statischer Methode erstellen. Liste aller Objekte anhand eines "ObjektesMitExtraEigenschaften" durchlaufen und vergleichen bei "Übereinstimmung" in ResultList addieren.

    @VaporiZed so was in der Art hatte ich glaub ich auch angedacht. Ich hoffe kaskadierende/verschaltete DropDownMenues beschreibt den Lösungsansatz (einigermaßen).

    @Kasi Wenn ich ehrlich bin verstehe ich den Ansatz nicht. Nicht weil er falsch ist oder so ... sondern mein Wissen ist noch zu gering ist, dass ich Dir da folgen kann.

    @ISliceUrPanties Kurze Verständnisfrage. Ich hab mir dein Programm runtergeladen und durchlaufen lassen (Polo, C60) und auch den Link angefangen zu lesen. Ich möchte ja nach verschiedenen Propertys suchen nicht nach mehreren Keywords in einem Property. Ich hab aus Deiner Info herausgelesen, dass man mit diesem Ansatz es schafft "matches" zu finden, wenn der Suchstring so aussehen würde " Farbe: rot oder blau".
    Ich wollte ja eher alle Rote Autos -> dann alle SUVs -> dann eventuell davon alle VWs -> resultListe. Also "Property Color"=rot und dann "Property Hersteller"=VW -> ReusltLst. Ob das jetzt zwei unterschiedliche Paar Schuhe sind hab ich noch nicht verstanden.
    Nichts destotrotz ist die Info super für den Fall, dass ich in einem Property mehrere SuchWorte habe mit AND/OR.


    ---------
    Ich hatte erwartet, dass es da eher eine Standard Lösung für solche Aufgaben gibt. Die Vielfallt der Antworten besagt anderes :)
    Hört sich jetzt blöd an ... ich bleib erstmal bei der Lösung die ich schon geschrieben habe. Aber...


    Tolle, gedankenanregende Antworten von Euch ... Danke :thumbsup:
    ---------
    Mist jetzt muss ich mir wohl doch den PredicateBuilder nochmal genauer ansehen ( @ErfinderDesRades Antwort nach dem verfassen gelesen )
    codewars.com Rank: 4 kyu

    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „nogood“ ()

    Alle deine Wünschen können damit umgesetzt werden. Mit predicate = predicate.And(p => p.ManufacturerName == "Volvo"); kannst du natürlich weitere Eigenschaften in dein Predicate mit aufnehmen. Mit PredicateBuilder.And() kannst du außerdem zwei Predicates miteinander verbinden. Damit hast du alles, was du brauchst. Richtig zusammensetzen musst nur du es noch :)
    @ISliceUrPanties Okay ... Ich hab mir Dein Programm jetzt nicht nur angeschaut, sondern auch rumgespielt/verändert.
    Es macht doch Tatsache was ich erreichen wollte :)
    So habe ich es zum testen geändert :

    C#-Quellcode

    1. var keywords = new[] { "Volkswagen","SUV"};
    2. var predicate = PredicateBuilder.False<Car>();
    3. foreach (var keyword in keywords)
    4. {
    5. predicate = predicate.And(p => p.Type == keyword);
    6. predicate = predicate.Or(p => p.ManufacturerName == keyword);
    7. }

    Ergebnis alle Volkswagen der Klasse SUV (Tiguan, eGolf).

    So jetzt müsste ich nur noch die Grundlagen dazu verstehen, die Du in Deiner Klasse PredicateBuilder benutzt hast.
    Ich versuch mal mein LINQ-Wissen zu verbessern. Im Moment hört es bei einfachen Where ... Select auf. Expression Tree etc. hab ich nur mal so das Wort gesehen. Falls es möglich ist, wäre es toll wenn Du noch kurz antworten könntest...

    1. Frage: Hab ich Dein Beispiel-Programm richtig verändert, um nach allen SUV der Marke VW zu suchen? Ich frage, da ich verschiedene Reihenfolgen OR / AND ausprobiert habe und es mir nicht sofort ersichtlich war warum es genau so
    "anscheinend" geklappt hat!?

    2. Ist das kein Problem, dass man in dem KeyWordsArray Begriffe der unterschiedlichen Propertys mischt (also {"Volkswagen"->ManufacturerName , "SUV"-> Typ})

    3.Ich hab mein Wissen was LINQ betrifft von www.tutorialsteacher.com/linq. Ich hab die Seiten bis "Standard Query Operators // Union" einmalig gelesen. Ich würde jetzt mal versuchen alles bis jetzt gelesene nochmal zu überfliegen und dann dort weiterlesen. Es kommen noch Unterkapitel "Expression", "ExpressionTree" ... Danach dann würde ich den link von Dir und Dein Programm nochmal versuchen zu verstehen (also das ich wirklich weiß was Zeile für Zeile los ist). Hört sich das nach einem effektiven Plan an (nur so nach Bauchgefühl)?

    Also nochmal Danke für die neuen Horizonte.
    codewars.com Rank: 4 kyu

    Dieser Beitrag wurde bereits 2 mal editiert, zuletzt von „nogood“ ()

    Ich werde es auf jeden Fall auch noch genauer anschauen, wenn ich dazukomme.

    Die Beispiele zeigen bis jetzt nur den Umgang mit Strings. Wie sieht es aber aus, wenn ein Property ein Datum oder eine Image ist, oder sogar ein selber erstelltes Klassenobjekt, dass nicht einfach so z.B. in einem DataGridView angezeigt werden kann, aber ich für die Such- und Filter-Verwendung doch irgendwie in Betracht ziehen möchte.

    Wird sicher noch ganz spannen, was damit möglich ist.

    Freundliche Grüsse

    exc-jdbi
    @nogood
    zu deinen Fragen.
    1. Nein. Es ist nur ein Zufall, dass das Ergebnis "richtig" ist.
    2. Ja, das ist ein Problem, wenn du so mehrere "Filter" für unterschiedliche Eigenschaften in eine Variable packst. Eine Möglichkeit, das über eine einzige Liste/Array zu lösen, bestünde darin eine eigene Filterklasse zu verwenden.
      Ich habe das Projekt etwas erweitert und noch mal angehängt. Vielleicht bekommst du so noch einen besseren Einblick in die grundsätzliche Idee. Es ist nicht "schön" (weil viel wiederholter Code), aber funktionell.
    3. Versuch nicht "seriell" eins nach dem anderen zu lesen um dann mein Beispiel "besser" zu verstehen. Schau dir das Beispiel an und das, was du nicht verstehst, schlägst du nach. Expressions (und auch ExpressionTree) ist da aber sicherlich ein gutes Stichwort. :thumbsup:
    Dateien

    nogood schrieb:

    um nach allen SUV der Marke VW zu suchen
    ich denke so:

    C#-Quellcode

    1. var predicate = PredicateBuilder.False<Car>();
    2. predicate = predicate.Or(p => p.ManufacturerName == "VW");
    3. predicate = predicate.And(p => p.Type == "SUV");
    oder

    C#-Quellcode

    1. var predicate = PredicateBuilder.False<Car>().Or(p => p.ManufacturerName == "VW").And(p => p.Type == "SUV");
    @ISliceUrPanties Zum Glück hab ich nochmal nachgefragt:). Danke für die Klärung
    @Kasi Ich hab erstmal genug Neues was ich nacharbeiten muss.
    @ErfinderDesRades Danke (Antwort gerade erst gesehen; keine Ahnung ob das so okay ist)

    Ich versuch mich jetzt erstmal daran den PredicateBuilder Ansatz zu verstehen.
    codewars.com Rank: 4 kyu

    Dieser Beitrag wurde bereits 3 mal editiert, zuletzt von „nogood“ ()

    Ich hab jetzt, aufbauend auf dem Sample aus post#12, einen PredicateBuilder ohne Queryable und Linq.Expressions gebaut:

    C#-Quellcode

    1. public static class PredicateBuilder2 {
    2. public static Func<T, bool> From<T>(Func<T, bool> e) { return e; }
    3. public static Func<T, bool> Or<T>(this Func<T, bool> expr1, Func<T, bool> expr2) {
    4. return new Func<T, bool>(x => expr1(x) || expr2(x));
    5. }
    6. public static Func<T, bool> And<T>(this Func<T, bool> expr1, Func<T, bool> expr2) {
    7. return new Func<T, bool>(x => expr1(x) && expr2(x));
    8. }
    9. }
    geht genausogut.

    C#-Quellcode

    1. var carList = new List<Car>
    2. {
    3. new Car() { Id = 1, Family = "", ManufacturerName = "Volvo", Name = "C60", Type = CarType.SUV, RegistrationDate = new DateTime(2020, 4, 5) },
    4. new Car() { Id = 2, Family = "", ManufacturerName = "Volvo", Name = "C50", Type = CarType.Limousine, RegistrationDate = new DateTime(2008, 8, 1) },
    5. new Car() { Id = 3, Family = "", ManufacturerName = "Volkswagen", Name = "Polo", Type = CarType.Compact, RegistrationDate = new DateTime(2015, 12, 6) },
    6. new Car() { Id = 4, Family = "", ManufacturerName = "Volkswagen", Name = "Golf", Type = CarType.Medium, RegistrationDate = new DateTime(2019, 11, 11) },
    7. new Car() { Id = 5, Family = "", ManufacturerName = "Volkswagen", Name = "Passat", Type = CarType.Limousine, RegistrationDate = new DateTime(2004, 3, 5) },
    8. new Car() { Id = 6, Family = "", ManufacturerName = "Volkswagen", Name = "Arteon", Type = CarType.Limousine, RegistrationDate = new DateTime(2018, 4, 5) },
    9. new Car() { Id = 7, Family = "", ManufacturerName = "Volkswagen", Name = "T-ROC", Type = CarType.CUV, RegistrationDate = new DateTime(2018, 7, 23) },
    10. new Car() { Id = 8, Family = "", ManufacturerName = "Volkswagen", Name = "eGolf", Type = CarType.Medium, RegistrationDate = new DateTime(2020, 1, 1) },
    11. new Car() { Id = 9, Family = "", ManufacturerName = "Volkswagen", Name = "Tiguan", Type = CarType.SUV, RegistrationDate = new DateTime(2020, 9, 5) },
    12. };
    13. var cars = carList.AsQueryable();
    14. List<Expression<Func<Car, bool>>> expressions = new List<Expression<Func<Car, bool>>>();
    15. //AddNameFilter(expressions);
    16. AddTypeFilter(expressions);
    17. AddDateFilter(expressions);
    18. foreach (var car in cars.Where(PredicateBuilder.CombineAnd(expressions))) {
    19. Console.WriteLine($"{car.Id} - {car.Name}");
    20. }
    21. var p1 = PredicateBuilder.False<Car>().Or(c => c.Type == CarType.SUV).Or(c => c.Type == CarType.Compact);
    22. var p2 = PredicateBuilder.False<Car>().Or(c => c.RegistrationDate >= new DateTime(2018, 1, 1));
    23. var p3 = p1.And(p2);
    24. foreach (var car in cars.Where(p3)) {
    25. Debug.WriteLine($"{car.Id} - {car.Name}");
    26. }
    27. var ep = PredicateBuilder2.From<Car>(c => c.Type == CarType.SUV).Or(c => c.Type == CarType.Compact);
    28. var ep2 = PredicateBuilder2.From<Car>(c => c.RegistrationDate >= new DateTime(2018, 1, 1));
    29. var ep3 = ep.And(ep2);
    30. foreach (var car in carList.Where(ep3)) {
    31. Debug.WriteLine($"{car.Id} - {car.Name}");
    32. }
    ergibt dreimal dieselbe Ausgabe ins Output-Fenster