LINQ is an extremely useful library with many applications. These applications are not all utilized or understood equally. In this article, we are going to take a look at some advanced LINQ capabilities to perform grouping, joining, partitioning, and even converting object types.

To download the source code for this article, you can visit our GitHub repository.

Let’s begin.

Grouping As Part of Advanced LINQ Functionalities

Grouping is when we have a data set and we group elements of that set by defined criteria.

Group

First, let’s begin with defining an Employee class. We can use the class properties to define criteria by which to group the data:

public class Employee
{
    public int EmployeeId { get; set; } = 0;
    public string Name { get; set; } = string.Empty;
    public string Department { get; set; } = string.Empty;
    public double Salary { get; set; } = 0.0;
}

We can use LINQ to perform grouping operations on our data set. The GroupBy() method is a LINQ advanced method that transforms a collection and reorganizes it into groups that are arranged by a common key. GroupBy() returns an IEnumerable<IGrouping<TKey, TElement>>. For example, we can group a collection of Employees by which Department they work in:

var Employees = new List<Employee>()
{
    new() 
    { 
        EmployeeId = 1, 
        Name = "Alvin Johnston", 
        Department = "Sales", 
        Salary = 55000.00 
    },
    new() 
    { 
        EmployeeId = 2, 
        Name = "Jessica Cuevas",
        Department = "Engineering", 
        Salary = 65000.00 
    },
}

Employees.GroupBy(x => x.Department);

You can check our source code to see the full list of employees.

In this case, TKey is a string because that is the type of Department. TElement is of type Employee as that is the object type that we are grouping.

If we print the resulting grouped data we will see:

Department - Engineering
-------------------------
Jessica Cuevas
Justin Vilches
Ashley Montoya

Department - IT
-------------------------
Joey Delgado
Silvio Mora

Department - Administration
-------------------------
Arjen Robben
Mohammad Salah

Department - Customer Service
-------------------------
Nasir Jones

Department - Sales
-------------------------
Alvin Johnston
Grace Silver

Grouping is a useful advanced LINQ process because it allows us to operate over a single data set. Say, for instance, we need to process payroll for all employees. Employees’ salaries are different per department. Instead of writing multiple queries against our database, we can query for all employees and then group them by the department. We can now operate over each group independently without having to query the database any further.

Group With Composite Keys

Next, let’s look at grouping with multiple keys. This is to say we can group data by more than one criterion as we did before with Department:

var employeeDepartmentGroups = employees.GroupBy(x => new { x.Department, x.Salary });

In this example, we are grouping employees by Department and Salary. We do this by creating an anonymous object in lambda provided to the GroupBy() method.

Printing this data out we will see grouped results:

Department, Salary - { Department = Sales, Salary = 55000 }
-----------------------------------------------
Alvin Johnston

Department, Salary - { Department = Engineering, Salary = 65000 }
------------------------------------------------------------------
Jessica Cuevas

Department, Salary - { Department = Sales, Salary = 75000 }
------------------------------------------------------------------
Grace Silver

Department, Salary - { Department = Engineering, Salary = 85000 }
------------------------------------------------------------------
Justin Vilches
Ashley Montoya

Department, Salary - { Department = IT, Salary = 85000 }
------------------------------------------------------------------
Joey Delgado
Silvio Mora

Department, Salary - { Department = Administration, Salary = 105000 }
------------------------------------------------------------------
Arjen Robben

Department, Salary - { Department = Administration, Salary = 115000 }
------------------------------------------------------------------
Mohammad Salah

Department, Salary - { Department = Customer Service, Salary = 45000 }
------------------------------------------------------------------
Nasir Jones

We can see, for example, that there are two people in the group where the employees are in the engineering department and are making an $85,000 salary.

Object properties are not the only criteria by which we can perform grouping. We can group data by our custom-defined keys:

var employeeDepartmentGroups = employees.GroupBy(employee => 
{
    var salaryLevel = employee.Salary < 50000 ? "Entry-Level" :
                      employee.Salary >= 50000 && employee.Salary <= 85000 ? "Mid-Level" :
                      "Senior-Level";

    return salaryLevel;
});

In this example, we create three salary bands on which to group employee salaries: Entry-Level, Mid-Level, and Senior-Level. We do this by creating an anonymous function in which we look at employee salaries and then return a string of which salary level the employee belongs to.

Now, let’s inspect the result once we print out these groups:

