In this article, we will discuss the new LINQ improvements introduced in .NET 6.  We will not talk about the LINQ itself because we already have an article on that topic.

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

So, let’s start.

Batching Support Using Chunk

Often when we work with an extensive collection of objects, we might have to split the collection into several smaller arrays. .NET introduces a new method in LINQ that now supports batching sequences into Size slices. This is the Chunk method:

public static IEnumerable<T[]> Chunk(this IEnumerable<T> source, int size);

This method takes an enumerable and the size as arguments and splits the given enumerable into chunks of the given size:

public static IList<Student> Students => new List<Student>()
{
    new Student("John", "CS", 10),
    new Student("James", "CS", 6),
    new Student("Mike", "IT", 8),
    new Student("Stokes", "IT", 0),
};

public static List<Student[]> Chunk(int pageSize)
{
    var studentChunks =  Students.Chunk(pageSize);
    return studentChunks.ToList();
}

In our Program class, we can call this Chunk method and pass 2 as the pageSize.

var studentChunks = LINQUtils.Chunk(2);
foreach (var studentChunk in studentChunks)
{
    Console.WriteLine(studentChunk.Count());
}

This will now divide the collection into arrays of size 2 each:

2
2

This new feature is helpful when we are trying to implement a paged API or send large data to a database.

LINQ Improvements with the MaxBy and MinBy Methods

Two new extension methods MaxBy and MinBy are introduced in LINQ to query the sequence for a maximum or minimum element that satisfies a given selector condition. The new methods take a selector query as an input and return the largest or the smallest element that satisfies the condition:

public static TSource MinBy<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, TResult> selector);

public static TSource MaxBy<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, TResult> selector);

Let’s take a look at a simple example that returns a list of students with the highest and lowest grades:

public static Student? MaxGrade()
{
    return Students.MaxBy(student => student.Grade);
}

public static Student? MinGrade()
{
    return Students.MinBy(student => student.Grade);
}

This will return the student object with maximum and minimum grades:

Name: John Dept: CS Grade: 10
Name: Stokes Dept: IT Grade: 0

Support For Default Parameters

LINQ methods like FirstOrDefault,SingleOrDefault, and LastOrDefault return the object that satisfies the condition and if there is no match, they return default values. You can read more about these methods in our LINQ Basics article.

However, with the improvements introduced in LINQ, we can now specify a default value to return from these methods. For example, in the specified student’s collection, we can now specify the method to return a new student instance instead of null, if the condition is not met:

static Student defaultStudent = new Student(name: "", department: "", grade: -1);

public static Student FirstOrDefaultStudent()
{
    return Students.FirstOrDefault(student => student.Name.Equals("Fake Student"), defaultStudent);
}

public static Student LastOrDefaultStudent()
{
    return Students.LastOrDefault(student => student.Name.Equals("Fake Student"), defaultStudent);
}

public static Student SingleOrDefaultStudent()
{
    return Students.SingleOrDefault(student => student.Name.Equals("Fake Student"), defaultStudent);
}

This will return the default student in all three methods:

Name:  Dept:  Grade: -1
Name:  Dept:  Grade: -1
Name:  Dept:  Grade: -1

This new improvement helps a lot in avoiding a NullReferenceException in .NET.

Index and Range Arguments

Index and Range arguments are already available in C#. Starting from .NET 6, they are now available for LINQ. It now supports negative Index for ElementAt and Range for Take methods.

Previously, if we wanted to get something from the end of the sequence, we had to calculate the length and subtract the index from the size. We can now use the ^ operator to get the index from the end of the list:

public static Student ElementAt(Index index)
{
    return Students.ElementAt(index);
}

var student = LINQUtils.ElementAt(^3); 
Console.WriteLine(student);

This will return the 3rd element from the end of the list:

Name: James Dept: CS Grade: 6

The Range struct  is now referenced in LINQ for the Take method. We can now take elements that fall within a range:

public static List<Student> Take(Range range)
{
    return Students.Take(range).ToList();
}

var studentsRange = LINQUtils.Take(1..3); 
PrintEnumerable(studentsRange);

This will return all the elements between indexes 1 and 3:

Name: James Dept: CS Grade: 6
Name: Mike Dept: IT Grade: 8

Get Count Without Enumeration

When we call the Count() method, LINQ checks if the count is readily available and returns it. However, if the count is not available, then it will enumerate the entire sequence and return the count. But this might cause a lot of time in some cases if an IQueryable is involved.

