SelectMany
in Entity Framework Core is an operator that flattens out the collection
of collections
into one single collection
of objects. It is a very useful operator with many use cases.
Database:
The Database for this tutorial is taken from the chinook database.Source Code:
The source code of this project available in GitHub. It also contains the script of the database
Example Query
Consider the following query. The query returns list of Customers
along with their Invoices
.
1 2 3 4 5 6 7 8 | SELECT FirstName, InvoiceDate, Total FROM Customer C LEFT JOIN Invoice I ON C.CustomerId = I.CustomerId WHERE FirstName LIKE 'A%' ORDER BY C.CustomerId, i.InvoiceId |
We can write the above query in Entity Framework Core as follows. The query returns the collection of Customers
. Under each Customer
we have collection
of Invoices
. The result is a collection
of collections
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | using (ChinookContext db = new ChinookContext()) { var customers = db.Customer .Where(c => c.FirstName.StartsWith("A")) .Select( c => new { c.FirstName, c.Invoice, } ) .ToList(); foreach (var customer in customers) { Console.WriteLine("{0}", customer.FirstName); foreach (var invoice in customer.Invoice) { Console.WriteLine("\t\t {0} {1}", invoice.InvoiceDate, invoice.Total); } } } |
SelectMany
The selectMany
helps flattens out the collection
of collections
into one single collection
of objects. Now let use re write the above query using the selectMany
In the example below Customers
is the first or outer collection
. We use the SelectMany
method on the Customers
collection
1 2 3 4 5 | db.ProductModels .Where(m=> m.Name.StartsWith("C")) .SelectMany( |
We need to choose the second or inner collection
as the first argument to the SelectMany
, which is Invoices
1 2 3 | SelectMany( c => c.Invoice |
Next, we need to choose the shape of our output using a projection query. The lambda expression gets two arguments. The first argument is a reference to the outer collection (Customers
). The second is a reference to the inner collection (Invoices
).
The query returns a single collection, which is similar to what you get when you execute the SQL
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | using (ChinookContext db = new ChinookContext()) { var customers = db.Customer .Where(c => c.FirstName.StartsWith("A")) .SelectMany( c => c.Invoice, (c,i) => new { c.FirstName, i.InvoiceDate, i.Total } ) .ToList(); foreach (var customer in customers) { Console.WriteLine("{0} {1} {2}", customer.FirstName, customer.InvoiceDate, customer.Total); } } //QUERY SELECT [c].[FirstName], [i].[InvoiceDate], [i].[Total] FROM [Customer] AS [c] INNER JOIN [Invoice] AS [i] ON [c].[CustomerId] = [i].[CustomerId] WHERE [c].[FirstName] LIKE N'A%' |
SelectMany Multiple collections
You can chain of selectMany
methods as shown below.
In our first SelectMany
, we have included InvoiceLine
collection in the projection. The result of this projection becomes outer
collection for the next SelectMany
1 2 3 4 5 6 7 8 9 10 | .SelectMany(c => c.Invoice, (c, i) => new { c.FirstName, i.InvoiceDate, i.Total, i.InvoiceLine //InvoiceLine is collection navigation property. is projected along with other data } ) |
Now in the second selectMany
we can flatten it again into a single collection.
1 2 3 | .SelectMany(p => p.InvoiceLine |
Also note that in the final projection we have included Track.Name
and Track.Album.Title
.They are reference navigational properties and not collections. Hence we do not have to use SelectMany
on them.
1 2 3 4 | TrackName = i.Track.Name, i.Track.Album.Title |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | using (ChinookContext db = new ChinookContext()) { var customers = db.Customer .Where(c => c.FirstName.StartsWith("A")) .SelectMany(c => c.Invoice, (c, i) => new { c.FirstName, i.InvoiceDate, i.Total, i.InvoiceLine } ) .SelectMany(p => p.InvoiceLine, (o,i) => new { o.FirstName, o.InvoiceDate, o.Total, i.Quantity, i.UnitPrice, TrackName=i.Track.Name, //Reference Property i.Track.Album.Title //Reference Property } ) .ToList(); foreach (var customer in customers) { Console.WriteLine("{0} {1} {2} {3} {4} {5} {6}", customer.FirstName, customer.InvoiceDate,customer.TrackName, customer.Title, customer.Quantity, customer.UnitPrice, customer.Total); } } //SQL SELECT [c].[FirstName], [i].[InvoiceDate], [i].[Total], [i0].[Quantity], [i0].[UnitPrice], [t].[Name] AS [TrackName], [a].[Title] FROM [Customer] AS [c] INNER JOIN [Invoice] AS [i] ON [c].[CustomerId] = [i].[CustomerId] INNER JOIN [InvoiceLine] AS [i0] ON [i].[InvoiceId] = [i0].[InvoiceId] INNER JOIN [Track] AS [t] ON [i0].[TrackId] = [t].[TrackId] LEFT JOIN [Album] AS [a] ON [t].[AlbumId] = [a].[AlbumId] WHERE [c].[FirstName] LIKE N'A%' |
References
Summary
SelectMany
converts collection
of collections
into one single collection of objects. You can also use it flatten the multiple hierarchical collections
into a single collection
.
Great job!
I would appreciate if you add an example with nested collections
Thanks
Very clear. Great!!!