Refactoring LINQ with Tuples

10/12/10

Refactoring LINQ with Tuples

C#/.NET 4.0 has some nice features, including the Tuple classes. While generic tuples do not override the == operator, they do override Equals. This allows tuples composed of primitive types to be compared.

An interesting use of this is in refactoring LINQ queries, some of which can get quite long. The problem is that multiple-key joins are generally performed with anonymous types (because they override Equals/GetHashCode to compare properties), but anonymous types cannot be returned from methods (unless the return type is dynamic). Queries may also select anonymous types.

Tuples provide a neat solution to this. Here is a contrived example, which assumes legacy data with multi-column keys. (Note that this is LINQ to Objects, not LINQ to SQL.) I probably would not refactor to this degree in production code, as the Item1/Item2 portions are unclear. Note the elegant join in GetOrdersWithCustomers, though.

internal class Program
{
    private static void Main()
    {
        IEnumerable<Customer> customers = GetCustomers();
        IEnumerable<Order> orders = GetOrders();
        IEnumerable<Item> items = GetItems();

        /* Original:

        var query =
            from order in orders
            join customer in customers
                on
                new
                    {
                        order.CustomerType,
                        order.CustomerId
                    }
                equals
                new
                    {
                        CustomerType = customer.Type,
                        CustomerId = customer.Id
                    }
            join item in items
                on order.Id equals item.OrderId
            select
                new
                    {
                        OrderId = order.Id,
                        CustomerName = customer.Name,
                        item.Product
                    };
        */

        // Refactored:

        var ordersWithCustomers =
            GetOrdersWithCustomers(orders, customers);

        var query =
            from x in ordersWithCustomers
            join item in items
                on x.Item1.Id equals item.OrderId
            select
                new
                    {
                        OrderId = x.Item1.Id,
                        CustomerName = x.Item2.Name,
                        item.Product
                    };

        foreach (var record in query)
        {
            Console.WriteLine(record);
        }
    }

    private static IEnumerable<Tuple<Order, Customer>>
        GetOrdersWithCustomers(
        IEnumerable<Order> orders,
        IEnumerable<Customer> customers)
    {
        return
            from order in orders
            join customer in customers
                on order.GetCustomerKey()
                equals customer.GetKey()
            select
                new Tuple<Order, Customer>
                (order, customer);
    }

    private static IEnumerable<Item> GetItems()
    {
        return
            new List<Item>
                {
                    new Item
                        {
                            OrderId = 10,
                            Product = "TenOne"
                        },
                    new Item
                        {
                            OrderId = 10,
                            Product = "TenTwo"
                        },
                    new Item
                        {
                            OrderId = 20,
                            Product = "TwentyOne"
                        }
                };
    }

    private static IEnumerable<Order> GetOrders()
    {
        return
            new List<Order>
                {
                    new Order
                        {
                            Id = 10,
                            CustomerType = "A",
                            CustomerId = 1
                        },
                    new Order
                        {
                            Id = 20,
                            CustomerType = "A",
                            CustomerId = 2
                        }
                };
    }

    private static IEnumerable<Customer> GetCustomers()
    {
        return
            new List<Customer>
                {
                    new Customer
                        {
                            Type = "A",
                            Id = 1,
                            Name = "Bob"
                        },
                    new Customer
                        {
                            Type = "B",
                            Id = 1,
                            Name = "Hobo"
                        },
                    new Customer
                        {
                            Type = "A",
                            Id = 2,
                            Name = "Bill"
                        },
                    new Customer
                        {
                            Type = "B",
                            Id = 2,
                            Name = "Shotgun"
                        }
                };
    }
}

internal class Customer
{
    public string Type { get; set; }
    public int Id { get; set; }
    public string Name { get; set; }

    public Tuple<string, int> GetKey()
    {
        return new Tuple<string, int>(Type, Id);
    }
}

internal class Order
{
    public int Id { get; set; }
    public string CustomerType { get; set; }
    public int CustomerId { get; set; }

    public Tuple<string, int> GetCustomerKey()
    {
        return new Tuple<string, int>
            (CustomerType, CustomerId);
    }
}

internal class Item
{
    public int OrderId { get; set; }
    public string Product { get; set; }
}


Your Host: webmaster@truewill.net
Copyright © 2000-2013 by William Sorensen. All rights reserved.