Linq - SelectMany Confusion

81

Por lo que entiendo de la documentación de SelectMany, se podría usar para producir una secuencia (aplanada) de una relación de 1 a muchos.

Tengo las siguientes clases

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

Luego trato de usarlos usando la sintaxis de expresión de consulta así

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

Esto da lo que necesito.

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

Supongo que esto se traduce en usar el método SelectMany cuando no se usa la sintaxis de la expresión de consulta.

De cualquier manera, estoy tratando de entender el uso de SelectMany. Entonces, incluso si mi consulta anterior no se traduce a SelectMany, dadas las dos clases y los datos simulados, ¿podría alguien proporcionarme una consulta de linq que use SelectMany?

Jackie Kirby
fuente
3
Vea la parte 41 de la serie Edulinq de Jon Skeet . Explica el proceso de traducción de expresiones de consulta.
R. Martinho Fernandes
2
Pensando en ello, vea también la Parte 9: SelectMany :)
R. Martinho Fernandes
3
La serie Edulinq de John Skeet ya está disponible aquí .
Dan Jagnow

Respuestas:

101

Aquí está su consulta usando SelectMany, modelada exactamente según su ejemplo. ¡Misma salida!

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

El primer argumento asigna a cada cliente a una colección de pedidos (completamente análogo a la cláusula "dónde" que ya tiene).

El segundo argumento transforma cada par emparejado {(c1, o1), (c1, o2) .. (c3, o9)} en un nuevo tipo, que hice igual que en su ejemplo.

Entonces:

  • arg1 asigna cada elemento de la colección base a otra colección.
  • arg2 (opcional) transforma cada par en un nuevo tipo

La colección resultante es plana, como cabría esperar en su ejemplo original.

Si omitiera el segundo argumento, terminaría con una colección de todos los pedidos que coinciden con un cliente. Sería solo eso, una colección plana de Orderobjetos.

Me cuesta mucho acostumbrarme a usarlo, a veces todavía tengo problemas para entenderlo. :(

Zafiro
fuente
2
Gracias por tu respuesta y explicación. Eso es exactamente lo que necesitaba. Gracias también por dar una respuesta completamente en el contexto de mi pregunta, hace que sea mucho más fácil de entender.
Jackie Kirby
1
Por el amor de Pete, ¿por qué poner .Where () dentro de SelectMany () me eludió durante tanto tiempo? Gracias por señalar eso ...
Tobias J
Solo para el registro, GroupBypodría ser una mejor opción para este escenario en particular.
Ekevoo
27

SelectMany () funciona como Select, pero con esa característica adicional de acoplar una colección seleccionada. Debe usarse siempre que desee una proyección de elementos de subcolecciones, y no le importe el elemento contenedor de la subcolección.

Por ejemplo, digamos que su dominio se ve así:

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

Para obtener la misma lista que deseaba, su Linq se vería así:

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

... que producirá el mismo resultado sin necesidad de la recogida plana de Pedidos. La toma SelectMany recogida y itera pedidos de cada cliente a través de que para producir una IEnumerable<Order>de una IEnumerable<Customer>.

KeithS
fuente
3
"(...) y no me importa el elemento contenedor de la subcolección". Si desea el aplanamiento, y le importa el elemento contenedor, hay una sobrecarga de SelectMany para eso :)
R. Martinho Fernandes
@Keith gracias por tu respuesta. ¿Cómo lo usaría con una colección plana de pedidos?
Jackie Kirby
Tu dominio parece un poco cuestionable. ¿Un pedido contiene un cliente que a su vez contiene muchos pedidos?
Buh Buh
@Buh Buh, no, un pedido contiene un CustomerId, no un Customer.
Jackie Kirby
1
@Buh Buh - He visto y hecho esto muchas veces; da como resultado un gráfico de objetos que se puede recorrer en cualquier dirección, no solo de arriba hacia abajo. Muy útil si su gráfico tiene varios "puntos de entrada". Si usa un ORM como NHibernate, es trivial incluir la referencia inversa porque ya existe en la tabla secundaria. Solo tienes que romper la referencia circular indicando que las cascadas bajan, no suben.
KeithS
5

Aunque esta es una pregunta antigua, pensé que mejoraría un poco las excelentes respuestas:

SelectMany devuelve una lista (que puede estar vacía) para cada elemento de la lista de control. Cada elemento de estas listas de resultados se enumeran en la secuencia de salida de las expresiones y, por lo tanto, se concatenan en el resultado. Por lo tanto, una lista 'lista -> b' [] -> concatenar -> b 'lista.

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
fuente
1

Aquí hay otra opción usando SelectMany

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

Si usa Entity Framework o LINQ to Sql y tiene una asociación (relación) entre las entidades, entonces puede hacerlo:

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