To avoid this enumeration, a new method TryGetNonEnumeratedCount has been introduced to get the count of sequences without enumerating the entire sequence. In case the count is not available, then it will return false and assign zero to the out variable:

public static bool CountStudents(out int count)
{
    var queryableStudents = Students.AsQueryable();

    return queryableStudents.TryGetNonEnumeratedCount(out count);
}

Now, let’s call this method in the Program class:

int count = -1; 
var doesCountExist = LINQUtils.CountStudents(out count); 
Console.WriteLine(doesCountExist);

Here, we are trying to get the count of students collection but since we are implementing it as an IQueryable, this will return false:

False

It will also set the count variable to 0 (zero). Of course, if we remove the AsQueryable method and use the Students collection as is, the method will return true and set the count to 4.

Zip With 3 IEnumerable

Previously, the Zip method was used for enumerating two sequences together. Starting from .NET 6, the Zip extension method supports up to 3 sequences:

public static IEnumerable<(string, string, int)> ZipEnumerables(List<string> names, List<string> departments, List<int> grades)
{
    return names.Zip(departments, grades);
}

Once we call this method:

var names = new List<string>() { "John", "James", "Mike" }; 
var departments = new List<string>() { "ME", "AP", "IT" }; 
var grades = new List<int>() { 10, 6, 8 }; 

var enumeratedList = LINQUtils.ZipEnumerables(names, departments, grades); 
PrintEnumerable(enumeratedList);

As a result, we will have multiple tuples with name, department, and grade as values:

(John, ME, 10)
(James, AP, 6)
(Mike, IT, 8)

Support for Set Operations

The existing set-based methods Distinct, Except, Intersect, and Union are now improved by LINQ. It now includes support for key-based selectors with the four set-based operations namely DistinctBy, ExceptBy, IntersectBy, and UnionBy

DistinctBy

We use DistinctBy when we want to get a distinct list of elements that satisfy a key selector. For example, let’s say we want to get a list of students who pursue studies in distinct departments:

public static IEnumerable<Student> DistinctByDepartment()
{
    return Students.DistinctBy(student => student.Department);
}

This will now return a student for each distinct department:

Name: John Dept: CS Grade: 10
Name: Mike Dept: IT Grade: 8

ExceptBy

Similar to ExceptExceptBy returns the sequence of elements that are present in the first enumerable but not in the second. One additional advantage of using the ExceptBy is that we can use a key selector to choose on which property we can run the comparison. Since we want the key comparison for departments, we need to select the department from the second list along with the key selector:

public static IEnumerable<Student> ExceptByDepartment(List<Student> secondList)
{
    return Students.ExceptBy(secondList.Select(student => student.Department), student => student.Department);
}

We can now return the list of students with the distinct department which is not present in the list of students we pass as the argument:

var studentsList = new List<Student>()
{
    new Student("Perry", "CE", 10),
    new Student("Dottin", "CU", 6),
    new Student("Sciver", "ME", 8),
    new Student("Mahesh", "IT", 3),
};

var unCommonStudents = LINQUtils.ExceptByDepartment(studentsList);
PrintEnumerable(unCommonStudents);

The CS department appears twice in the first list of students but not in the second. However, since this is a set operation it will only consider distinct values and hence will return the first student to the CS department:

Name: John Dept: CS Grade: 10

IntersectBy

As the name suggests, we use IntersectBy when we want to get the intersection of two sequences. Similar to other By methods, this method also looks for the intersection of the key selectors:

public static IEnumerable<Student> IntersectByDepartment(List<Student> secondList)
{
    return Students.IntersectBy(secondList.Select(student => student.Department), student => student.Department);
}

This will return the list of distinct students whose department is common in both the lists:

Name: John Dept: CS Grade: 10
Name: Mike Dept: IT Grade: 8

UnionBy

We use UnionBy for combining two or more sequences into a single sequence using a key selector. For example, if we use the department as our key selector, the combined list will contain distinct elements for all the departments from both the sequences :

public static IEnumerable<Student> UnionByDepartment(List<Student> secondList)
{
    return Students.UnionBy(secondList, student => student.Department);
}

This will print the list of the student names with distinct departments from both the lists for all the departments:

Name: John Dept: CS Grade: 10
Name: Mike Dept: IT Grade: 8
Name: Perry Dept: CE Grade: 10
Name: Dottin Dept: CU Grade: 6
Name: Sciver Dept: ME Grade: 8

Conclusion

In this article, we discussed a lot about the new LINQ methods as well as the improvements to the existing ones. We also learned about their use cases along with examples.