What's New in .NET 9 and C# 13

Introduction

.NET 9 and C# 13 have been released some days ago, so it's time for my own resume of the new features. There are tons of improvements, quite a few related to performance, so, beware, if you want the whole thing, you should read the official documentation, these are just my personal choices!

New Escape Sequence

In previous C# versions, you probably have used \u001b or \x1b escape characters. Now, to prevent occasional typos, C# 13 has introduced \e as a character literal escape sequence for the Escape character. This allows this code:

//old style, \u001b
var redText = "\u001b[31mThis is red\u001b[0m";
Console.WriteLine(redText);
//new style, with \e
var boldText = "\e[1mThis is bold\e[0m";
Console.WriteLine(boldText);

This feature is specified in https://github.com/dotnet/csharplang/issues/7400.

Overload Resolution Priority

There is a new attribute, [OverloadResolutionPriority], which can be used to control what overload is used preferably, in the case when two or more overloads can be used, depending on the passed arguments. It is used like this:

[OverloadResolutionPriority(1)]
public void SomeOperation(params ReadOnlySpan<int> numbers) { ... }
//default priority is 0, the higher, the better
public void SomeOperation(params int[] numbers) { ... }

So when you call:

SomeOperation(1, 2, 3);

It will call the overload that takes a ReadOnlySpan<int> argument.

This feature was implemented as per https://github.com/dotnet/runtime/issues/102173.

Implicit Index Access

In C# 13, we are now able to use the implicit “from the end” index operator in an object initializer expression to index an array from its end, instead of the beginning, without caring for how many elements there are in it. For example:

int[] intArray = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ];
var last = intArray[^1];  //10
var beforeLast = intArray[^2];  //9

The "starting" position is 1 (^1), and it points to the latest element, the first one will be ^length, where length is, of course, the size of the array.

This would be the same as having:

var last = intArray[intArray.Length - 1];  //10

This feature was implemented as per https://github.com/dotnet/csharplang/issues/7684.

Partial Properties and Indexers

As with partial methods, we can now declare and possibly implement partial properties (including indexers) over different files. The containing class (as well as the property) needs to be declared as partial:

//Profile.cs
public partial Profile
{
    public partial string Email { get; set; }
}

//Profile.Partial.cs
public partial Profile
{
    private string _email;
    public partial string Email
    {
        get
        {
            return _name;
        }
        set
        {
            _name = value?.ToLowerInvariant();
        }
    }
}

This feature is useful when we have class generators, like with partial methods.

Introduced by https://github.com/dotnet/csharplang/issues/6420.

allows ref struct And ref struct in Async Methods and Lambdas

These are actually two features, the first, allows developers to specify a new modifier for generic interfaces, so as to allow the usage of a ref struct type as a template parameter.

A ref struct is a structure that lives on the thread stack, and, because of that, it is the subject of some constraints like, it cannot be boxed. The new allows ref struct generic template modifier allows for these structures to be used as template generic parameters. Instead of imposing a restriction, it allows a wider range of types to be used as generic template parameters. Most people probably won't be too bothered with this, but it may come useful for high-performance scenarios and no-allocations.

See the full spec here: https://github.com/dotnet/csharplang/pull/5689.

Support for Primary Constructors in Logging Source Generator

This one is primarily targetted at logger message generators. With it, the ILogger parameter is automatically discovered in primary constructors, not just in the parameters to the partial method:

public partial class ExtendedLogger(ILogger logger)
{
    [LoggerMessage(EventId = 100, Level = LogLevel.Information, Message = "This comes from a logger generator: `{message}`")]
    public partial string LogExt(string message);
}

The ticket for this feature is https://github.com/dotnet/runtime/pull/101660.

SearchValues Enhancements

The SearchValues API, introduced in .NET 8, got some improvements, namely, it can now work with strings and perform case-insentive matches. A quick example:

var text = "The quick brown fox jumps over the lazy dog";
var values = SearchValues.Create([ "dog", "fox" ], StringComparison.OrdinalIgnoreCase);
var contains = text.AsSpan().ContainsAny(values);

This feature was requested by ticket https://github.com/dotnet/runtime/issues/87689.

New TryGetNonEnumeratedCount Method

There's a new extension method that operates on IEnumerable<T> types, and tries to retrieve the item count for a collection without actually traversing it: t's TryGetNonEnumeratedCount, and what it does is, it checks if the collection is a ICollection<T> (most collections are), IListProvider<T>, or ICollection, in which case it returns the value of the Count property. If it's not, then it must traverse it, as the Count() method does.

The originating ticket is https://github.com/dotnet/runtime/issues/27183.

New LINQ Methods

There are three new useful LINQ methods:

Version 7 Guids

The venerable Guid class got a new generation method that returns sequential Guids, according to version 7 UUID (RFC 9562). This means that they can be ordered and used as a database table primary key, for example, because the values won't be scattered. The Guid class remains pretty much unchanged, but there is a new generator for version 7, Guid.CreateVersion7(), and one that takes as a seed a DateTimeOffset parameter.

