C# LINQ from beginner to expert - Part 4

As promised back in Part 3 time for worked examples using the operations already covered: Select(), SelectMany(), Where() and Aggregate(). Also know as map, fmap, filter and fold.

Data Structures

Before we dive in, here is the model we will query. I have given them a "real world" sort of feel, structured for query examples I want to show. Probably not a good model to build a real system on :)

// List of shows/events available
public class Event
{
    public int EventId {get;set;}
    public string Performer {get; set;}
    public string ShowName {get; set;}
    public string Venue {get; set;}
    public string Date {get; set;}
}

// An 
public class Order
{
    public int OrderId {get;set;}
    public int EventId {get;set;}
    public Person[] People {get;set;}
    public string[] SeatCodes {get;set;}
}

public class Person
{
    public int PersonId {get;set;}
    public string FirstName {get;set;}
    public string LastName {get;set;}
}

A simple enough structure but complex enough for our needs here :)

Time for some fun

We will start off gentle and build up. First we build a list of people as "LastName, FirstName" which are attending a given event.

var peopleAttendingEvent = orders
    // Get all the orders for the event
    .Where(order => order.EventId == requiredEventId)
    // Flattern the list of people
    .SelectMany(order => order.People)
    // Project the name into the required shape
    .Select(person => $"{person.LastName}, {person.FirstName}")
    // Execute
    .ToArray();

That should be easy enough to follow if you have been through Part 1, Part 2 and Part 3. Refer back if you need to :)

If you look at the query, you should notice it's just a series of simple logical steps. Think about how that might look without LINQ, I bet you are soon deep within nested for loops building an array of names.

Before moving on a word on complexity. When you start to use LINQ you will quickly learn that queries can get complex spanning far too many lines. Before you know it you have a monster query that will be difficult to maintain. So in the next example you will learn how to manage that complexity and break queries into reusable building blocks.

Time to build a seating plan for the same event?

// Pair up the names with seats in an order, this assumes
// there are an equal number of elements in People and Seats
// Uses the version of Select() with the position index
IEnumerable<string>> BuildOrderSeating(Order order)
{
    return order.People.Select(
        (person, index) => 
        {
            var seat = $"Seat {order.SeatCodes[index)}: ";
            var fullName = $"{person.LastName}, {person.FirstName}";
            return seat + fullName;
        });
}

var seatingPlan = orders
    .Where(order => order.EventId == requiredEventId)
    .SelectMany(BuildOrderSeating)
    .ToArray();

So we have a nice reusable function that for an order returns a collection of the names paired with their assigned seat. Because the query in the function has not been executed via .ToArray() or similar it means the collection produced by the function is still lazy.

To build the seating plan we filter the orders as we did before and then for each order we get its seating plan. We are able to pass BuildOrderSeating directly to SelectMany() because the signatures match up.

If you refer back to Part 2 you will see SelectMany() expects a function from a "thing" to a collection of "things". Our function takes an Order and returns a collection or strings so it's a match.

It is well worth learning how to break apart queries into composable blocks like this. You end up with a bunch of Lego blocks you can plug together in different ways. To highlight this I will refactor the two queries we already have and break them apart some more, all I am really doing is simple reduction by removing all duplicate statements in the code.

First the building blocks we will use, You should be able to see all of these from the above queries. They are all simple, focused and if you think about it, near impossible to screw up :)

// Build a full name for a person
Func<Person, string> formatName =
    person => $"{person.LastName}, {person.FirstName};

// Constructs a predicate used to filter orders
Func<Order, bool> HasEventId(int requiredEventId)
    => order => order.EventId == requiredEventId;

IEnumerable> BuildOrderSeating(Order order)
{
    return order.People.Select(
        (person, index) => 
        {
            var seat = $"Seat {order.SeatCodes[index)}: ";
            return seat + formatName(person);
        });
}

// And now the pay off. Clean obvious code 
// that tells you exactly what is going on.

var peopleAttendingEvent = orders
    .Where(HasEventId(requiredEventId))
    .SelectMany(order => order.People)
    .Select(formatName);

var seatingPlan = orders
    .Where(HasEventId(requiredEventId))
    .SelectMany(BuildOrderSeating);

I have left off the execution in the queries, the ToArray(). This is intentional, you can execute them when you need the results. As they stand they are another building block. The final example for this part gets a simple count of the seats sold.

// First the obvious choice
var seatsSold =  orders
    .Where(HasEventId(requiredEventId))
    .Select(order => order.SeatCodes.Length)
    .Sum();

// But we could also do this
var seatsSold = seatingPlan.Count();

As with all software, sometimes you compromise performance to build on what you have. Not executing your queries until you want the results means you have them for building blocks later on.

This is probably a good place to break for this part. You probably need time to digest this lot and experiment a little.

Feel free to ask any questions you have below and upvote if you are enjoying the series, it helps me know I am on the right track. Do you want more samples or explore some more of the operations available in LINQ?

Happy coding

Woz

H2
H3
H4
3 columns
2 columns
1 column
1 Comment