C# .NET

Advanced LINQ Operations in C#: Unlock the Full LINQ Potential

Bhushan Kadam
Cover Image for Advanced LINQ Operations in C#: Unlock the Full LINQ Potential

Introduction

LINQ, or Language Integrated Query, is a set of features in C# that allows developers to perform querying and manipulating data in a more convenient and expressive way. LINQ provides a consistent and unified syntax for querying and manipulating various types of data, such as arrays, lists, XML documents, and databases.

This is part 2 of the LINQ in C# article. Don't forget to check out Part 1 of this blog as well where we explained what LINQ is and covered the primary LINQ operations.

LINQ Operations

We have already covered the operations of Filtering, Sorting, Grouping, and Projection in Part 1 of this blog. In this blog, we will cover the remaining LINQ Operations.

Set Operations

Set operations are one of the main features of LINQ. Set operations are operations that combine two or more sets of data based on certain criteria.

The most common set operations in LINQ are -

  1. Union - Returns the elements that are present in either set A or set B or both

  2. Intersect - Returns the elements that are present in both set A and set B

  3. Except - Returns the elements that are present in set A but not in set B

  4. Distinct - Returns the distinct elements from a set.

Let's understand each operation with an example

Union

Using the Union, we can return the data which is present in either Set A or Set B, or both.

Here is an example of using the Union operator to combine two lists of integers.

List<int> list1 = new List<int> { 1, 2, 3, 4, 5 };
List<int> list2 = new List<int> { 4, 5, 6, 7, 8 };

// Use the Union operator to combine the two lists
var union = list1.Union(list2);

foreach (int num in union)
{
    Console.WriteLine(num);
}

In this example, we have used the Union operator to combine the two lists, resulting in a new list that contains the numbers present in either of the two lists.

Intersect

Using Intersect, we can return the common elements in two sets. Which means the elements that are present in both set A and set B.

Here is an example of using the Intersect operator to get the elements that are present in both lists.

List<int> list1 = new List<int> { 1, 2, 3, 4, 5 };
List<int> list2 = new List<int> { 4, 5, 6, 7, 8 };

// Use the Intersect operator to get the elements that are present in both the lists
var intersection = list1.Intersect(list2);

foreach (int num in intersection)
{
    Console.WriteLine(num);
}

In this example, we have used the Intersect operator to find the numbers that are present in both of the two lists.

Except

Using Except, we can return the elements that are present in set A but not in set B.

Here is an example of using the Except operator to get the elements that are present in the first list but not present in the second list.

List<int> list1 = new List<int> { 1, 2, 3, 4, 5 };
List<int> list2 = new List<int> { 4, 5, 6, 7, 8 };

// Use the Except operator to get the elements that are present in the first list but not present in the second list
var except = list1.Except(list2);

foreach (int num in except)
{
    Console.WriteLine(num);
}

In this example, we have used the Except operator to get the numbers that are present in the first list but not present in the second list.

Distinct

Using Distinct, we can return the distinct elements from a set.

Here is an example of using the "Distinct" operator to get the distinct elements from a list.

List<int> list1 = new List<int> { 1, 2, 3, 4, 5, 5, 4, 3, 2, 1 };

// Use the Distinct operator to get the distinct elements from the list
var distinct = list1.Distinct();

foreach (int num in distinct)
{
    Console.WriteLine(num);
}

Aggregation

Aggregation is one of the main features of LINQ. Aggregation is the process of applying a mathematical operation to a collection of values.

The most common aggregation operations in LINQ are -

  1. Count - Returns the number of elements in a collection.

  2. Sum - Returns the sum of all elements in a collection.

  3. Min - Returns the minimum element in a collection.

  4. Max - Returns the maximum element in a collection.

  5. Average - Returns the average of all elements in a collection.

Let's look at each aggregation operation with an example.

Count

Let's look at the example of the Count Operator. In the below code, we will use Count to count the number of elements in a list of integers.

List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

// Use the Count operator to count the number of elements in the list
int count = numbers.Count();
Console.WriteLine("Number of elements in the list: " + count);

Sum

Let's look at the example of the Sum Operator. In the below code, we will use Sum to get the sum of all elements in a list of integers.

List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

// Use the Sum operator to get the sum of all elements in the list
int sum = numbers.Sum();
Console.WriteLine("Sum of all elements in the list: " + sum);

