LINQ: Basic Queries and Grouping

Dan Gray

2020/06/06

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.)

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.

Basics

One can start with simple collections to demonstrate the basic syntax.

// 1. 
            int[] numbers = { 5, 6, 3, 2, 1, 5, 6, 7, 8, 4, 234, 54, 14, 653, 3, 4, 5, 6, 7 };
            
            var getTheNumbers = from number in numbers
                                where number > 5
                                where number < 10
                                orderby descending
                                select number;
                                
                                Console.WriteLine(string.Join(",", getTheNumbers)
                                                  
            8,7,7,6,6,6                               

Each query must use the from....ìn and select clauses. The range variable is typically denoted as a singular letter of the Enumerable being accessed.Methods can be used on the variables, and conditionals can be combined.

// 2.
          string[] catNames = { "Lucky", "Bella", "Luna", "Oreo", "Simba", "Toby", "Loki", "Oscar" };
          
          var catsWithA = from c in catNames
                          where c.Contains("a" && (c.Length <5))
                          select c;
                          
          Luna

Dealing with Objects

A slightly more typical class of personal data can be used to demonstrate some functionality. First to create the class.

internal class Person
    {
        private string firstName;
        private string lastName;
        private int id;
        private int height;
        private int age;
        private int weight;

        private Gender gender;

        public string FirstName
        {
            get
            {
                return this.firstName;
            }
            set
            {
                this.firstName = value;
            }
        }

        public string LastName
        {
            get
            {
                return this.lastName;
            }
            set
            {
                this.lastName = value;
            }
        }

        public int ID
        {
            get
            {
                return this.id;
            }
            set
            {
                this.id = value;
            }
        }

        public int Height
        {
            get
            {
                return this.height;
            }
            set
            {
                this.height = value;
            }
        }

        public int Age
        {
            get
            {
                return this.age;
            }
            set
            {
                this.age = value;
            }
        }

        public int Weight
        {
            get
            {
                return this.weight;
            }
            set
            {
                this.weight = value;
            }
        }

        public Gender Gender
        {
            get
            {
                return this.gender;
            }
            set
            {
                this.gender = value;
            }
        }

        public Person(string firstName, string lastName, int id, int height, int age, int weight, Gender gender)
        {
            this.firstName = firstName;
            this.lastName = lastName;
            this.id = id;
            this.Height = height;
            this.Age = age;
            this.Weight = weight;
            this.Gender = gender;
        }
    }


  internal enum Gender
    {
        Male,
        Female
    }

The type (People) of results (var peopleOrdered) returned by this query is of class TElement.

// 3.
        var peopleOrdered = from p in peopleOrderedwhere (p.FirstName.Length == 4)
                            orderby p.Height descending, p.Weight
                            select p;
                            
        foreach (var p in peopleOrdered)
        {
            Console.WrieLine($"Name: {p.FirstName}, Height: {p.Height}, Weight: {p.Weight}")
                              
        }
        
        Name: Noal, Height: 195, Weight: 76
        Name: Paul, Height: 190, Weight: 85
        Name: John, Height: 170, Weight: 88
        Name: Joan, Height: 167, Weight: 58
        Name: Kyle, Height: 164, Weight: 77
        Name: Anna, Height: 164, Weight: 77
        Name: Anna, Height: 150, Weight: 48

One can also return Anonymous Types using the keyword new.

// 4.
        var youngPeople = from p in people
                        where p.Age < 25
                        select new
                        {
                            Name = p.FirstName,
                            EstimatedAge = p.Age
                        };
                        
        foreach(var p in youngPeople)
        {
          Console.WriteLine($"My name is {p.Name} and I am {p.estimatedAge} old.")  
        }                  
                        
        My name is Anna and I am 23 old.
        My name is Kyle and I am 21 old.
        My name is Maria and I am 23 old.
        My name is Stacey and I am 23 old.                  

Nested Collections and Grouping

The let keyword can be elegantly used to query nested data collections. In addition the group...by clause enables a grouping within dat by common “keys”.

// 5.
        List<List><int> lists = new List<List><int>>
          {
            new List<int> {1,2,3}
            new List<int> {4,5,6}
            new List<int> {7,8,9}
          };
        
        var allNums = from l in lists
                      let digits = l
                      from d in digits
                      let squared = d * d
                      where squared < 50
                      select squared;
                      
        foreach( var n in allNums)
        {
          Console.WriteLine(n);  
        }
                      
        1
        4
        9
        16
        25
        36
        49             

Grouping with a single key can also be combined with conditionals.

// 6. 
        var alphaGrp = from p in people
                       orderby p.FirstName
                       group p by p.FirstName[0]
                       
        foreach (var grp in alpabecticalGroup)
        {
          Console.WriteLine($"{grp.Key}");

          foreach (var item in grp)
            {
              Console.WriteLine($"FirstName: {item.FirstName}, LastName: {item.LastName}, Age: {item.Age} ");
            }
        }   
        
        A
        FirstName: Anna, LastName: Memm, Age: 23
        FirstName: Anna, LastName: Kendrick, Age: 32
        J
        FirstName: Joan, LastName: Small, Age: 28
        FirstName: John, LastName: Denver, Age: 25
        K
        FirstName: Kyle, LastName: Ellis, Age: 21
        M
        FirstName: Maria, LastName: Jones, Age: 23
        N
        FirstName: Noal, LastName: Fields, Age: 35
        P
        FirstName: Paul, LastName: Newton, Age: 36
        S
        FirstName: Stacey, LastName: Blue, Age: 23
        T
        FirstName: Tod, LastName: Vachev, Age: 25

Grouping by more than one key requires the group...by and new clauses.

// 7.
        var multiGroups = from p in people
                          group b by new {p.Gender, p.Age}
                          
         foreach (var grp in multiGroups)
          {
            Console.WriteLine($"{grp.Key}");

            foreach (var item in grp)
              {
                Console.WriteLine($"FirstName: {item.FirstName}, Height: {item.Height}");
              }
         }
         
          { Gender = Male, Age = 25 }
          FirstName: Tod, Height: 180
          FirstName: John, Height: 170
          { Gender = Female, Age = 23 }
          FirstName: Anna, Height: 150
          FirstName: Maria, Height: 160
          { Gender = Male, Age = 21 }
          FirstName: Kyle, Height: 164
          { Gender = Male, Age = 32 }
          FirstName: Anna, Height: 164
          { Gender = Female, Age = 28 }
          FirstName: Joan, Height: 167
          { Gender = Male, Age = 35 }
          FirstName: Noal, Height: 195
          { Gender = Male, Age = 23 }
          FirstName: Stacey, Height: 170
          { Gender = Male, Age = 36 }
          FirstName: Paul, Height: 190

As all queries need to end in either a group...by or select clause, in order to sort or order the result (say based on membership count per grouping) one would need to take the query result above (multiGroups) and create another query with these addition conditionals and methods.

Another approach is to use the into clause to hold the intermediate query results in a similar way to let and then continue the query with the required conditional requirements.

// 8.
        var peopleByAge = from p in people
                          group p by p.Age into ageGroup
                          orderby ageGroup.Key
                          select ageGroup;
        
        foreach (var grp in peopleByAge)
          {
            Console.WriteLine($"{grp.Key}");

            foreach (var item in grp)
            {
              Console.WriteLine($"FirstName: {item.FirstName}, Weight: {item.Weight}");
            }
        } 
                          
        21
        FirstName: Kyle, Weight: 77
        23
        FirstName: Anna, Weight: 48
        FirstName: Maria, Weight: 55
        FirstName: Stacey, Weight: 65
        25
        FirstName: Tod, Weight: 70
        FirstName: John, Weight: 88
        28
        FirstName: Joan, Weight: 58
        32
        FirstName: Anna, Weight: 77
        35
        FirstName: Noal, Weight: 76
        36
        FirstName: Paul, Weight: 85                  

Naturally the new and into clauses can be chained for multiple keys and conditionals.

// 9.
        var severalGroups = from p in people
                            group p by new { p.Gender, p.Age} into ageGenderGroup
                            orderby ageGenderGroup.Count() descending
                            select ageGenderGroup;
                            
         foreach (var grp in severalGroups)
          {
            Console.WriteLine($"Gender: {grp.Key.Gender}, Age: {grp.Key.Age}");

            foreach (var item in grp)
            {
              Console.WriteLine($"Height: {item.Height}, FirstName: {item.FirstName}");
            }

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

The grouping key can also be defined from a referenced intermediate variable (via let). An example to show the chaining of all these concepts.

Custom keys can be used by using the ternary operator to defien preferred names.

// 10.
        var evenOddSorting = from n in numbers
                             orderby n
                             let evenOrOdd = (n % 2 == 0) ? "Even" : "Odd" 
                             group n by evenOrOdd into nums
                             orderby nums.Count()
                             select nums;
                             
        foreach (var grp in evenOddSorting)
        {
          Console.WriteLine($"{grp.Key}");

          foreach (var item in grp)
          {
            Console.WriteLine($"    {item}");
          }
        }       
                             
        Even
            1
            3
            3 
            5
            5
            5
            7
            7
            653
        Odd
            2
            4
            4
            6
            6
            6
            8
            14
            54
            234