The latest .NET release has new APIs to make your code cleaner and more efficient.
LINQ gets a boost with CountBy, AggregateBy, and Index, while Task.WhenEach simplifies parallelism. Collections see exciting additions like ReadOnlySet and a generic OrderedDictionary.
In this post, we’ll dive into these features, showing how they can solve common problems and elevate your .NET projects. Let’s explore!
CountBy
Let’s start with LINQ! .NET 9 introduces the new LINQ method CountBy.
The CountBy method allows for the calculation of the frequency of a key.
Person[] persons =
[
new ("Jan", "Kowalski", "Poland"),
new ("John", "Doe", "US"),
new ("Tom", "Riddle", "UK"),
new ("Jane", "Doe", "US"),
];
var countByCountry = persons.CountBy(p => p.Country);
foreach (KeyValuePair<string, int> count in countByCountry)
Console.WriteLine($"{count.Key} {count.Value}");
// Poland 1
// US 2
// UK 1
record Person(string Name, string Surname, string Country);
AggregateBy
The following new LINQ method is the AggregateBy.
The AggregateBy method allows for grouping elements by a key and returns the accumulated value for each group.
Product[] products =
[
new ("Ball", "Sports", 10),
new ("Laptop", "Electronics", 1500),
new ("Bike", "Sports", 300),
new ("Monitor", "Electronics", 400)
];
var aggregated = products
.AggregateBy(p => p.Category,
seed: 0M,
(totalPrice, curr) => totalPrice + curr.Price);
foreach (var item in aggregated)
Console.WriteLine($"{item.Key} total price is ${item.Value}");
// Sports total price is $310
// Electronics total price is $1900
public record Product(string Name, string Category, decimal Price);
Index
If you ever needed the element index of collection in the foreach loop, you could use the Select method.
.NET 9 brings a better and cleaner way to obtain the element index. Meet the Index method!
string message = "Hello";
foreach ((int index, char @char) in message.Index())
{
Console.WriteLine($"Character {@char} has index {index}");
}
// Character H has index 0
// Character e has index 1
// Character l has index 2
// Character l has index 3
// Character o has index 4
Task.WhenEach
.NET 9 introduces the Task.WhenEach method.
The Task.WhenEach method allows to join scheduled tasks and iterate through them as each one is completed.
using HttpClient http = new()
{
BaseAddress = new Uri("https://api.github.com")
};
http.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("Dotnet", "9"));
Task<GitHubUser> user1 = http.GetFromJsonAsync<GitHubUser>("users/okyrylchuk");
Task<GitHubUser> user2 = http.GetFromJsonAsync<GitHubUser>("users/jaredpar");
Task<GitHubUser> user3 = http.GetFromJsonAsync<GitHubUser>("users/davidfowl");
await foreach (Task<GitHubUser> task in Task.WhenEach(user1, user2, user3))
{
Console.WriteLine($"Name: {task.Result.Name}, Bio: {task.Result.Bio}");
}
//Name: David Fowler, Bio: Distinguished Engineer
//Name: Oleg Kyrylchuk, Bio: Microsoft MVP | Software developer
//Name: Jared Parsons, Bio: C# compiler lead
record GitHubUser(string Name, string Bio);
Previously, you had to repeatedly use Task.WaitAny on a set of tasks to pick off the next one that completes.
List<Task<GitHubUser>> tasks = [ user1, user2, user3 ];
List<GitHubUser> users = new();
while (tasks.Any())
{
var completedTask = await Task.WhenAny(tasks);
tasks.Remove(completedTask);
users.Add(await completedTask);
}
ReadOnlySet
.NET 9 introduces the ReadOnlySet.
The built-in read-only wrapper around an arbitrary mutable HashSet was missing in previous .NET versions.
HashSet<int> set = [ 1, 2, 3, 4, 5 ];
ReadOnlySet<int> readOnlySet = new(set);
Generic OrderedDictionary
The OrderedDictionary type has existed in .NET since an early age.
.NET 9 introduces the generic counterpart.
The OrderedDictionary creates a dictionary where the order of key-value pairs can be maintained.
OrderedDictionary<int, string> d = new()
{
[1] = "apple",
[2] = "banana",
[3] = "cherry",
};
d.Add(4, "orange");
d.RemoveAt(1); // Remove "banana"
d.RemoveAt(2); // Remove "orange"
d.Insert(1, 5, "elderberry"); // Insert "elderberry" at index 1
foreach (KeyValuePair<int, string> entry in d)
Console.WriteLine(entry);
// Output:
// [1, apple]
// [5, elderberry]
// [3, cherry]
Guid Version 7
NET 9 introduces a new GUID implementation based on timestamp and random.
You can create a Guid using the CreateVersion7() method.
More about GUID 7 you can read in my previous post.
var guid7 = Guid.CreateVersion7();
Console.WriteLine($"V{guid7.Version}: {guid7}");
// V7: 019378c3-ef98-773f-a043-762914c97d8c
Base64Url Helper
.NET 9 introduces a new Base64Url type.
The existing Convert.ToBase64String method can produce a string with ‘/’, ‘+’, or ‘=’ characters. They are not safe for URLs because they have special meanings in URLs.
The Base64Url helper produces the string without these characters.
byte[] toEncodeAsBytes = Encoding.UTF8.GetBytes("hello world");
var oldBase64 = Convert.ToBase64String(toEncodeAsBytes);
var newBase64 = Base64Url.EncodeToString(toEncodeAsBytes);
Console.WriteLine(oldBase64);
// aGVsbG8gd29ybGQ=
Console.WriteLine(newBase64);
// aGVsbG8gd29ybGQ
Regex.EnumerateSplits
.NET 9 introduces the Regex.EnumerateSplits method.
It works like existing Regex.Split method, it splits the string by given Regex.
The difference is that the new method accepts ReadOnlySpan<char> and returns Range struct without incurring any allocation.
ReadOnlySpan<char> input = "abcdefghij";
foreach (Range r in Regex.EnumerateSplits(input, "[aei]"))
{
Console.Write($"{input[r]}");
}
// Output: bcdfghj
PriorityQueue.Remove
The PriorityQueue type was introduced in .NET 6.
However, it missed the Remove method, which is helpful in various algorithms, such as Dijkstra’s algorithm.
.NET 9 adds the Remove method.
PriorityQueue<string, int> pq = new();
pq.Enqueue("A", 2);
pq.Enqueue("B", 2);
pq.Enqueue("C", 1);
pq.Remove("B", out string rElement, out int rPriority);
Console.WriteLine(
$"Removed element: {rElement}, priority: {rPriority}");
// Output: Removed element: B, priority: 2
while (pq.Count > 0)
{
var element = pq.Dequeue();
Console.WriteLine($"Element: {element}");
}
// Output:
// Element: C
// Element: A
TimeSpan.From Overloads
.NET 9 adds new overloads for the TimeSpan.From methods.
Previously, they accepted double type, which is a binary-based floating-point format. It can cause bugs.
The new overloads accept int and long to achieve the desired result.
// .NET 8 and older
var timeSpan1 = TimeSpan.FromSeconds(101.832);
Console.WriteLine(timeSpan1);
// 00:01:41.8319999
// .NET 9
var timeSpan2 = TimeSpan.FromSeconds(seconds: 101, milliseconds: 832);
Console.WriteLine(timeSpan2);
// 00:01:41.0008320