Tuesday 14 July 2009

Collapsing foreach Loops using LINQ

Much of the business logic we have traditionally written has been buried in a heap of nested foreach loops and if statements. All this additional cermoney obfiscates the meaning of the code making finding what a method does more difficult.

One the key strength of LINQ, as I see it, is in its ability to put your application's business logic center stage. By seperating your query from your command within each method, the clarity of those methods is greatly increased.

Time for a example I think. Take the following example of traditional business logic:

public void FulfillOrders()
{
foreach(Customer customers in Customers)
{
if(customer.CanPlaceOrders())
{
foreach(Order order in customer.Orders)
{
if(!order.IsShipped()
{
foreach(OrderLine line in order.Lines)
{
if(line.IsInStock())
{
customer.Ship(order, line);
}
else
{
customer.BackOrder(order, line);
}
}
}
}
}
}
}

OK, I admit, not a great bit of DDD - but I'll live with that; its not the point of this blog. However this code, is fairly difficult to read and its going to quickly become unmaintainable as more business logic is heaped upon it. LINQ allows us to collapse down those foreach loops and if statements into a query thereby achieving command query seperation within the method.

public void FulfillOrders()
{
var allOrders = from customer in Customers
from order in cutomer.Orders
from line in order.Lines
where customer.CanPlaceOrders()
&& !order.IsShipped
select new {Customer = customer,
Order = order,
Line = line};

var shippable = from item in allOrders
where item.IsInStock()
select item;

var outOfStock = from item in allOrders
where !item.IsInStock()
select item;

foreach(var item in shippable)
{
item.Customer.Ship(item.Order, item.Line);
}

foreach(var item in outOfStock)
{
item.Customer.BackOrder(item.Order, item.Line);
}
}

Note, how in the allOrders query I am stacking up the "from" statements with no joins. This tells LINQ to do a cross join. But wouldn't this mean that every customer is joined to every order and every order line regardless of whether the customer owned the orders. Well, no; in the object model the Orders belong to the Customer and the OrderLines belong to the Orders. In this situation LINQ is smart enough to figure out which constraints should apply.

In this new "LINQed-up" version we have clearly defined the queries (at the top) from the commands (at the bottom). This leads to clarity in code readability which in-turn leads to an increase in maintainability. Getting to grips with the syntax of LINQ can take some time, but the increase in code maintainability is well worth the effort.