LINQ query operators: lose that foreach already!


Now that .NET 3.5 is out with all its LINQ query operator goodness, I feel like going on a mean streak of trashing a lot of our (now) pointless foreach loops.  Some example operations include:

  • Transformations
  • Aggregations
  • Concatenations
  • Filtering

As I mentioned in my last post, temporary list creation is a great pointer to find opportunities for losing the foreach statements.  I keep the foreach statement when the readability and understandability of the code drops with the LINQ change, but otherwise, a lot less temporary objects are floating around.  Personally, the jury is still out for me whether it’s clearer to return “IEnumerable” over “LineItem[]”, but the temporary array creation doesn’t seem to have much of a point.

Transformations

Transformations are easy to spot. You’ll create a new List, then loop through some other List and create a Something from the OtherThing:

public OrderSummary[] FindOrdersFor(int customerId)
{
    IEnumerable<Order> orders = GetOrdersForCustomer(customerId);
    List<OrderSummary> orderSummaries = new List<OrderSummary>();

    foreach (Order order in orders)
    {
        orderSummaries.Add(new OrderSummary
                            {
                                CustomerName = order.Customer.Name,
                                DateSubmitted = order.DateSubmitted,
                                OrderTotal = order.GetTotal()
                            });
    }

    return orderSummaries.ToArray();
}

Note the temporary list creation, just to return an array.  With LINQ query operators, I can use the Select method to do the same transformation in less code:

public OrderSummary[] FindOrdersFor(int customerId)
{
    return GetOrdersForCustomer(customerId)
        .Select(order => new OrderSummary
                             {
                                 CustomerName = order.Customer.Name,
                                 DateSubmitted = order.DateSubmitted,
                                 OrderTotal = order.GetTotal()
                             })
        .ToArray();
}

By chaining the methods together, it comes out much more readable.

Aggregations

Aggregations can be found when you’re looping through some list for some kind of calculation.  For example, the GetTotal method on Order loops to build up the total based on each item’s ItemPrice:

public decimal GetTotal()
{
    decimal total = 0.0m;

    foreach (LineItem lineItem in GetLineItems())
    {
        total += lineItem.ItemPrice;
    }

    return total;
}

Again, this can be greatly reduced using LINQ query operators and the Sum method:

public decimal GetTotal()
{
    return GetLineItems()
        .Sum(item => item.ItemPrice);
}

Not only is the code much smaller, but the intent is much easier to discern.  Sometimes a calculation can be tricky, and in those cases LINQ isn’t bringing anything to the table.  As always, use good judgement and keep an eye on readability.

Concatenations

Oftentimes I need to combine many lists into one flattened list.  For example, suppose I need a list of OrderLineItem summary items, perhaps to display on a grid to the end user.  However, I want to display all order line items for all orders, which is difficult to build up manually:

public LineItemSummary[] FindLineItemsFor(int customerId)
{
    IEnumerable<Order> orders = GetOrdersForCustomer(customerId);
    List<LineItemSummary> lineItemSummaries = new List<LineItemSummary>();

    foreach (Order order in orders)
    {
        IEnumerable<LineItemSummary> tempSummaries = TransformLineItems(order, order.GetLineItems());

        lineItemSummaries.AddRange(tempSummaries);
    }

    return lineItemSummaries.ToArray();
}

Note the two temporary lists: one to hold the concatenated list, and the other to hold each result as we loop through.  With the SelectMany method, this becomes much shorter:

public LineItemSummary[] FindLineItemsFor(int customerId)
{
    return GetOrdersForCustomer(customerId)
        .SelectMany(order => TransformLineItems(order, order.GetLineItems()))
        .ToArray();
}

No temporary lists are created, and all of the LineItemSummary objects are concatenated correctly.  Nested foreach loops as well as the AddRange method are indicators that the SelectMany method could be used.

Filtering

Filtering looks similar to transformations, except there’s an “if” statement that controls adding to the temporary list.  Suppose we want only the expensive LineItemSummary items:

public LineItemSummary[] FindExpensiveLineItemsFor(int customerId)
{
    IEnumerable<Order> orders = GetOrdersForCustomer(customerId);
    List<LineItemSummary> lineItemSummaries = new List<LineItemSummary>();

    foreach (Order order in orders)
    {
        foreach (LineItem item in order.GetLineItems())
        {
            if (item.ItemPrice > 100.0m)
                lineItemSummaries.Add(new LineItemSummary
                                          {
                                              CustomerName = order.Customer.Name,
                                              DateSubmitted = order.DateSubmitted,
                                              ItemPrice = item.ItemPrice,
                                              ItemName = item.ProductName
                                          });
        }
    }

    return lineItemSummaries.ToArray();
}

This example has both concatenation and filtering.  The filtering can be taken care of with the Where method, and we’ll use the same technique earlier with the SelectMany method:

public LineItemSummary[] FindExpensiveLineItemsFor(int customerId)
{
    return GetOrdersForCustomer(customerId)
        .SelectMany(order => TransformLineItems(order, order.GetLineItems()))
        .Where(item => item.ItemPrice > 100.0m)
        .ToArray();
}

By adding the Where method, we can filter out only the expensive line items.  The method chaining looks much better than the nested foreach loops coupled with the if statement, and got rid of the temporary list creation.

Lose the foreach

With the new LINQ query operators, any temporary list creation and foreach loop should be considered suspect.  By understanding the operations LINQ gives us, we can not only reduce the amount of code we create, but the end result matches the original intent far better.

Not every foreach or temporary list should be removed, as sometimes long chains and large lambdas tend to muddy rather than clear the picture.  But for a great deal of scenarios, LINQ query operators can vastly improve the readability of transformation (Select), aggregation (Sum), concatenation (SelectMany) and filtering (Where) sections of your code.

Enhancing mappers with LINQ