2011-02-16 18 views
75

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

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

entonces trato de usarlos utilizando la sintaxis de expresión de consulta al igual que

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 le da a 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 el uso del método SelectMany cuando no se utiliza la sintaxis de la expresión de consulta.

De cualquier manera, estoy tratando de entender cómo se usa 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?

+3

Ver [parte 41] (http://msmvps.com /blogs/jon_skeet/archive/2011/01/28/reimplementing-linq-to-objects-part-41-how-query-expressions-work.aspx) de la [serie Edulinq] de Jon Skeet (http://msmvps.com) /blogs/jon_skeet/archive/tags/Edulinq/default.aspx). Explica el proceso de traducción de la expresión de consulta. –

+2

Pensando en ello, consulte también [Parte 9: SelectMany] (http://msmvps.com/blogs/jon_skeet/archive/2010/12/27/reimplementing-linq-to-objects-part-9-selectmany.aspx) :) –

+3

La serie Edulinq de John Skeet ya está disponible [aquí] (http://codeblog.jonskeet.uk/category/edulinq/). –

Respuesta

96

Aquí está su consulta usando SelectMany, modelado exactamente después de su ejemplo. ¡Mismo resultado!

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

El primer argumento mapas de cada cliente a un conjunto de órdenes (completamente análogo al 'dónde' cláusula que ya tiene).

El segundo argumento transforma cada par coincidente {(c1, o1), (c1, o2) .. (c3, o9)} en un nuevo tipo, que he hecho igual que en el ejemplo.

Así:

  • arg1 mapas de 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 plano como era de esperar en su original ejemplo.

Si omitiera el segundo argumento, terminaría con un conjunto de todas las órdenes que coinciden con un cliente. Sería simplemente eso, una colección plana de objetos Order.

Usarlo lleva mucho tiempo acostumbrándome, todavía me cuesta trabajo acostumbrarme a ello. :(

+2

Gracias por su respuesta y explicación. Eso es exactamente lo que necesitaba. Gracias también por proporcionar una respuesta completamente en el contexto de mi pregunta, lo hace mucho más fácil de entender. –

+0

Por el bien de Pete, ¿por qué poner el .Where() * dentro * de SelectMany() me elude por tanto tiempo? Gracias por señalar eso ... –

+0

Solo para el registro, 'GroupBy' podría ser una mejor opción para este escenario en particular. – Ekevoo

26

SelectMany() funciona como Seleccionar, pero con esa característica adicional de aplanar una colección que se selecciona. Debe usarse siempre que desee una proyección de elementos de subcolecciones, y no le importa el elemento que contiene la subcolección.

Por ejemplo, digamos que su dominio veía 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 quería, su LINQ sería algo como esto:

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

... que será producir el mismo resultado sin necesidad de la colección plana de pedidos. SelectMany toma cada colección de Órdenes del Cliente y la repite para producir un IEnumerable<Order> desde un IEnumerable<Customer>.

+2

* "(...) y no le importa el elemento que contiene la subcolección." * Si desea el aplanamiento, y si le importa el elemento que lo contiene, hay una [sobrecarga de SelectMany] (http://msdn.microsoft.com/en-us/library/bb534631.aspx) :) –

+0

@Keith gracias por su respuesta. ¿Cómo lo usaría con una colección de pedidos plana? –

+0

Tu dominio parece un poco cuestionable. ¿Un pedido contiene un cliente que a su vez contiene muchos pedidos? –

5

Aunque esta es una vieja pregunta, pensé que iba a mejorar las excelentes respuestas un poco:.

SelectMany devuelve una lista (que puede estar vacía) para cada elemento de la lista de control Cada elemento de éstos listas de resultados se enumeran en las expresiones secuencia de salida y así se concatenan en el resultado por lo tanto, a. 'lista ->' lista [] - concatenate> -> b' 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); 
     } 
    } 
} 
0

Aquí es otra opción. utilizando SelectMany

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

si se utiliza el marco de la entidad o LINQ to SQL y tiene una asociación (relación) entre las entidades, a continuación, puede hacerlo:

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