LINQ: Method Syntax and Lambda Operators

Dan Gray

2020/06/11

Preamble

LINQ is a modern query language which presents a simple, yet powerful way to query data-collections in a variety of sources. It offers a consistent model for working with data from a variety of sources (XML documents, SQL databases, ADO.NET databases, .NET collections etc.)

As an alternative to the “standard” query syntax, LINQ can also be used with a method based syntax and complementary lambda operators - shortening query length and offering a concise expression form.

Motivation

LINQ presents an opportunity to manipulate data from within a .NET based environment - raising the bar for software-based data-manipulation and querying. In addition it reinforces learning-concepts and interaction modalities within the C# language.

Quick Comparison: Query vs. Method

A quick comparison of both syntax types - finding odd numbers in an array.

In both cases n acts as the range (parameter).

To the right side of the () the method declaration acts as the in-place function.

// 1.

      int[] numbers = { 5, 6, 3, 2, 1, 5, 6, 7, 8, 4, 234, 54, 14, 653, 3, 4, 5, 6, 7 };

        var oddQuery = from n in numbers
               where n % 2 == 1
               select n;
               
        Console.WriteLine(string.Join(", ", oddQuery));
                      
        5, 3, 1, 5, 7, 653, 3, 5, 7
               
        
        var oddSyntax = numbers.Where(n => (n % 2 ==1))
        
        Console.WriteLine(string.Join(", ", oddSyntax));
        
        5, 3, 1, 5, 7, 653, 3, 5, 7

Combination of Where and Select

The Where and Select methods are demonstrated below.

// 2.

 List<Warrior> warriors = new List<Warrior>()
            {
                new Warrior() {Height = 180},
                new Warrior() {Height = 190},
                new Warrior() {Height = 160},
                new Warrior() {Height = 140},
                new Warrior() {Height = 185},
                new Warrior() {Height = 170},
            };
 
 .....
 ..........
 ..............

 private internal Warrior
 {
  int Height {get; set;} 
 }
 
      var shortWarriors = Warriors.Where(wh => (wh.Height <= 150))
                                  .Select(wh => wh.Height);
                                  
        Console.WriteLine(string.Join(", ",shortWarriors));
                                  
        140                          

Grouping and MultKey Grouping

Grouping is easily implemented. Using the same list of people as in previous examples.

// 3.

        var simpleGrouping = people.GroupBy(p => p.Gender)
        
        foreach(var item in simpleGrouping)
        {
          Console.WriteLine($"Gender:{item.Key}"); 
          
          foreach(var p in item)
          {
            Console.WriteLine($"    Name: {p.Name}, Age: {p.Age}"); 
          }
        }
        
        
        Male
          Name: Tod, Age: 25
          Name: John, Age: 25
          Name: Kyle, Age: 21
          Name: Neil, Age: 35
          Name: Paul, Age: 36
        Female
          Name: Anna, Age: 23
          Name: Anna, Age: 32
          Name: Maria, Age: 23
          Name: Joan, Age: 28
          Name: Stacey, Age: 23

Grouping with multiple keys is just as digestible.

// 4.

      var multiKeyGrouping = people.GroupBy(p => new {p.Gender}, {p.Age})
      
      foreach(var item in multiKeyGrouping)
      {
        Console.WriteLine($"{item.Key.Gender}, {item.Key.Age}");
        
        foreaach(var p in item)
        {
          ConsoleWriteLine($"{p.FirstName}, {p.Height}");
        }
      }
      

      Male, 25
      Name: Tod, Height: 180
      Name: John, Height: 170
      Female, 23
      Name: Anna, Height: 150
      Name: Maria, Height: 160
      Name: Stacey, Height: 170
      Male, 21
      Name: Kyle, Height: 164
      Female, 32
      Name: Anna, Height: 164
      Female, 28
      Name: Joan, Height: 167
      Male, 35
      Name: Neil, Height: 195
      Male, 36
      Name: Paul, Height: 190

Ordering can be further chained onto the query (in the example below, ordering the results by the membership count per group in ascending order).

// 5.

      var orderedMultiKeyGrouping = people.GroupBy(p => new {p.Gender, p.Age}).OrderBy(p => p.Count())

.....
..........
.............

Grouping by Custom Keys

Custom Keys can be defined by using the ternary operator within the query.

