Search  
Always will be ready notify the world about expectations as easy as possible: job change page
Apr 16, 2022

7 improvements you might have missed in .NET 6

Author:
Matthew MacDonald
Source:
Views:
2781

Sometimes it’s the little things that count.

When .NET 6 dropped this November, a few top-line items grabbed all the attention. I’m talking about C# 10, hot reload, Blazor’s AOT compilation, and some serious inner loop performance improvements. All of them are worthy enhancements. But if you look beyond the marquee items, you’ll see that .NET 6 also includes a number of small but smart refinements. These are the conveniences that make day-to-day programming more bearable — if you know where to find them.

Here are my favorites.

1. Easy null parameter checking

No one ever had trouble writing a null-parameter check at the top of a method. But the task is so frequent that even the C# language is experimenting with a dedicated operator to automate the job. What we got instead was a helper method that turns code like this: 

public UpdateAddress(int personId, Address newAddress)
{
    if (newAddress == null)
    {
        throw new ArgumentNullException("newAddress");
    }
    ...
}

into this: 

public UpdateAddress(int personId, Address newAddress)
{
    ArgumentNullException.ThrowIfNull(newAddress);
    ...
}

It’s nothing magical. But every fractional improvement in code readability counts when you have a massive codebase just waiting for a typo.

2. Separating dates and times

If you been coding in .NET for longer than TimeSpan.Zero, you’re well acquainted with the DateTime struct, which stores an exact calendar date and time. And you’ve probably used code like this to “zero out” the time information: 

DateTime dateValue = someDateFunction();
DateTime dateOnly = dateValue.Date;

This works perfectly well, but there’s always the chance that you’ll forget to strip out the time value, which can mess up comparisons and other types of date calculations. And even if you don’t forget, there are other possible edge cases. For example, how do you tell the difference between a DateTime object with no time information and one that’s set for 12:00 AM midnight? (Short answer: You can’t.)

.NET 6 finally plugs this hole in the simplest way possible — by creating DateOnly and TimeOnly types that can only hold part of a DateTime. And although the awkward names are a bit controversial, their use in code is straightforward and sensible: 

// Make a DateOnly explicitly (for Jan 20, 2022)
DateOnly d1 = new DateOnly(2022, 1, 20);

// Make a DateOnly from a DateTime
DateOnly d2 = DateOnly.FromDateTime(dateValue);

// Make a TimeOnly explicitly (at 3:30 PM)
TimeOnly t1 = new TimeOnly(15, 30);

// Combine a DateOnly and a TimeOnly
DateTime dtCombined = d1.ToDateTime(t1);

// Convert a DateOnly to a DateTime (at 12:00 AM)
DateTime dtCopy = d1.ToDateTime(new TimeOnly(0,0));

And of course, the DateOnly and TimeOnly types support the same basic properties and methods as DateTime.

And while we’re on the subject of time and its many computer representations, Microsoft has done a bit more work in .NET 6 to iron out compatibility issues between time zones on Windows and other operating systems, if that’s important to you.

3. Making a priority queue

We all know Stack collections, which are LIFO (the last item added is the first you retrieve). And we know Queue collections, which are FIFO (the first item added is the first you retrieve). Priority queues are different — items are retrieved based on their importance. You assign the importance when you add the item.

The simplest way to use a PriorityQueue is with an integer that represents each item’s priority. Here’s an example with a collection of strings: 

// Fill the queue
var queue = new PriorityQueue<string, int>();
queue.Enqueue("Item with priority 2", 2);
queue.Enqueue("Item with priority 1", 1);
queue.Enqueue("Item with priority 3", 3);

// Get out all the items out
while (queue.TryDequeue(out string item, out int priority))
{
    Console.WriteLine(item);
}

When you run this code, you’ll get the item with the lowest numeric priority first. So the order will be priority 1, then priority 2, and finally priority 3. It makes no difference in what order you add the items.

Instead of using an integer, you could prioritize items with dates (chronologically) or strings (alphabetically). But the most interesting approach is to build your own IComparer. You may have seen the IComparer interface before — for example, you use it if you want to apply custom sorting in an array. With a PriorityQueue, you can use it to define different rules of priority.

All an IComparer needs to do is implement a Compare() method that examines two items. You signal which “bigger” or “smaller” by returning a positive or negative integer. Here’s a really simple example with an IComparer that knows how to sort instances of a custom ProjectStatus class based on due date: 

public class ProjectComparer: IComparer<ProjectStatus>
{
    public int Compare(ProjectStatus a, ProjectStatus b)
    {
        // Compare two ProjectStatus objects based on the due dates.
        return a.Due.CompareTo(b.Due);
     }
}

public class ProjectStatus
{
    public int Status {get; set;}
    public DateOnly Due {get; set;}
}

And now you can use that ProjectComparer when you build a PriorityQueue: 

var queue = new  PriorityQueue<ProjectData, ProjectStatus>(new ProjectComparer());

4. Convenient encryption for small data

Encryption is one of those things that you use indirectly all the time, but you rarely use directly. (In fact, using manual encryption is a code smell — an indication that you might be doing something yourself that can be handled more safely and robustly by a proper library.)

