Practical .NET

Getting All the Results in LINQ with Methods

Peter follows up on advice for joining collections with LINQ and getting all of the results, but this time, he uses the method-based syntax.

A little while back, I did a column on how to join two collections in LINQ to generate queries like, "Get me all of the customers, with their salesorders (if they have any)," or, "Get me all of the customers who have no salesorders." Generating these queries requires something like the SQL outer join and I walked through constructing that query in the column.

I got some feedback from readers of that column who use the LINQ method-based syntax (I used the SQL-like syntax in the column). This tip addresses that feedback by showing the method-based syntax, again in both Visual Basic and C# (though, I suspect that most programmers could translate from one syntax to the other).

Joining Collections with LINQ
Your first step is to call the GroupJoin method from the collection you want all the objects from (that's Customers in my example). You must pass the GroupJoin method four parameters:

  • The collection you want to join to (the collection that might be missing items; SalesOrders, in this case)
  • A lambda expression specifying the property from the collection you called GroupJoin from that you want to use in the join (the Id from the Customer object)
  • A lambda expression specifying the property from the collection in the first parameter that you want to use in the join (the CustomerId from the SalesOrder object)
  • The equivalent of a select statement that's used to build the object that you want returned

That final lambda expression is passed one object from the initial collection (Customers, in my case) and a collection of all the matching objects from the other collection (SalesOrders, in my case). In this example, I'm using that last lambda expression to build an anonymous object with two properties:

  • One called Key that holds the CustomerId
  • One called SalesOrders that holds all the SalesOrders

The resulting code looks like this in Visual Basic:

Dim = db.Customers.GroupJoin(db.SalesOrders,
                             Function(c) c.Id,
                             Function(s) s.CustomerId,
                             Function(c, s) New With {.Key = c, .SalesOrders = s})

In C#, it looks like this:

var mos = db.Customers.GroupJoin(db.Salesorders,
                                 c => c.Id,
                                 s => s.CustomerId,
                                 (c, s) => new { Key = c.Id, SalesOrders = s })

That will give you a result. Specifically, you'll end up with a collection of objects with each object having a property holding single CustomerId and another property holding the collection of SalesOrder objects for that CustomerId. That may not be the result you want.

Getting a Flattened Result
What you probably want is a collection of objects with each object having the property with the single Customer Id and another property holding a single SalesOrder object (the Customer Id will repeat across multiple objects, once for each SalesOrder). For this kind of transformation ("flattening" a result), you need to call the SelectMany method.

A warning: My SelectMany call is a little ugly, for two reasons. First, I need to call the DefaultIfAnyMethod on the SalesOrders collection -- this method will provide a null/Nothing value when there's no matching SalesOrder for a Customer (this prevents LINQ dropping Customers because there's no matching object). Second, I need to use an Immediate If to handle working with that null value when there's a Customer with no matching SalesOrder. In the following code, I've used the name ("mo") to refer to the objects returned from my GroupJoin method.

The SelectMany method requires two parameters (both lambda expressions). The first parameter specifies where matching objects are drawn from. In this case that's the property holding my SalesOrders collection. I call the DefaultIfEmpty method here to supply a value, even when the property is empty because there were no matching salesorders for the Customer.

In C#, the code looks like this:

var res = mos.SelectMany(mo => mo.SalesOrders.DefaultIfEmpty(),

In Visual Basic, like this:

Dim res = mos.SelectMany(Function(mo) mo.SalesOrders.DefaultIfEmpty(),

The second parameter is like a Select method: It's where you specify the object you'll return from the SelectMany call. The lambda expression you pass here must accept two parameters: each object from the collection you're processing (in this case, that's the object from my GroupJoin) and the matching object from the first parameter.

I return an anonymous object consisting of the Customer Id and the OrderId from the SalesOrder selected in the first parameter. To handle the null/Nothing values I get from that first parameter, I use an Immediate If to supply a default value.

Putting that all together, my SelectMany looks like this in C#:

Dim res = mos.SelectMany(mo => mo.SalesOrders.DefaultIfEmpty(),
                        (c, s) => new { c.Key.Id, OrderId = s == null ? "Not Found" : s.Id.ToString() });

and this in Visual Basic:

Dim res = mos.SelectMany(Function(mo) mo.SalesOrders.DefaultIfEmpty(),
                         Function(c, s) New With {c.Key.Id, .OrderId = If(s Is Nothing, "Not Found", s.Id.ToString)})

If you want to do it in one LINQ statement (in order, I suspect, to frighten the next programmer), it looks like this in C#:

var res = db.Customers.GroupJoin(db.Salesorders,
                                 c => c.Id,
                                 s => s.CustomerId,
                                (c, s) => new { Key = c, SalesOrders = s }).
                       SelectMany(mo => mo.SalesOrders.DefaultIfEmpty(),
                                 (c, s) => new { c.Key.Id, OrderId = s == null ? "Not Found" : s.Id.ToString() });

and this in Visual Basic:

Dim = db.Customers.GroupJoin(db.SalesOrders,
                             Function(c) c.Id,
                             Function(s) s.CustomerId,
                             Function(c, s) New With {.Key = c, .SalesOrders = s}).
                    SelectMany(Function(mo) mo.SalesOrders.DefaultIfEmpty(),
                               Function(c, s) New With {c.Key.Id, .OrderId = If(s Is Nothing, "Not Found", s.Id.ToString)})

(Degree of fright is dependent on the next programmer's level of expertise, of course ...)

About the Author

Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter tweets about his VSM columns with the hashtag #vogelarticles. His blog posts on user experience design can be found at http://blog.learningtree.com/tag/ui/.

comments powered by Disqus

Featured

  • Full Stack Hands-On Development with .NET

    In the fast-paced realm of modern software development, proficiency across a full stack of technologies is not just beneficial, it's essential. Microsoft has an entire stack of open source development components in its .NET platform (formerly known as .NET Core) that can be used to build an end-to-end set of applications.

  • .NET-Centric Uno Platform Debuts 'Single Project' for 9 Targets

    "We've reduced the complexity of project files and eliminated the need for explicit NuGet package references, separate project libraries, or 'shared' projects."

  • Creating Reactive Applications in .NET

    In modern applications, data is being retrieved in asynchronous, real-time streams, as traditional pull requests where the clients asks for data from the server are becoming a thing of the past.

  • AI for GitHub Collaboration? Maybe Not So Much

    No doubt GitHub Copilot has been a boon for developers, but AI might not be the best tool for collaboration, according to developers weighing in on a recent social media post from the GitHub team.

  • Visual Studio 2022 Getting VS Code 'Command Palette' Equivalent

    As any Visual Studio Code user knows, the editor's command palette is a powerful tool for getting things done quickly, without having to navigate through menus and dialogs. Now, we learn how an equivalent is coming for Microsoft's flagship Visual Studio IDE, invoked by the same familiar Ctrl+Shift+P keyboard shortcut.

Subscribe on YouTube