Поиск  
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

Автор:
Matthew MacDonald
Источник:
Просмотров:
2661

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!

Похожее
Oct 17
Author: walter Torricos
Intro What is the Result pattern? Basically it is a great way to write error-tolerant code that can be composed. Do you feel that phrase sounds familiar? If you like F# you are right, I’ve taken it from the Results...
Apr 8
Author: João Simões
Performance comparison between LinkedList and ToArray Some weeks ago I created an article comparing the performance of ToList versus ToArray when creating short lived collections that won’t be mutated, usually used to prevent multiple enumerations when iterating over a temporary...
Jun 5, 2023
Author: Juan Alberto España Garcia
In this section, we’ll explore the world of unit testing in C# and .NET, learn what unit testing is, why it’s important, and the landscape of testing frameworks and tools available to developers. What is Unit Testing? Unit testing is...
Nov 22, 2021
Author: MBARK T3STO
Dispose and Finalize are two methods you often use to release resources occupied by your .NET and .NET Core applications running in the context of the CLR. Most importantly, if you have unmanaged resources in your application, you should release...
Написать сообщение
Тип
Почта
Имя
*Сообщение