var guid1 = Guid.CreateVersion7();
var guid2 = Guid.CreateVersion7(DateTimeOffset.UtcNow);

If no DateTimeOffset is supplied, CreateVersion7() uses DateTimeOffet.UtcNow.

Read about the proposal here https://github.com/dotnet/runtime/issues/88290 and https://github.com/dotnet/runtime/issues/103658.

New Task.WhenEach Method

This one will come very handy: Task.WhenEach will return each Task as it finished processing:

using var client = new HttpClient();
var sites = new Dictionary<string, Task<string>>
{
  ["Google"] = client.GetStringAsync("https://google.com"),
  ["Weblogs"] = client.GetStringAsync("https://weblogs.asp.net/ricardoperes"),
  ["Blogger"] = client.GetStringAsync("https://developmentwithadot.blogspot.com"),
  ["Devblogs"] = client.GetStringAsync("https://devblogs.microsoft.com")
};
await foreach (var task in Task.WhenEach(sites.Values))
{
    var site = sites.SingleOrDefault(x => x.Value == task).Key;
    Console.WriteLine($"Finished: {site}");
    var html = await task;
}

Read more here: https://github.com/dotnet/runtime/issues/97355.

New HybridCache

There's a new kind of cache around, HybridCache, which acts in a peculiar way: it first stores all items in the process' memory (like an in-memory cache), but, if there is a distributed cache available, it also uses it. It features an atomic get-or-add method, cache stampede protection, and allows for customised serialisers. I won't go into details here, as I'll be writing a post on cache soon, stay tuned!

Read the full spec here: https://github.com/dotnet/aspnetcore/issues/54647.

New PriorityQueue Remove Method

There's also a new class method for PriorityQueue<TElement, TPriority>, Remove(). This method removes and returns the first (just removed) element in the collection that matches a specified one according to an optional provided comparer.

if queue.Remove(element, out var existingElement, out var existingPriority)) { ... }

We can also pass some comparer, which must implement IEqualityComparer<T>:

if queue.Remove(element, out var existingElement, out var existingPriority, new MyCustomComparer())) { ... }

Specification is here; https://github.com/dotnet/runtime/issues/93925.

Named HttpClient Registrations

Is is now possible to register named HttpClient restristrations to the Dependency Injection (DI) framework. I already blogged about this here, essentially, its about calling the AddAsKeyed() method after AddHttpClient().

builder.Services.AddHttpClient("GitHub", client =>
{
    client.BaseAddress = new("https://github.com");
}).AddAsKeyed();

This then allows retrieving the HttpClient instance as a named registration, using , for example, [FromKeyedService("GitHub")] ([FromKeyedService] attribute is also new in .NET 9).

BinaryFormatter is Gone

The venerable BinaryFormatter class is now gone, which is to say, it now always throws an exception when used. Read about it here. You know will have to resort to other serialisers, such as the built-ins JsonSerializer, XmlSerializer, or third-parties such as Newtonsoft.Json, MessagePack and Protocol Buffers, amongst many others.

Saving an Emitted Assembly

There is a new AssemblyBuilder implementation that can persist generated IL code to a generic stream or disk file. It's PersistedAssemblyBuilder and if offers a new Save method with two overloads.

Read the spec here: https://github.com/dotnet/runtime/issues/97015.

Params Collections

The params modifier has been used since always to specify "any number of arguments". It has always been applied to some array, but, now we can have other collection classes instead, which offer some advantages.

public string JoinTogether(params IReadOnlyCollection<string> words) { ... }

You can use any collection that implements IEnumerable<T> with params, most likely it will be an iterate-only, non-mutable, collection.

Read about the feature request here: https://github.com/dotnet/roslyn/issues/36.

New Lock Class

When it comes to using the lock statement, we could use any reference type, and just define a block:

private readonly object _lock = new object();
public void SynchronisedMethod()
{
    lock (_lock)
    {
        //perform some thread-critical operation
    }
}

Now we have a new type, unsurprisingly named Lock, just to help with this. A Lock is instantiated and then we can enter it, and also check if it is currently help by the current thread:

private readonly Lock _lock = new Lock();
public void SynchronisedMethod()
{
    //with a scope
    using (_lock.EnterScope())
    {
        //perform some thread-critical operation
    }

    //manually acquiring and releasing
    if (!_lock.IsHeldByCurrentThread)
    {
        _lock.Enter();
        //perform some thread-critical operation
        _lock.Exit();
    }
}

The Enter and EnterScope are different, because Enter will hold the lock on the object until we explicitly release it by calling Exit, while EnterScope will return an IDisposable object, which, when disposed of, will release the lock; both will block until the lock can be acquired. There is also a TryEnter method. which will only succeed if the lock can be acquired in a specified amount of time.

Read the full spec here https://github.com/dotnet/runtime/issues/34812 and here https://github.com/dotnet/csharplang/issues/7104.

JSON Schema and Enums as Strings