// 5.

        var cusEvenOdd = numbers.OrderBy(n => n)
                                .GroupBy(n => (n % 2 == 0) ? "Even" : "Odd")
                                .OrderBy(n => n.Count());
                                
                                
        foreach(var item in cusEvenOdd)
          {
            Console.WriteLine($"{item.Key}");     
            
            foreach(var n in item)
            {
              Console.WriteLine($"{n}");
            }
          }
          
          Odd
          1
          3
          3
          5
          5
          5
          7
          7
          653
          Even
          2
          4
          4
          6
          6
          6
          8
          14
          54
          234                                            

The conditions and ternary operators can be arranged within a Anonymous Method for readability.

// 6.

      var cusAgeGroups = people.GroupBy(p=>
          {
              var young = p.Age < 25;
              var adult = p.Age > 30;
              
              var age = young ? "Young" : adult ? "Adult" : Senior;
              
              return age;
          });
      
      foreach(var item in cusAgeGroups)
      {
        Console.WriteLine($"    {item.Key}");
        
        foreach(var p in item)
        {
          Console.WriteLine($"{p.FirstName}, {p.Age}");
        }
      }
      
        Senior
      Tod, 25
      John, 25
      Joan, 28
        Young
      Anna, 23
      Kyle, 21
      Maria, 23
      Stacey, 23
        Adult
      Anna, 32
      Neil, 35
      Paul, 36

Selections of Group Membership

Occasionally its very useful to just retrieve membership-counts of the various groups.

// 7.

    var howManyInGroup = people.GroupBy(p => p.Gender)
          .Select (g => new
                   {
                      Gender = g.Key,
                      NumOfPeople = g.Count()
                   });
          
          foreach( var item in howManyInGroup)
          {
            Console.WriteLine($"{item.Gender}");
            Console.WriteLine($"{item.NumOfPeople}");
          }
          
          Male
          5
          Female
          5

Inner Joins

It helps to be familiar with the Query Syntax approach before starting here - but in VSCode/2019 you also get Intelli-Sense to determine the method arguments.

// 8.

      var innerJoinMS = suppliers.Join(buyers, s => s.District, b => b.District,
            (s, b)=> new
            {
              SupplierName = s.Name,
              BuyerName = b.Name,
              District = s.District
            });
      
      foreach(var item in innerJoinMS)
      {
          Console.WriteLine($"District: {item.District}, Supplier Name: {item.Supplier}, Buyer Name: {item.BuyerName}");
      }
      
    District: Point Break, Supplier Name: Andy, Buyer Name: John
    District: West Side, Supplier Name: Tod, Buyer Name: Paul
    District: West Side, Supplier Name: Tod, Buyer Name: Angie
    District: West Side, Supplier Name: Ian, Buyer Name: Paul
    District: West Side, Supplier Name: Ian, Buyer Name: Angie
    District: South, Supplier Name: Anna, Buyer Name: Joan
    District: South, Supplier Name: Anna, Buyer Name: George
    District: Central, Supplier Name: Jerry, Buyer Name: Flo
    District: Central, Supplier Name: Jerry, Buyer Name: Mary
    District: Central, Supplier Name: Paul, Buyer Name: Flo
    District: Central, Supplier Name: Paul, Buyer Name: Mary

Composite Join

Composite Joins just matches across >=2 keys between the two collections.

The keys must be stored in their own Anonymous objects (in in a comparable way to the Query approach).

// 9.

    var compositeJoinMS = suppliers.Join(buyers,
            s=> new {s.District, s.Age},
            b => new{b.Disrict, s.Age},
            (s, b) => new
            {
              SupplierName = s.Name,
              BuyerName = b.Name,
              District = s.District,
              Age = s.Age
            });
    
    foreach(var item in compositeJoinMS)
      {
        Console.WriteLine($"{item.District}, {item.Age}");
        Console.WriteLine($"   Supplier: {item.SupplierName}");
        Console.WriteLine($"   Buyer: {item.BuyerName}");
      }
    
    Point Break, 25
        Supplier: Andy
        Buyer: John
    West Side, 35
        Supplier: Tod
        Buyer: Paul
    South, 30
        Supplier: Anna
        Buyer: Joan
    Central, 28
        Supplier: Jerry
        Buyer: Mary