Salary - Entry-Level
-----------------------------------------------
Nasir Jones

Salary - Mid-Level
-----------------------------------------------
Alvin Johnston
Jessica Cuevas
Grace Silver
Justin Vilches
Joey Delgado
Ashley Montoya
Silvio Mora

Salary - Senior-Level
-----------------------------------------------
Arjen Robben
Mohammad Salah

Lastly, we can group data by criteria and then return a custom final object using LINQ. This final object can represent a desired transformation of the group data. To put this into action, let’s look at an example where we sum the salary of each employee per department:

var employeeDepartmentAggregateReport = employees.GroupBy(x => x.Department).Select(group => new
{
    DepartmentName = group.First().Department,
    TotalDepartmentSalaryCosts = group.Sum(empl => empl.Salary)
});

In this LINQ expression, we transform our list of all employee data into a custom object meant to serve as a salary aggregate report for each department. GroupBy() will group our employee list by department. Select() will return a new anonymous object with the department name and salary total.

After we print the department salary aggregate data, we can find our results:

Department Salary Report

Sales - Total Salary - 130000
-----------------------------------------------
Engineering - Total Salary - 235000
-----------------------------------------------
IT - Total Salary - 170000
-----------------------------------------------
Administration - Total Salary - 220000
-----------------------------------------------
Customer Service - Total Salary - 45000

There are many reasons to use LINQ to produce robust code. GroupBy() is a powerful tool in our belt to transform data meaningfully.

Use Advanced LINQ Methods to Convert Data Types 

Often we want to transform an object from one type to another. We can use a few LINQ methods to do this easily on a single object or collection of objects as well.

For the examples, we will use the Director and Administrator classes. These classes inherit from the Employee class:

public class Director : Employee
{
    public string Permissions { get; set; } = string.Empty;
    public bool AbleToHire { get; set; } = false;
}

public class Administrator : Employee
{
    public bool AbleToFire { get; set; } = false;
}

Also, for this section, we are going to use a list of Employees for our examples:

var MixedEmployees = new List<Employee>()
{
    new Director() 
    { 
        AbleToHire = true, 
        Permissions = "READ_WRITE_CREATE_DELETE", 
        EmployeeId = 1,
        Name = "Rodrigo Suarez",
        Department = "Leadership",
        Salary = 175000.00
    },
    new Employee() 
    { 
        EmployeeId = 2,
        Name = "Jessica Cuevas",
        Department = "Engineering",
        Salary = 65000.00
    }, ...
};

You can check the source code to see the full list of MixedEmployees

OfType<T>

We can use OfType<T>() to pull out all instances of T from MixedEmployees and return them in an IEnumerable<T>.  This is an incredibly useful advanced LINQ function as we can take advantage of objects that inherit from a base class or implement a common interface:

var directors = employeeList.OfType<Director>();

Executing this code, directors will be an IEnumerable<Director> with one element. In this case, it is the director Rodrigo Suarez.

ConvertAll

First, let’s say that this method is not exactly a LINQ method as it comes from the System.Collections.Generics namespace, but it is used a lot in LINQ operations. This method will cast all elements of a List<> to another class type:

var admins = employeeList.OfType<Administrator>();
admins.ToList().ConvertAll(d => JsonSerializer.Serialize(d));

Here, we use OfType() to get an IEnumerable<Administrator> as we did in the previous example. Next, we transform the enumerable into a List. We do this because ConvertAll() can only be used on a List. Finally, we pass a lambda to transform elements of the collection into another type. In this case, we transform Administrator objects into strings using JsonSerializer.Serialize().

As you can see, we have to use the ToList method as ConvertAll works only with a List. We can also use the Select method, to project the result, but it returns IEnumerable as a result, and if we want a List as a result, we have to use ToList.

AsQueryable

Lastly, let’s discuss IQueryable. IQueryable is an interface that is popularly used to build queries over collections. Moreover, ORMs, such as Entity Framework, use IQueryables to interface with databases. Given the prevalence of databases and cloud database services, IQueryable is an important topic to understand.¬† With IQueryable, we can build complex queries with multiple selections or filter statements before a query is executed. We can convert collections to IQueryable using the AsQueryable() method:

var queryableEmployees = employeeList.AsQueryable();

