web-dev-qa-db-fra.com

Linq - SelectMany Confusion

D'après ce que j'ai compris de la documentation de SelectMany, on pourrait l'utiliser pour produire une séquence (aplatie) d'une relation à plusieurs.

J'ai cours suivants

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

J'essaie ensuite de les utiliser en utilisant la syntaxe d'expression de requête comme si

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

Cela donne ce dont j'ai besoin.

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

Je suppose que cela se traduit par l'utilisation de la méthode SelectMany lorsque vous n'utilisez pas la syntaxe d'expression de requête?

Quoi qu'il en soit, j'essaie de comprendre ce que j'ai fait avec SelectMany. Donc, même si ma requête ci-dessus ne se traduit pas par SelectMany, étant donné les deux classes et les données fictives, quelqu'un pourrait-il me fournir une requête linq qui utilise SelectMany?

81
Jackie Kirby

Voici votre requête utilisant SelectMany, modélisé exactement d'après votre exemple. Même résultat!

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

Le premier argument établit une correspondance entre chaque client et une collection de commandes (tout à fait analogue à la clause "où" que vous avez déjà).

Le deuxième argument transforme chaque paire correspondante {(c1, o1), (c1, o2) .. (c3, o9)} en un nouveau type, que j'ai identique à votre exemple.

Alors:

  • arg1 mappe chaque élément de la collection de base vers une autre collection.
  • arg2 (optionnel) transforme chaque paire en un nouveau type

La collection résultante est plate comme vous le souhaiteriez dans votre exemple d'origine.

Si vous omettiez le deuxième argument, vous obtiendriez une collection de toutes les commandes correspondant à un client. Ce serait juste cela, une collection plate d'objets Order.

Il faut beaucoup de temps pour s’y habituer et j’ai toujours du mal à comprendre. :(

101
Sapph

SelectMany () fonctionne comme Select, mais avec cette fonctionnalité supplémentaire d’aplatissement d’une collection sélectionnée. Il doit être utilisé chaque fois que vous souhaitez une projection d'éléments de sous-collections, sans vous soucier de l'élément contenant de la sous-collection.

Par exemple, supposons que votre domaine ressemble à ceci:

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

Pour obtenir la même liste que vous vouliez, votre Linq ressemblerait à ceci:

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

... qui produira le même résultat sans avoir besoin de la collection de commandes à plat. SelectMany prend la collection de commandes de chaque client et effectue une itération pour produire un fichier IEnumerable<Order> d'un IEnumerable<Customer>.

27
KeithS

Bien que ce soit une vieille question, je pensais améliorer un peu les excellentes réponses:

SelectMany renvoie une liste (qui peut être vide) pour chaque élément de la liste de contrôle. Chaque élément de ces listes de résultats est énuméré dans la séquence de sortie des expressions et est donc concaténé dans le résultat. Par conséquent, une liste 'liste -> b' [] -> concaténer -> b '.

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

Voici une autre option utilisant SelectMany

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

Si vous utilisez Entity Framework ou LINQ to Sql et que vous avez une association (relation) entre les entités, vous pouvez le faire:

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