Min

Let's look at the example of the Min Operator. In the below code, we will use Min to get the minimum element in a list of integers.

List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

// Use the Min operator to get the minimum element in the list
int min = numbers.Min();
Console.WriteLine("Minimum element in the list: " + min);

Max

Let's look at the example of the Max Operator. In the below code, we will use Max to get the maximum element in a list of integers.

List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

// Use the Max operator to get the maximum element in the list
int max = numbers.Max();
Console.WriteLine("Maximum element in the list: " + max);

Average

Let's look at the example of the Average Operator. In the below code, we will use Average to get the average of all elements in a list of integers.

List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

// Use the Average operator to get the average of all elements in the list
double average = numbers.Average();
Console.WriteLine($"Average of all elements in the list is: {average}");


Joining

One of the main features of LINQ is the ability to perform join operations. It is the process of combining two or more sets of data based on a common key.

The most common join operations in LINQ are -

  1. Inner Join - Returns only the matching records from both sets.

  2. Left Join - Returns all records from the left set and matches records from the right set.

  3. Right Join - Returns all records from the right set and matches records from the left set.

  4. Full Outer Join - Returns all records from both sets, with NULLs in place of non-matching records.

Let's look at each Join operation with an example.

Inner Join

Let's look at the example of the Inner join Operator. In the below code, we will use Inner Join to perform an inner join on two lists of objects.

List<Person> people = new List<Person> {
    new Person { ID = 1, Name = "John", Age = 25 },
    new Person { ID = 2, Name = "Jane", Age = 35 },
    new Person { ID = 3, Name = "Bob", Age = 30 }
};
List<Address> addresses = new List<Address> {
    new Address { PersonID = 1, Street = "Main St", City = "New York", State = "NY" },
    new Address { PersonID = 2, Street = "Park Ave", City = "Los Angeles", State = "CA" },
    new Address { PersonID = 3, Street = "Lakeview St", City = "Chicago", State = "IL" }
};

