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.
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 Except
, ExceptBy
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.