Linq - SelectMany Confusion

81

Nach dem, was ich aus der Dokumentation von SelectMany verstehe, könnte man damit eine (abgeflachte) Sequenz einer 1-viele-Beziehung erzeugen.

Ich habe folgende Klassen

  public class Customer
  {
    public int Id { get; set; }
    public string Name { get; set; }
  }

  class Order
  {
    public int Id { get; set; }
    public int CustomerId { get; set; }
    public string Description { get; set; }
  }

Ich versuche dann, sie unter Verwendung der Abfrageausdruckssyntax wie folgt zu verwenden

  var customers = new Customer[]
  {
    new Customer() { Id=1, Name ="A"},
    new Customer() { Id=2, Name ="B"},
    new Customer() { Id=3, Name ="C"}
  };

  var orders = new Order[]
  {
    new Order { Id=1, CustomerId=1, Description="Order 1"},
    new Order { Id=2, CustomerId=1, Description="Order 2"},
    new Order { Id=3, CustomerId=1, Description="Order 3"},
    new Order { Id=4, CustomerId=1, Description="Order 4"},
    new Order { Id=5, CustomerId=2, Description="Order 5"},
    new Order { Id=6, CustomerId=2, Description="Order 6"},
    new Order { Id=7, CustomerId=3, Description="Order 7"},
    new Order { Id=8, CustomerId=3, Description="Order 8"},
    new Order { Id=9, CustomerId=3, Description="Order 9"}
  };

  var customerOrders = from c in customers
                       from o in orders
                       where o.CustomerId == c.Id
                       select new 
                              { 
                                 CustomerId = c.Id
                                 , OrderDescription = o.Description 
                              };

  foreach (var item in customerOrders)
    Console.WriteLine(item.CustomerId + ": " + item.OrderDescription);

Das gibt was ich brauche.

1: Order 1
1: Order 2
1: Order 3
1: Order 4
2: Order 5
2: Order 6
3: Order 7
3: Order 8
3: Order 9

Ich gehe davon aus, dass dies zur Verwendung der SelectMany-Methode führt, wenn die Syntax des Abfrageausdrucks nicht verwendet wird.

In beiden Fällen versuche ich, meinen Kopf mit SelectMany herumzuwickeln. Könnte mir jemand eine Linq-Abfrage mit SelectMany zur Verfügung stellen, selbst wenn meine obige Abfrage angesichts der beiden Klassen und Scheindaten nicht in SelectMany übersetzt wird?

Jackie Kirby
quelle
3
Siehe Teil 41 der Edulinq-Reihe von Jon Skeet . Es erklärt den Übersetzungsprozess für Abfrageausdrücke.
R. Martinho Fernandes
2
Denken Sie darüber nach, siehe auch Teil 9: SelectMany :)
R. Martinho Fernandes
3
John Skeets Edulinq-Serie ist jetzt hier verfügbar .
Dan Jagnow

Antworten:

101

Hier ist Ihre Abfrage mit SelectMany, genau nach Ihrem Beispiel modelliert. Gleiche Ausgabe!

        var customerOrders2 = customers.SelectMany(
            c => orders.Where(o => o.CustomerId == c.Id),
            (c, o) => new { CustomerId = c.Id, OrderDescription = o.Description });

Das erste Argument ordnet jeden Kunden einer Sammlung von Bestellungen zu (völlig analog zu der bereits vorhandenen 'where'-Klausel).

Das zweite Argument wandelt jedes übereinstimmende Paar {(c1, o1), (c1, o2) .. (c3, o9)} in einen neuen Typ um, den ich wie in Ihrem Beispiel erstellt habe.

So:

  • arg1 ordnet jedes Element in der Basissammlung einer anderen Sammlung zu.
  • arg2 (optional) wandelt jedes Paar in einen neuen Typ um

Die resultierende Sammlung ist flach, wie Sie es in Ihrem ursprünglichen Beispiel erwarten würden.

Wenn Sie das zweite Argument weglassen würden, würden Sie am Ende eine Sammlung aller Bestellungen erhalten, die einem Kunden entsprechen. Es wäre genau das, eine flache Sammlung von OrderObjekten.