// Use the Join operator to perform an inner join on the two lists
var joinedData = people.Join(addresses,
    person => person.ID,
    address => address.PersonID,
    (person, address) =>

Left Join

Let's look at the example of the Left Join Operation.

List<Person> people = new List<Person> {
    new Person { ID = 1, Name = "John", Age = 25 },
    new Person { ID = 2, Name = "Jane", Age = 35 },
    new Person { ID = 3, Name = "Bob", Age = 30 }
};
List<Address> addresses = new List<Address> {
    new Address { PersonID = 1, Street = "Main St", City = "New York", State = "NY" },
    new Address { PersonID = 2, Street = "Park Ave", City = "Los Angeles", State = "CA" }
};

// Use the GroupJoin operator to perform a left join on the two lists
var leftJoin = people.GroupJoin(addresses,
    person => person.ID,
    address => address.PersonID,
    (person, addresses) => new { Person = person, Addresses = addresses })
    .SelectMany(x => x.Addresses.DefaultIfEmpty(), (x, y) => new { x.Person, Addresses = y });

foreach (var item in leftJoin)
{
    Console.WriteLine("Name: " + item.Person.Name);
    if (item.Addresses != null)
    {
        Console.WriteLine("\t" + item.Addresses.Street + ", " + item.Addresses.City + ", " + item.Addresses.State);
    }
    else
    {
        Console.WriteLine("\tNo address found");
    }
}

In this example, the "GroupJoin" operator is used to perform a left join on the two lists, where the "people" list is the left set and the "addresses" list is the right set. The first lambda expression passed to the "GroupJoin" operator (person => person.ID) defines the key used to match records from the left set (people) with records from the right set (addresses). The second lambda expression passed to the "GroupJoin" operator (address => address.PersonID) defines the key used to match records from the right set (addresses) with records from the left set (people).

The "GroupJoin" operator returns an IEnumerable of an anonymous type, where each element contains a person and a collection of addresses

Right Join

A right join is used to combine two collections and return all the elements from the right collection (the second collection in the Join statement) along with any matching elements from the left collection (the first collection in the Join statement). If there is no match for a particular element in the right collection, the result will contain a default value in place of the missing element from the left collection.

Let's look at the example of the Right Join Operation.

List<Customer> customers = new List<Customer>()
{
    new Customer() { Id = 1, Name = "John Smith" },
    new Customer() { Id = 2, Name = "Jane Doe" },
    new Customer() { Id = 3, Name = "Bob Johnson" }
};

List<Order> orders = new List<Order>()
{
    new Order() { Id = 1, CustomerId = 1, Amount = 100 },
    new Order() { Id = 2, CustomerId = 2, Amount = 200 },
    new Order() { Id = 3, CustomerId = 4, Amount = 300 }
};

var rightJoin = from c in customers
                join o in orders on c.Id equals o.CustomerId into co
                from o in co.DefaultIfEmpty()
                select new { Customer = c, Order = o };

foreach (var item in rightJoin)
    Console.WriteLine(item);

In this example, we have two lists of objects, "customers" and "orders". We want to join these two lists based on the "Id" property of the "customers" list and the "CustomerId" property of the "orders" list. The Join statement uses the "on" keyword to specify the properties to join on and the "into" keyword to create a new variable "co" that holds the results of the join. The "DefaultIfEmpty()" method is used to return a default value if there is no match.

The result of the right join will be enumerable of the anonymous type which contains the "Customer" and "Order" properties.

After running this code, the output will be -

{ Customer = { Id = 1, Name = "John Smith" }, Order = { Id = 1, CustomerId = 1, Amount = 100 } }
{ Customer = { Id = 2, Name = "Jane Doe" }, Order = { Id = 2, CustomerId = 2, Amount = 200 } }
{ Customer = { Id = 3, Name = "Bob Johnson" }, Order =  }

Full Outer Join

A full outer join is used to combine two collections and return all the elements from both collections along with any matching elements. If there is no match for a particular element in either collection, the result will contain a default value in place of the missing element.

List<Customer> customers = new List<Customer>()
{
    new Customer() { Id = 1, Name = "John Smith" },
    new Customer() { Id = 2, Name = "Jane Doe" },
    new Customer() { Id = 3, Name = "Bob Johnson" }
};

List<Order> orders = new List<Order>()
{
    new Order() { Id = 1, CustomerId = 1, Amount = 100 },
    new Order() { Id = 2, CustomerId = 2, Amount = 200 },
    new Order() { Id = 3, CustomerId = 4, Amount = 300 }
};

var fullOuterJoin = from c in customers
                join o in orders on c.Id equals o.CustomerId into co
                from o in co.DefaultIfEmpty()
                select new { Customer = c, Order = o }
                union
                from o in orders
                join c in customers on o.CustomerId equals c.Id into oc
                from c in oc.DefaultIfEmpty()
                select new { Customer = c, Order = o };

foreach (var item in fullOuterJoin)
    Console.WriteLine(item);

In this example, we have two lists of objects, "customers" and "orders". We want to join these two lists based on the "Id" property of the "customers" list and the "CustomerId" property of the "orders" list. The Join statement uses the "on" keyword to specify the properties to join on, the "into" keyword to create a new variable, and the "DefaultIfEmpty()" method to return a default value if there is no match.

Then, we are using the union keyword to join the left join results with the right join results.

The result of the full outer join will be enumerable of the anonymous type which contains the "Customer" and "Order" properties.

In this example, the output will be -

{ Customer = { Id = 1, Name = "John Smith" }, Order = { Id = 1, CustomerId = 1, Amount = 100 } }
{ Customer = { Id = 2, Name = "Jane Doe" }, Order = { Id = 2, CustomerId = 2, Amount = 200 } }
{ Customer = { Id = 3, Name = "Bob Johnson" }, Order =  }
{ Customer = , Order = { Id = 3, CustomerId = 4, Amount = 300 } }

Conclusion

In this part 2 of the LINQ blog, we studied some more important LINQ operations like Set Operations, Join Operations, and aggregations. If you want some basic understanding of LINQ, check out part 1 of this blog where we have explained LINQ and also explained the primary LINQ operations of Filtering, Projection, Sorting, and Grouping.

I have tried to cover most of the important LINQ operations, in this blog, but you can always check more on Microsoft Official Documentation.

Thank you for taking the time to read this blog post. We hope that you found the information provided in the blog to be helpful and informative. For similar content, please check out our other blogs.

We appreciate your support and feedback, and we would love to hear from you if you have any questions or comments about the blog. If you have any specific topic you want us to cover in the future, please feel free to let us know.

Once again, thank you for reading our blog, and we look forward to your continued support.