Now, we can use queryableEmployees to build a query over our collection of employees. IQueryable objects can be used in conjunction with an ORM library to interface with our project’s data storage solution. AsQueryable() can be called on arrays, List, Dictionary, Lookup, Stack, Hashtable, and most other collections that implement IEnumerable.

Joining 

Joining is similar to grouping as they allow us to group data by defined criteria. The difference is that joining allows us to do this with two different collections, as opposed to a single collection. In this section we will cover three different variations of joining: inner join,  group join, and lastly left outer join.

Inner Join

Inner join is the basic variation of a joining operation. Let’s define a second collection to which we can join the employees collection we previously defined:

var directors = new List<Director>()
{
    new Director() 
    {
        AbleToHire = true,
        Permissions = "READ_WRITE_CREATE_DELETE",
        EmployeeId = 100,
        Name = "Nikola Jokic",
        Department = "Leadership",
        Salary = 175000.00,
        DepartmentResponsibleFor = "Engineering"
    },
    new Director() 
    {
        AbleToHire = true,
        Permissions = "READ_WRITE_CREATE_DELETE",
        EmployeeId = 101,
        Name = "Petr Cech",
        Department = "Leadership",
        Salary = 175000.00,
        DepartmentResponsibleFor = "IT"
    }, ...
};

Now, let’s join the directors collection to the employees collection:

var join = employees.Join(directors, 
    em => em.Department,
    dir => dir.DepartmentResponsibleFor,
    (em, dir) => new { EmployeeName = em.Name, DirectorName = dir.Name, Department = em.Department });

We call Join() on the employees collection. The first parameter is the second collection we want to join with. In this case, we are joining with directors. 

The next parameter is a lambda expression where we select a property from the Employee class to use a key selector. Similarly, the next parameter is the same but for the Director class.

The last parameter is a lambda that creates a new anonymous object containing the information from an employee and director object that has matching key selectors Department and DepartmentResponsibleFor.

Let’s take a look at the resulting data:

Department: Engineering
------------------------
Employee: Jessica Cuevas
Director: Nikola Jokic

Department: Engineering
------------------------
Employee: Justin Vilches
Director: Nikola Jokic

Department: Engineering
------------------------
Employee: Montoya
Director: Nikola Jokic

Department: IT
------------------------
Employee: Joey Delgado
Director: Petr Cech

Department: IT
------------------------
Employee: Silvio Mora
Director: Petr Cech

The result is a matching of a director and an employee from the department the director is responsible for. 

Group Join

Group Join is very similar to an inner join but the difference is the resulting data is grouped by elements of the collection the method was called on. In the case of our previous examples, the resulting joined data is a single Director element joined with all the employees they are responsible for.

First, let’s examine code that performs a group join on our collections:

var groupJoin = directors.GroupJoin(employees,
    dir => dir.DepartmentResponsibleFor,
    em => em.Department,
    (dir, emGroup) => new 
    {
        dir.Name,
        EmployeeGroup = emGroup 
    });

In this example, we are doing a group join of the directors collection with the employees collection. This results in a collection of objects that are comprised of the director and all the employees in the department they are responsible for. The first three parameters are the same as Join(). The last parameter is a lambda with the parameters of a director and a group of employees.

We organize this data in a new anonymous object:

Department: Engineering -- Director: Nikola Jokic 
--------------------------------------------- 
Employee: Jessica Cuevas 
Employee: Justin Vilches 
Employee: Ashley Montoya 

Department: IT -- Director: Petr Cech 
--------------------------------------------- 
Employee: Joey Delgado 
Employee: Silvio Mora 

Department: R&D -- Director: Carl Friedrich Gauss
--------------------------------------------- 

The output here shows each director grouped with all the employees in their respective departments.

Left Outer Join

A left outer join is a group join where elements in the inner collection are represented in the final result, even when there are no matching elements in the outer collection. If we look at the directors collection we can see we have a director that is responsible for the R&D department. We have no employees in the R&D department. Nevertheless, with a left outer join, we will still see the R&D director in our final result.

With LINQ, we can perform a left outer join by supplying a default outer object when there are no matching objects in the outer collection. To supply this default object we will use DefaultIfEmpty():

var groupJoin = directors.GroupJoin(employees,
    dir => dir.DepartmentResponsibleFor, 
    em => em.Department, 
    (dir, emGroup) => new 
    {
        dir.Name,
        EmployeeGroup = emGroup.DefaultIfEmpty(new() { Name = "No Name" })
    });

