BlackWaspTM
LINQ
.NET 3.5+

LINQ Joins (2)

The eighth part of the LINQ to Objects tutorial investigates the Join standard query operator and the equivalent use of the join clause. These allow two or more sets of data to be combined according to key information in each collection.

Join Standard Query Operator

In our first example we will join the two collections using the basic version of the Join standard query operator. This is an extension method of all types that support IEnumerable<T>. It joins two lists, known as the outer and inner data, using four parameters. The first parameter supplies the inner list; the outer list is the object that the extension method is being executed against. The other three arguments accept Func delegates, each of which we will supply as a lambda expression. The three delegates are as follows:

  • Outer Key Selector. This delegate accepts a single argument of the type of the outer collection's items. It returns a key value, which may be of any type.
  • Inner Key Selector. This delegate is similar to the outer key selector. It defines how the key values are extracted from the inner collection. Where the outer and inner keys match, the data will be joined in the final results.
  • Result Selector. The third Func delegate defines how the results of the join will be projected. The delegate has two parameters, which will receive the outer and inner items for each matching key pair. The returned value is added to the collection returned by the Join method.

To see the Join operator in action, try executing the following code:

var joined = stock.Join(categories, s => s.Category, c => c.Name,
    (stockItem, category) => new
    {
        Name = stockItem.Name,
        Price = stockItem.Price,
        MinorCategory = category.Name,
        MajorCategory = category.MajorCategory
    });

Here the outer collection is provided by the stock variable and the inner collection by the categories list. The first lambda expression obtains a key from each stock item by returning the Category property. The second argument creates a key for each category by returning the Name property. Wherever these two items are equal, a resultant item will be created using the third lambda. This generates an anonymously typed object containing the name and price from the stock item and the category and major category from the stock category object. The results of the operation are as follows:

{ Name = Apple, Price = 0.3, MinorCategory = Fruit, MajorCategory = Fresh }
{ Name = Banana, Price = 0.35, MinorCategory = Fruit, MajorCategory = Fresh }
{ Name = Orange, Price = 0.29, MinorCategory = Fruit, MajorCategory = Fresh }
{ Name = Cabbage, Price = 0.49, MinorCategory = Vegetable, MajorCategory = Fresh }
{ Name = Carrot, Price = 0.29, MinorCategory = Vegetable, MajorCategory = Fresh }
{ Name = Lettuce, Price = 0.3, MinorCategory = Vegetable, MajorCategory = Fresh }
{ Name = Milk, Price = 1.12, MinorCategory = Dairy, MajorCategory = Chilled }

Using Alternative Comparers

When the outer and inner keys are compared, the two items must match exactly for a result to be generated. If there are minor differences, for example if the keys are strings and one is upper case whilst the other is lower case, the items will not be joined. This can be overcome by using an alternative comparer. Any existing or custom comparer that implements the IEqualityComparer<T> interface may be used.

To demonstrate, change the name of the "Vegetable" category to "vegetable". When you re-run the program you will see that the stock items in the "Vegetable" category are no longer present in the results. However, if we use a case-insensitive comparer, passed as the final argument to the Join method, these will reappear as seen in the sample below. Note that because the category name is being projected from the category object, the MinorCategory value for vegetables is returned in lower case.

var joined = stock.Join(categories, s => s.Category, c => c.Name,
    (stockItem, category) => new
    {
        Name = stockItem.Name,
        Price = stockItem.Price,
        MinorCategory = category.Name,
        MajorCategory = category.MajorCategory
    }, StringComparer.OrdinalIgnoreCase);

/* RESULTS

{ Name = Apple, Price = 0.3, MinorCategory = Fruit, MajorCategory = Fresh }
{ Name = Banana, Price = 0.35, MinorCategory = Fruit, MajorCategory = Fresh }
{ Name = Orange, Price = 0.29, MinorCategory = Fruit, MajorCategory = Fresh }
{ Name = Cabbage, Price = 0.49, MinorCategory = vegetable, MajorCategory = Fresh }
{ Name = Carrot, Price = 0.29, MinorCategory = vegetable, MajorCategory = Fresh }
{ Name = Lettuce, Price = 0.3, MinorCategory = vegetable, MajorCategory = Fresh }
{ Name = Milk, Price = 1.12, MinorCategory = Dairy, MajorCategory = Chilled }

*/

NB: Change the name of the category back to "Vegetable" for the remaining example.

21 August 2010