Skip to content
Bruno Sonnino
Menu
  • Home
  • About
Menu

Linq improvements in .NET 6

Posted on 28 January 2022

A nice improvement in .NET is the introduction of LINQ, in .NET 4.5. With that, working with data was simplified a lot and, when I go to a language that doesn’t have something like it, I feel lost (having to deal with for and foreach became painful for me 😃.

The features available in LINQ made my code more synthetic and readable, but sometimes, there was something that wasn’t easily attained with the default features. Microsoft heard that and introduced new features, many of them I was expecting since a long time. These new features came with no huge announcements, but, nevertheless, they are very nice improvements.

Index and Range parameters

Index and Ranges were introduced in C#8, they can ease a lot when you must get a subrange of an array or list:

var arr = Enumerable.Range(1,100).ToArray();
Console.WriteLine($"[{string.Join(",",arr[10..15])}]");
Console.WriteLine(arr[^1]);
var list = new List<int>(arr);
Console.WriteLine(list[^1]);
Console.WriteLine(list[^5]);
C#

When you run this code, you will get something like this:

[11,12,13,14,15]
100
100
96
PowerShell

This is nice, but when you wanted to work with Linq, you were not able to use indexes and ranges. Until now. .NET 6 allows using Ranges and Indices in Linq queries. This is very nice, because when you wanted a subrange of an IEnumerable, you should do something like:

list.Skip(10).Take(5)
C#

Now, you can do the same with:

list.Take(10..15)
C#

Or take the last 10 elements with

list.Take(^10..)
C#

You can also take a single element with ElementAt using Indices. To get the last element in the list, you can use:

list.ElementAt(^1)
C#

Chunking

One thing that is very common is to divide our data in chunks, in order to present it in pieces, so the user does not have to scroll long lists of information. Until now, you had to program that by hand, which could lead to errors. With the new Chunk method, you can split your data in chunks. For example, if you want to split the data in blocks of seven elements, you can do:

var chunked = list.Chunk(7);
C#

With this code, you will obtain something like

[1,2,3,4,5,6,7]
[8,9,10,11,12,13,14]
[15,16,17,18,19,20,21]
[22,23,24,25,26,27,28]
[29,30,31,32,33,34,35]
[36,37,38,39,40,41,42]
[43,44,45,46,47,48,49]
[50,51,52,53,54,55,56]
[57,58,59,60,61,62,63]
[64,65,66,67,68,69,70]
[71,72,73,74,75,76,77]
[78,79,80,81,82,83,84]
[85,86,87,88,89,90,91]
[92,93,94,95,96,97,98]
[99,100]
PowerShell

And you can get one chunk with

chunked.ElementAt(3)
chunked.ElementAt(^1)
C#

Zipping

Sometimes you want to combine three Enumerables into one. Combining Enumerables is done using the Zip method, which allowed to combine only two items at once. .NET 6 introduced the possibility of zipping three sequences at once (if you want more sequences, you must chain Zip functions). For example, if you have these three enumerables:

var list1 = Enumerable.Range(1,100).Select(i => $"ID {i}").ToList();
var list2 = Enumerable.Range(1,100).Select(i => $"Name {i}").ToList();
var list3 = Enumerable.Range(1,100).Select(i => $"Address {i}").ToList();
C#

You can combine it into an IEnumerable of tuples with three elements each with:

var zipped = list1.Zip(list2,list3);
C#

One note, here: in .NET 5 you could use a function to zip two sequences int another one and generate anything else than a tuple. .NET 6 didn’t change that and, if you want to zip the three IEnumerables into an IEnumerable of a class, for example, you must still do something like this:

var zipped1 = list1.Zip(list2, (l1, l2) => new { ID = l1, Name = l2 })
    .Zip(list3, (l1, l2) => new { ID = l1.ID, Name = l1.Name, Address = l2 });
C#

DistinctBy, ExceptBy, UnionBy, InterceptBy

One thing that I use a lot is the distinct operator, to get unique values in a sequence. Until now, when I had a class and wanted to get distinct values in a class by some field, and I’m not interested in the other fields, I had to do something like:

public record Person(string Name, int Age);
var people = new List
{
    new Person("John", 30 ),
    new Person("Peter", 40),
    new Person("Mary", 20 ),
    new Person("Jane", 30 ),
    new Person("Larry", 50),
    new Person("Anne", 50 ),
    new Person("Paul", 20),
};
var distinctByAge = people.GroupBy(p => p.Age).Select(g => g.Key);
C#

That worked fine, but lacked clarity – the intent was not explicit and it was hard to understand – Why this GroupBy is there?

In .NET 6, the DistinctBy comes to solve that. Now, you can use something like this to get the distinct values :

var distinctAges = people.DistinctBy(p => p.Age).Select(p => p.Age);
C#

Now the intent is clear and the code is easier to follow.

You can also use ExceptBy, to filter a sequence depending on another, like in

var excludedAges = new List<int> {30,40};
var people1 = people.ExceptBy(excludedAges, p => p.Age);
C#

One note, here. Due to the way ExceptBy is coded (it uses a HashSet), it will only add the first duplicate element in the result. In our code, it should show:

Person { Name = Mary, Age = 20 }
Person { Name = Larry, Age = 50 }
Person { Name = Anne, Age = 50 }
Person { Name = Paul, Age = 20 }
PowerShell

But it only shows:

Person { Name = Mary, Age = 20 }
Person { Name = Larry, Age = 50 }
PowerShell

If you want all items that don’t match the excluded ages, you should still go with:

var people2 = people.Where(p => !excludedAges.Contains(p.Age));
C#

If you want to join two sequences, removing duplicates between them, you can use the UnionBy method. This code joins the two lists into another, removing the duplicates:

var people3 = new List<Person>
{
    new Person("John", 20 ),
    new Person("Peter", 25),
    new Person("Paul", 20 ),
    new Person("Ringo", 22 ),
    new Person("George", 23),
    new Person("Anne", 50 ),
    new Person("Mark", 20),
};
var people4 = people.UnionBy(people3, p => p.Name);
C#

If you want the have the names present in both lists, you can use the IntersectBy method:

var includedAges = new List<int> {30,40};
var people5 = people.IntersectBy(includedAges, p => p.Age);
C#

In the same way of the ExceptBy, the duplicates are not included. If you want to include them, you should use:

var people6 = people.Where(p => includedAges.Contains(p.Age));
C#

MaxBy and MinBy

When using the methods Max and Min, the sequences should implement the IComparable interface, so they could be compared and the maximum and minimum evaluated. That posed a problem, especially if the class you wanted to compare didn’t implement the IComparable interface. Now, with MinBy and MaxBy you don’t have to use the IComparable and can use something like:

var minByAge = people.MinBy(p => p.Age);
var maxByAge = people.MaxBy(p => p.Age);
C#

This code won’t show all the elements with minimum age. To get that, you should use something like

var minAge = people.Select(p => p.Age).Min();
var allMinByAge = people.Where(p => p.Age == minAge);
var maxAge = people.Select(p => p.Age).Max();
var allMaxByAge = people.Where(p => p.Age == maxAge);
C#

FirstOrDefault, LastOrDefault, SingleOrDefault with a default parameter

These three functions returned Default(T) if the element was not found or the list was empty. This could pose a problem or extra checks if the element was not found. Now, we can set a default value when the element is not found and, in this case, we don’t have to deal with null checks:

var firstOrDefault = people.FirstOrDefault(p => p.Age == 25,new Person("Unknown",25));
var lastOrDefault = people.LastOrDefault(p => p.Age == 25,new Person("Unknown",25));
var singleOrDefault = people.SingleOrDefault(p => p.Age == 25,new Person("Unknown",25));
C#

In all the three cases, the code will return a Person with name Unknown and Age = 25

TryGetNonEnumeratedCount

When you have an IEnumerable and you use the Count() method, it will enumerate the collection, even if it has another method to get the count in another way, thus penalizing the performance. For that, .NET 6 implemented the TryGetNonEnumeratedCount method to try to use another method to get the count, if available. This function will return true if a faster method was available, or false, if not. That way, you can take an action to use something more performant and avoid multiple enumerations. For example:

IEnumerable<Person> people7 = people;
Console.WriteLine(people7.TryGetNonEnumeratedCount(out int count));
C#

Will return true, because The List implements the Count property to get the count. When you have an IEnumerable, result of a Linq operation, like in

var people6 = people.Where(p => includedAges.Contains(p.Age));
Console.WriteLine(people6.TryGetNonEnumeratedCount(out int count1));
C#

It will return false and the count1 variable will have the actual count of the sequence.

Conclusion

As you can see, there are several improvements to Linq in .NET 6. They were not huge improvements, but brought some ease to the development. I’m sure that I will use them a lot.

The sample code for this article is at https://github.com/bsonnino/LinqImprovements

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

  • May 2025
  • December 2024
  • October 2024
  • August 2024
  • July 2024
  • June 2024
  • November 2023
  • October 2023
  • August 2023
  • July 2023
  • June 2023
  • May 2023
  • November 2022
  • October 2022
  • September 2022
  • August 2022
  • June 2022
  • April 2022
  • March 2022
  • February 2022
  • January 2022
  • July 2021
  • June 2021
  • May 2021
  • April 2021
  • March 2021
  • February 2021
  • January 2021
  • December 2020
  • October 2020
  • September 2020
  • April 2020
  • March 2020
  • January 2020
  • November 2019
  • September 2019
  • August 2019
  • July 2019
  • June 2019
  • April 2019
  • March 2019
  • February 2019
  • January 2019
  • December 2018
  • November 2018
  • October 2018
  • September 2018
  • August 2018
  • July 2018
  • June 2018
  • May 2018
  • November 2017
  • October 2017
  • September 2017
  • August 2017
  • June 2017
  • May 2017
  • March 2017
  • February 2017
  • January 2017
  • December 2016
  • November 2016
  • October 2016
  • September 2016
  • August 2016
  • July 2016
  • June 2016
  • May 2016
  • April 2016
  • March 2016
  • February 2016
  • October 2015
  • August 2013
  • May 2013
  • February 2012
  • January 2012
  • April 2011
  • March 2011
  • December 2010
  • November 2009
  • June 2009
  • April 2009
  • March 2009
  • February 2009
  • January 2009
  • December 2008
  • November 2008
  • October 2008
  • July 2008
  • March 2008
  • February 2008
  • January 2008
  • December 2007
  • November 2007
  • October 2007
  • September 2007
  • August 2007
  • July 2007
  • Development
  • English
  • Português
  • Uncategorized
  • Windows

.NET AI Algorithms asp.NET Backup C# Debugging Delphi Dependency Injection Desktop Bridge Desktop icons Entity Framework JSON Linq Mef Minimal API MVVM NTFS Open Source OpenXML OzCode PowerShell Sensors Silverlight Source Code Generators sql server Surface Dial Testing Tools TypeScript UI Unit Testing UWP Visual Studio VS Code WCF WebView2 WinAppSDK Windows Windows 10 Windows Forms Windows Phone WPF XAML Zip

  • Entries RSS
  • Comments RSS
©2025 Bruno Sonnino | Design: Newspaperly WordPress Theme
Menu
  • Home
  • About