We return a new Employee object with the Name “No Name” when emGroup is empty. emGroup will only be empty if there are no employees that match the department for which the director is responsible.

Let’s examine the output of this method:

Department: Sales -- Director: Rodrigo Suarez
---------------------------------------------
Employee: Alvin Johnston
Employee: Grace Silver

Department: Engineering -- Director: Nikola Jokic
---------------------------------------------
Employee: Jessica Cuevas
Employee: Justin Vilches
Employee: Ashley Montoya

Department: IT -- Director:Petr Cech
---------------------------------------------
Employee: Joey Delgado
Employee: Silvio Mora

Department: R&D -- Director: Carl Friedrich Gauss
---------------------------------------------
Employee: No Name

As we can see, without any employees in the R&D department there is a grouping for the director of the department but with the “No Name” default employee.

Generating Sequences

Where it is useful LINQ allows us easily generate sequences of objects robustly and intuitively. We can use Range() and Repeat() to generate sequences in an IEnumerable.

Repeat(T element, int count) can be used to create an IEnumerable<T> of element repeated a count number of times.

Range(int start, int count) will generate an IEnumerable<int> that starts at start and will continue the sequence for a count number of times.

Repeat() is a flexible method as it is a generic method that will accept any type we want to use and generate a sequence from it.

Let’s take a look at an example:

Enumerable.Repeat(new Employee(), 100);

Here we generate a collection of 100 Employee. Repeat() can be used creatively to avoid writing a loop to do the same task or to easily generate mock data in unit tests.

Similarly, Range() can also be used creatively to generate sequences and even be used in a way where the sequence matters not. Range() used with filtering can produce even more complex sequences:

Enumerable.Range(5, 5); // 5, 6, 7, 8, 9, 10
Enumerable.Range(1, 10).Select(n => n * n); // 1, 4, 9, 16, 25 ... 100
Enumerable.Range(1, 100).Where(n => n % 2 == 0); // 2, 4, 6, 8 ... 100
Enumerable.Range(1, 100).Select(_ => PerformSomeAction());

In the first example, we have a simple use of Range() where we generate the sequence of integers starting at 5 up to 10.

Next, we generate a list of all squares of the sequence 1 to 10. This example shows that we can apply a logical operation on a sequence of elements using Select().

In the third example, we filter the sequence by only elements that are even integers.

Lastly, we use Select() and a discard _ to call PerformSomeAction() 100 times. Using _ indicates that we do not care about the value of the element in the sequence just that we want to call a method for as many elements as there are in the collection.

In essence, this is a great, short way to repeat an action several times.

Partitioning Operations

There are a few methods that allow us to partition collections sequentially in different ways. We will use the list of numbers for our examples:

List<int> ints = new List<int>() { 2, 7, 2, 4, 5, 8, 9, 6, 1, 8, 9, 7 };

First, let’s discuss Skip() and SkipWhile(). Both these methods indicate we want to skip through the collection’s elements sequentially. Skip(int x) will skip through x number of elements and return the rest of the IEnumerable we are operating over. SkipWhile() will accept a lambda as a parameter indicating we will skip elements until the condition is false.

Let’s consider the example of using both methods:

ints.Skip(7);   // 6, 1, 8, 9, 7
ints.SkipWhile(i => i < 9) // 9, 6, 1, 8, 9, 7

The first example skips the first 7 elements in ints and then returns the rest of the List. The next example skips all elements until an element doesn’t satisfy the condition i < 9. This results in a collection of all elements starting at element 9 (index 6) and ahead.

On the other hand, we have Take() and TakeWhile() these methods will get the collection’s elements sequentially. Take(int count) will return a list of the first count number of elements. TakeWhile() will accept a lambda that evaluates all elements until the condition is false. Here are examples of these methods in action:

ints.Take(5); // 2, 7, 2, 4, 5 
ints.TakeWhile(i => i < 9) // 2, 7, 2, 4, 5, 8

First, Take() gets the first 5 elements in ints and then returns them as a List. Second,¬† TakeWhile() gets all elements until an element doesn’t satisfy the condition i < 9. This result is a collection of elements starting at element 2 (index 0) until element 8 (index 5).

Conclusion

Overall, these are a few topics on some advanced uses for LINQ. Learning the fundamentals of these methods and applying them creatively can change the quality and efficiency of our code. They can make interfacing with a database much easier or even replace long sections of code with a few lines of LINQ code.