Vintage film cameras in a shelf

Four new LINQ methods in .NET 6: Chunk, DistinctBy, Take, XOrDefault

LINQ isn’t a new feature in the C# language. It was released back in C# version 3.0 in the early 2000s. And, after more than ten years, it was finally updated with the .NET 6 release. These are four of the new LINQ methods and overloads in .NET 6.

.NET 6 introduced new LINQ methods like Chunk, DistinctBy, MinBy, and MaxBy. Also, new overloads to existing methods like Take with ranges and FirstOrDefault with an optional default value. Before this update, to use similar methods, custom LINQ methods or third-party libraries were needed.

1. Chunk

Chunk splits a collection into buckets or “chunks” of at most the same size. It receives the chunk size and returns a collection of collections.

For example, let’s say we want to watch all movies we have in our catalog. But, we can only watch three films on a single weekend. Let’s use the Chunk method for that.

// This is a Console app without the Main class declaration
// and with global using statements
var movies = new List<Movie>
{
    new Movie("Titanic", 1998, 4.5f),
    new Movie("The Fifth Element", 1997, 4.6f),
    new Movie("Terminator 2", 1991, 4.7f),
    new Movie("Avatar", 2009, 5),
    new Movie("Platoon", 1986, 4),
    new Movie("My Neighbor Totoro", 1988, 5)
};

// Split the movies list into chunks of three movies
var chunksOf3 = movies.Where(movie => movie.Rating > 4.5f)
                      .Chunk(3);

foreach (var chunk in chunksOf3)
{
    PrintMovies(chunk);
}

// Output:
// The Fifth Element,Terminator 2,Avatar
// My Neighbor Totoro

static void PrintMovies(IEnumerable<Movie> movies)
{
    Console.WriteLine(string.Join(",", movies.Select(movie => movie.Name)));
}

record Movie(string Name, int ReleaseYear, float Rating);

Notice, we used three recent C# features: the Top-level statements, records from C# 9.0, and global using statements from C# 10.0. We don’t need all the boilerplate code to write a Console application anymore.

The Chunk method returns chunks with at most the given size. It returns fewer elements in the last bucket when there aren’t enough elements in the source collection.

2. DistinctBy

Unlike Distinct, DistinctBy receives a delegate to select the property to use as the comparison key and returns the objects containing the distinct values, not only the distinct values themselves.

Let’s find the movies containing unique ratings, not just the ratings.

var movies = new List<Movie>
{
    new Movie("Titanic", 1998, 4.5f),
    new Movie("The Fifth Element", 1997, 4.6f),
    new Movie("Terminator 2", 1991, 4.7f),
    new Movie("Avatar", 2009, 5),
    new Movie("Platoon", 1986, 4),
    new Movie("My Neighbor Totoro", 1988, 5)
};

var distinctRatings = movies.DistinctBy(movie => movie.Rating);
PrintMovies(distinctRatings);

// Output:
// Titanic,The Fifth Element,Terminator 2,Avatar,Platoon

Also, there are similar alternatives to existing methods such as MinBy, MaxBy, ExceptBy, IntersectBy, and UnionBy. They work with a delegate to select a property to use as the comparison key and return the “containing” objects, not only the result.

We can take a look at DistinctBy source code to see what’s inside a LINQ method. It’s not that intimidating after all.

Vintage movie camera
Photo by Joshua Hanks on Unsplash

3. Take with Ranges

Take receives a range of indexes to pick a slice of the input collection, not only the first consecutive elements. Take with a range replaces Take followed by Skip to choose a slice of elements.

Let’s choose a slice of our catalog using Take with ranges and the index-from-end operator.

var movies = new List<Movie>
{
    new Movie("Titanic", 1998, 4.5f),
    new Movie("The Fifth Element", 1997, 4.6f),
    new Movie("Terminator 2", 1991, 4.7f),
    new Movie("Avatar", 2009, 5),
    new Movie("Platoon", 1986, 4),
    new Movie("My Neighbor Totoro", 1988, 5)
};