Die Verwendung ist sehr gewöhnungsbedürftig, manchmal habe ich immer noch Probleme, meinen Kopf darum zu wickeln. :(

Sapph
quelle
2
Vielen Dank für Ihre Antwort und Erklärung. Genau das brauchte ich. Vielen Dank auch dafür, dass Sie im Zusammenhang mit meiner Frage eine Antwort gegeben haben, die das Verständnis erheblich erleichtert.
Jackie Kirby
1
Um Pete's willen, warum ist mir das Einfügen von .Where () in SelectMany () so lange entgangen? Vielen Dank für den Hinweis ...
Tobias J
Nur für die Aufzeichnung, GroupBykönnte eine bessere Option für dieses spezielle Szenario sein.
Ekevoo
27

SelectMany () funktioniert wie Select, jedoch mit der zusätzlichen Funktion, eine ausgewählte Sammlung zu reduzieren. Es sollte immer dann verwendet werden, wenn Sie eine Projektion von Elementen von Untersammlungen wünschen, und sich nicht um das enthaltende Element der Untersammlung kümmern.

Angenommen, Ihre Domain sah folgendermaßen aus:

public class Customer
  {
    public int Id { get; set; }
    public string Name { get; set; }
    public List<Order> Orders { get; set; }
  }

  class Order
  {
    public int Id { get; set; }
    public Customer Customer { get; set; }
    public string Description { get; set; }
  }

Um die gleiche Liste zu erhalten, die Sie wollten, würde Ihr Linq ungefähr so ​​aussehen:

var customerOrders = Customers
                        .SelectMany(c=>c.Orders)
                        .Select(o=> new { CustomerId = o.Customer.Id, 
                                           OrderDescription = o.Description });

... die das gleiche Ergebnis erzielen, ohne dass die flache Sammlung von Bestellungen erforderlich ist. Das SelectMany nimmt die Auftragssammlung jedes Kunden und durchläuft diese, um eine IEnumerable<Order>aus einer zu erstellen IEnumerable<Customer>.

KeithS
quelle
3
"(...) und kümmere dich nicht um das enthaltende Element der Untersammlung." Wenn Sie die Abflachung wünschen und sich um das enthaltende Element kümmern, gibt es eine Überladung von SelectMany dafür :)
R. Martinho Fernandes
@ Keith danke für deine Antwort. Wie würde ich es mit einer flachen Sammlung von Bestellungen verwenden?
Jackie Kirby
Ihre Domain sieht etwas fragwürdig aus. Eine Bestellung enthält einen Kunden, der wiederum viele Bestellungen enthält?
Buh Buh
@Buh Buh, nein eine Bestellung enthält einen KundenId kein Kunde.
Jackie Kirby
1
@Buh Buh - ich habe das schon oft gesehen und getan; Dies führt zu einem Objektdiagramm, das in jede Richtung und nicht nur von oben nach unten durchlaufen werden kann. Sehr nützlich, wenn Ihr Diagramm mehrere "Einstiegspunkte" hat. Wenn Sie ein ORM wie NHibernate verwenden, ist es trivial, die Rückreferenz einzuschließen, da sie bereits in der untergeordneten Tabelle vorhanden ist. Sie müssen nur den Zirkelverweis brechen, indem Sie angeben, dass die Kaskaden nach unten und nicht nach oben gehen.
KeithS
5

Obwohl dies eine alte Frage ist, dachte ich, ich würde die hervorragenden Antworten ein wenig verbessern:

SelectMany gibt für jedes Element der Controlling-Liste eine Liste zurück (die möglicherweise leer ist). Jedes Element in diesen Ergebnislisten wird in der Ausgabesequenz der Ausdrücke aufgelistet und somit mit dem Ergebnis verknüpft. Daher eine 'Liste -> b' Liste [] -> verketten -> b 'Liste.

using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Linq;
using System.Diagnostics;
namespace Nop.Plugin.Misc.WebServices.Test
{
    [TestClass]
    public class TestBase
    {
        [TestMethod]
        public void TestMethod1()
        {  //See result in TestExplorer - test output 
            var a = new int[]{7,8};
            var b = new int[]
                    {12,23,343,6464,232,75676,213,1232,544,86,97867,43};
            Func<int, int, bool> numberHasDigit = 
                    (number
                     , digit) => 
                         ( number.ToString().Contains(digit.ToString()) );

            Debug.WriteLine("Unfiltered: All elements of 'b' for each element of 'a'");
            foreach(var l in a.SelectMany(aa => b))
                Debug.WriteLine(l);
            Debug.WriteLine(string.Empty);
            Debug.WriteLine("Filtered:" +  
            "All elements of 'b' for each element of 'a' filtered by the 'a' element");
            foreach(var l in a.SelectMany(aa => b.Where(bb => numberHasDigit(bb, aa))))
                Debug.WriteLine(l);
        }
    }
}
George
quelle
1

Hier ist eine weitere Option mit SelectMany

var customerOrders = customers.SelectMany(
  c => orders.Where(o => o.CustomerId == c.Id)
    .Select(p => new {CustomerId = c.Id, OrderDescription = p.Description}));

Wenn Sie das Entity Framework oder LINQ to Sql verwenden und eine Zuordnung (Beziehung) zwischen den Entitäten haben, können Sie dies tun:

var customerOrders = customers.SelectMany(
  c => c.orders
   .Select(p => new {CustomerId = c.Id, OrderDescription = p.Description}));
Владимир Береза
quelle