On those times that you do need to use encryption, .NET has always made it a little awkward. There’s more boilerplate to write than you expect, and you need to deal with a whole stream abstraction even if you’re encrypting small values. In .NET 6, the simple case — encrypting a small chunk of data— becomes easier with a small set of new methods. Here’s the Aes.EncryptCbc() method encrypting an entire block of bytes in one go: 

byte[] data = default;

using (Aes aes = Aes.Create())
{
    aes.Key = ...
    byte[] iv = ...    // One shot encryption
    byte[] encrypted = aes.EncryptCbc(data, iv);
}

At a bare minimum, you still need to understand how to create and manage a symmetric encryption key and initialization vector. But there’s no padding, transforming, or chunking your data. DecryptCbc() works the same magic in reverse.

5. Quick cryptographic random numbers

.NET 6 has a few other simplified cryptographic utility methods for encryption with different algorithms and hashing (in which case the code savings is not as significant). But RandomNumberGenerator.GetBytes() is a useful addition. It fills a byte array with cryptographically strong pseudorandom values: 

byte[] randomBytes = RandomNumberGenerator.GetBytes(100);

You can convert these bytes to another type of data as needed: 

byte[] randomBytes = RandomNumberGenerator.GetBytes(4);
int randomInt = BitConverter.ToInt32(randomBytes);

6. Chunking collections with LINQ

Loyal readers may have noticed that I’m a little lazy when it comes to LINQ functionality. (In other words, I’m sometimes guilty of lapsing into iterative logic when a LINQ query could solve the problem more elegantly.) But that hasn’t stopped me from appreciating LINQ’s new Chunk() extension method, which gives us an easy way to fetch batches of items from a queryable collection. If you’ve ever struggled with Skip() and Take() or some of the other workarounds I’ve politely ignored, this new syntax will delight you: 

// Let's try this out with a collection of 100 random numbers
var numbers = new List<int>();
for (int i=0; i<100; i++)
{
    var rand = new Random();
    numbers.Add(rand.Next(1, 100));
}

// We'll grab ten numbers at a time
var chunkSize = 10;
 
// Grab just one chunk
foreach(var chunk in numbers.Chunk(chunkSize))
{
    Console.WriteLine("New chunk");
    foreach(var item in chunk)
    {
        Console.WriteLine(item);
    }
}

The output looks something like this: 

New chunk
923
809
842
51
478
50
554
710
327
604
New chunk
996
600
373
889
433
513
494
721
339
757
New chunk
700
920
371
...

It’s just that easy!

7. Asynchronous waiting with a timeout

If you’re a regular user of .NET’s Task API, you know that long running tasks should support cancellation, but sometimes they don’t. There are a few ways to work around this limitation, but in .NET 6 there’s a helpful WaitAsync() method with a built-in timeout. If the task isn’t completed when the timeout expires, the task keeps going, but your code stops waiting and can try something else. 

// Wait up to 30 seconds for this task:
await someTask.WaitAsync(TimeSpan.FromSeconds(30));

8. And a bonus: Easy parallelism

ForEachAsync() is a clever little method that was completely off my radar until Scott Hanselman shared this example from Oleg Kyrylchuk (follow him here on Twitter). 

using System.Net.Http.Headers;
using System.Net.Http.Json;

var userHandlers = new []
{
    "users/okyrylchuk",
    "users/shanselman",
    "users/jaredpar",
    "users/davidfowl"
};

using HttpClient client = new()
{
    BaseAddress = new Uri("https://api.github.com"),
};

client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("DotNet", "6"));ParallelOptions parallelOptions = new()
{
    MaxDegreeOfParallelism = 3
};

await Parallel.ForEachAsync(userHandlers, parallelOptions, async (uri, token) =>
{
    var user = await client.GetFromJsonAsync<GitHubUser>(uri, token);
    Console.WriteLine($"Name: {user.Name}\nBio: {user.Bio}\n");
});

public class GitHubUser
{
    public string Name { get; set; }
    public string Bio { get; set; }
}

ForEachAsync() launches a set of tasks that take place simultaneously (queued down to three at a time using the MaxDegreeOfParallelism property in this example). If you have separate operations to perform that don’t depend on each other, it makes for pretty elegant code. Oleg’s example above is fetching GitHub profiles for different users over HTTP. Neat!

Similar
Jun 3
Author: Dayanand Thombare
Introduction Delegates are a fundamental concept in C# that allow you to treat methods as objects. They provide a way to define a type that represents a reference to a method, enabling you to encapsulate and pass around methods as...
30 ноября 2020 г.
Если вы Web-разработчик и ведете разработку для браузера, то вы точно знакомы с JS, который может исполняться внутри браузера. Существует мнение, что JS не сильно подходит для сложных вычислений и алгоритмов. И хотя в последние годы JS cделал большой рывок...
Aug 22, 2021
The following are a set of best practices for using the HttpClient object in .NET Core when communicating with web APIs. Note that probably not all practices would be recommended in all situations. There can always be good reasons to...
Aug 11, 2021
Author: Mel Grubb
Code Generation Code generation is a great way to apply patterns consistently across a solution or to create multiple similar classes based on some outside input file, or even other classes in the same solution. The tooling has changed over...
Send message
Type
Email
Your name
*Message