There is a new class, JsonSchemaExporter, that allows us to get the JSON schema for a given type. The method for doing that is GetJsonSchemaAsNode(). As an example, for this class:

public class Data
{
    public required string Name { get; set; }
    public string? Value { get; set; }
    public List<Data> Children { get; } = new List<Data>();
}

This call, which uses the default options:

var schema = JsonSchemaExporter.GetJsonSchemaAsNode(JsonSerializerOptions.Default, typeof(Data));

Returns this:

var schema = JsonSchemaExporter.GetJsonSchemaAsNode(JsonSerializerOptions.Default, typeof(Data));
var json = schema.ToJsonString();

Will return this schema:

{
   "type":[
      "object",
      "null"
   ],
   "properties":{
      "Name":{
         "type":"string"
      },
      "Value":{
         "type":[
            "string",
            "null"
         ]
      },
      "Children":{
         "type":[
            "array",
            "null"
         ],
         "items":{
            "$ref":"#"
         }
      }
   },
   "required":[
      "Name"
   ]
}

The requesting ticket for this is https://github.com/dotnet/runtime/issues/102788.

There is also a new attribute, [JsonStringEnumMember], which can be used to have an enumeration value be serialised as a string, instead of its underlying type (if not specified, it's int), and with a given name. This should be pretty straightforward, but here is an example:

public enum Colour
{
    [JsonStringMember("red")]
    Red = 1,
    [JsonStringMember("green")]
    Green = 2,
    [JsonStringMember("blue")]
    Blue = 3
}

Read the full spec for the attribute here: https://github.com/dotnet/runtime/issues/74385.

New Collections

Some of you may know that I am very interested about .NET collections. It should come as no surprise, as the proper choice of a collection can dramatically influence the performance of our code. Lucky for us, Microsoft keeps adding interesting and useful collection classes to the library, this time, two new ones:

Alternative Lookup Methods for Hashed Collections

Because performance is nowadays a major concert, a request was made to implement an alternative method that is allocation-free for hashed collections.

var colours = new Dictionary<string, string>
{
    ["red"] = "#ff0000",
    ["green"] = "#00ff00",
    ["blue"] = "#0000ff"
};
var lookup = colours.GetAlternateLookup<ReadOnlySpan<char>>();
var containsGreen = lookup.ContainsKey("green".AsSpan());

See the ticket here for dictionaries https://github.com/dotnet/runtime/issues/27229 and here https://github.com/dotnet/runtime/issues/101694 for sorted sets and sorted dictionaries.

New Base64Url Class

There is a new Base64Url which contains some helpful methods for working with Base64 strings. This was essentially because Convert.ToBase64String() could produce some invalid outputs, but not just that. It offers new decode and encode methods to different types, including spans.

byte[] stringToEncode = Encoding.UTF8.GetBytes("Hello, Base64 World!");
var unsafeEncoding = Convert.ToBase64String(stringToEncode);
var safeEncoding = Base64Url.EncodeToString(stringToEncode);

The originating ticket is https://github.com/dotnet/runtime/issues/66841.

New Cryptography Additions

KMAC hashing algorithm and ChaCha20-Poly1305 cipher are now supported in .NET 9.

Read the specs here https://github.com/dotnet/runtime/issues/93494 for KMAC and https://github.com/dotnet/runtime/issues/45130 for ChaCha20-Poly1305.

Metrics Gauge Instrument

There's a new Gauge<T> class for synchronous, non-additive values for metrics. It can be used like this (example adapted from the Microsoft documentation):

Meter meter = new ("Measurement.Sound");
var gauge = meter.CreateGauge<int>(name: "NoiseLevel", unit: "dB", description: "Background Noise Level");
gauge.Record(10, new TagList(new KeyValuePair<string, object?>("Room", 1)));

Read the full spec here: https://github.com/dotnet/runtime/issues/92625.

DATAS Garbage Collector Mode

.NET 9 also introduced a new garbage collector mode, DATAS. DATAS stands for Dynamic Adaptation to Application Sizes, and can lead to better performance of our server apps under heavy loads. It is set by default on server apps.

This feature was introduced here: https://github.com/dotnet/runtime/issues/79658.

OpenAPI vs Swashbuckle

Swashbuckle, which was included in previous versions and used for generating API documentation, was dropped in favour of OpenAPI. It is still possible to use it, of course, but Microsoft decided to roll out a new documentation API, which, at this time, does not have a UI to go along. For this reason, you can still use Swashbuckle UI, or other similar products. The new Microsoft.AspNetCore.OpenApi library is referenced by default on web projects.

Read about the decision here: https://github.com/dotnet/aspnetcore/issues/54599.

Conclusion

As you can see, some interesting features, even if you won't be using all of them at once. If you want more information, or believe something important is missing, please give me a shout!

References

https://github.com/dotnet/core/tree/main/release-notes/9.0/9.0.0

https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-9/overview

https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-13

https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-9/libraries

Comments

Popular posts from this blog

Audit Trails in EF Core

Domain Events with .NET

ASP.NET Core Pitfalls – Posting a String