var rangeOfMovies = movies.Take(^5..3);
PrintMovies(rangeOfMovies);

// Output
// The Fifth Element,Terminator 2

Notice Take(^5..3) selects elements starting from the fifth position from the end (^5) up to the third position from the start (3). We didn’t need to use the Skip method for that.

Now that we have Take with Ranges is easier to find the last “n” elements of a collection. We would need Take(^n...). For example, let’s find the last three movies on our catalog.

var movies = new List<Movie>
{
    new Movie("Titanic", 1998, 4.5f),
    new Movie("The Fifth Element", 1997, 4.6f),
    new Movie("Terminator 2", 1991, 4.7f),
    new Movie("Avatar", 2009, 5),
    new Movie("Platoon", 1986, 4),
    new Movie("My Neighbor Totoro", 1988, 5)
};

var lastThreeMovies = movies.Take(^3..);
PrintMovies(lastThreeMovies);

// Output
// Avatar,Platoon,My Neighbor Totoro

Notice Take() did all the trick. Before this .NET update, we had to use Skip(movies.Count - 3).Take(3). More compact.

4. XOrDefault methods with an optional default value

FirstOrDefault has a new overload to return a default value when the collection is empty or doesn’t have any elements that satisfy the given condition. Other methods with the suffix ‘OrDefault’ have similar overloads.

Let’s find in our catalog of movies a “perfect” film. Otherwise, let’s return our favorite movies from all times.

Before this update, we had to check if a LINQ query with FirstOrDefault returned a non-null value, or we had to use the LINQ DefaultIfEmpty method. Like this,

var allTimesFavorite = new Movie("Back to the Future", 1985, 5);

// Using the Null-coalescing assignment ??= operator
var favorite = movies.FirstOrDefault(movie => movie.Rating == 10);
favorite ??= allTimesFavorite;
//       ^^^

// Or
// Using the DefaultIfEmpty method
var favorite = movies.Where(movie => movie.Rating == 10)
                     .DefaultIfEmpty(allTimesFavorite)
                     // ^^^^^
                     .First();

With the new overload, we can pass a safe default. Like this,

var allTimesFavorite = new Movie("Back to the Future", 1985, 5);

var movies = new List<Movie>
{
    new Movie("Titanic", 1998, 4.5f),
    new Movie("The Fifth Element", 1997, 4.6f),
    new Movie("Terminator 2", 1991, 4.7f),
    new Movie("Avatar", 2009, 5),
    new Movie("Platoon", 1986, 4),
    new Movie("My Neighbor Totoro", 1988, 5)
};

// We have a safe default now. See the second parameter
var favorite = movies.FirstOrDefault(movie => movie.Rating == 10, allTimesFavorite);
// We don't need to check for null here
Console.WriteLine(favorite.Name);

// Output:
// Back to the future

Notice the second parameter we passed to the FirstOrDefault method.

To use these new methods and overloads, install on your machine the latest version of the .NET 6 SDK from the .NET official page and use as target framework .net6 in your csproj files. For example, this is a sample csproj file for a Console app using .net6,

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
    <!--             ^^^^^  -->
  </PropertyGroup>

</Project>

Voilà! These are four new LINQ methods released in the .NET 6 updated. I really like the FirstOrDefault with a safe default. That help us to prevent one of the common LINQ mistakes: using the XOrDefault methods without null checking afterwards.

To learn about LINQ and other methods, check my quick guide to LINQ, these five common LINQ methods in Pictures, how to use LINQ GroupBy method and two new LINQ methods in .NET 9.

If you want to write more expressive code to work with collections, check my course Getting Started with LINQ on Educative, where I cover from what LINQ is, to refactoring conditionals with LINQ and to the its new methods and overloads in .NET6. All you need to know to start using LINQ in your everyday coding.

Happy coding!