.NET8 Officially Released, New Changes in C#12

.NET8 Officially Released, New Changes in C#12

Although .NET 8 brings many enhancements in areas such as artificial intelligence, cloud-native, performance, native AOT, etc., I am still most interested in the changes in the C# language and some framework-level aspects. Below I introduce the new features in C# 12 and the framework that I find most practical.

Last updated 11/17/2023 5:36 PM
不止dotNET
8 min read
Category
.NET
Tags
.NET C# AOT Native AOT AI

Although version 8 brings many enhancements, such as AI, cloud-native, performance, native AOT, etc., I am most concerned about changes in the C# language and some framework-level changes. Below I will introduce the new features in C# 12 and the framework that I think are practical.

img

At the .NET Conf 2023 conference, .NET 8 was officially released. .NET 8 is a long-term support (LTS) version, which means it will receive three years of support and patches. We also plan to upgrade the framework from .NET Core 3.1 to 8. I will share the upgrade process later after it is completed.

To use .NET 8, you need to install the relevant SDK, which can be downloaded at this address: https://dotnet.microsoft.com/en-us/download/dotnet/8.0, or upgrade VS2022 to 17.8.

Although version 8 brings many enhancements in areas such as AI, cloud-native, performance, native AOT, etc., I am still most concerned about changes in the C# language and some framework-level changes. Below I will introduce the new features in C# 12 and the framework that I think are practical. For a complete list of updates, see the official documentation: https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-8.

Serialization Enhancements

Built-in Support for Additional Types

  1. Serialization is now supported for additional types: Half, Int128, UInt128. In .NET 7, serializing these types did not throw an error, but the content could not be properly retrieved.
  2. Serialization is now supported for ReadOnlyMemory<T> and Memory<T> types.
  3. When T is byte, the serialization result is base64; otherwise, it is a JSON array.
using System.Text.Json;
// Output: [65500,170141183460469231731687303715884105727,340282366920938463463374607431768211455]
Console.WriteLine(JsonSerializer.Serialize(new object[] { Half.MaxValue, Int128.MaxValue, UInt128.MaxValue }));
// Output: "AQIDBAUG"
Console.WriteLine(JsonSerializer.Serialize<ReadOnlyMemory<byte>>(new byte[] { 1,2,3,4,5,6}));
// Output: [1,2,3]
Console.WriteLine(JsonSerializer.Serialize<Memory<int>>(new int[] { 1, 2, 3 }));

Interface Hierarchy

IDerived value = new DerivedImplement { Base = 0, Derived = 1 };
Console.WriteLine(JsonSerializer.Serialize(value));
// Output: {"Base":0,"Derived":1}

public interface IBase
{
    public int Base { get; set; }
}

public interface IDerived : IBase
{
    public int Derived { get; set; }
}

public class DerivedImplement : IDerived
{
    public int Base { get; set; }
    public int Derived { get; set; }
}

In the code above, the IDerived interface inherits from IBase and thus has two properties.

In previous versions (3.1, 6, 7), using the interface IDerived (which contains two properties) to receive an object instance and then serializing it would result in only: {"Derived":1}. The inherited property Base was not recognized.

In version 8, this has been improved and the expected result is obtained. It is worth noting that if you used a workaround to handle this before, you need to test and adjust accordingly after upgrading.

Naming Policies

The following image shows the naming policies supported for serialization in version 8:

img

In previous versions (3.1, 6, 7), only CamelCase was supported. The new policies added in version 8 are:

  • KebabCaseLower: lowercase with hyphens, e.g., user-name.
  • KebabCaseUpper: uppercase with hyphens, e.g., USER-NAME.
  • SnakeCaseLower: lowercase with underscores, e.g., user_name.
  • SnakeCaseUpper: uppercase with underscores, e.g., USER_NAME.
var options1 = new JsonSerializerOptions
{
    PropertyNamingPolicy = JsonNamingPolicy.KebabCaseLower,
};
var options2 = new JsonSerializerOptions
{
    PropertyNamingPolicy = JsonNamingPolicy.KebabCaseUpper,
};
var options3 = new JsonSerializerOptions
{
    PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
};
var options4 = new JsonSerializerOptions
{
    PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseUpper,
};
Console.WriteLine(JsonSerializer.Serialize(new UserInfo() { UserName = "oec2003" }, options1));
Console.WriteLine(JsonSerializer.Serialize(new UserInfo() { UserName = "oec2003" }, options2));
Console.WriteLine(JsonSerializer.Serialize(new UserInfo() { UserName = "oec2003" }, options3));
Console.WriteLine(JsonSerializer.Serialize(new UserInfo() { UserName = "oec2003" }, options4));

public class UserInfo
{
    public string? UserName { get; set; }
}

The results are as follows:

img

Call API to Directly Get an Object

Now suppose an interface returns data as shown in the following image:

img

If you were using a version prior to 8, you would need to get the interface content first, then deserialize it. The code would look like this:

const string RequestUri = "http://localhost:5145/user";
using var client = new HttpClient();
var stream = await client.GetStreamAsync(RequestUri);
// Deserialize
var users = JsonSerializer.DeserializeAsyncEnumerable<UserInfo>(stream);
await foreach (UserInfo user in users)
{
    Console.WriteLine($"Name: {user.userName}");
}
Console.ReadKey();

public record UserInfo(string userName);

In version 8, you can directly call the GetFromJsonAsAsyncEnumerable method to get the object without deserializing:

const string RequestUri = "http://localhost:5145/user";
using var client = new HttpClient();
IAsyncEnumerable<UserInfo> users = client.GetFromJsonAsAsyncEnumerable<UserInfo>(RequestUri);

await foreach (UserInfo user in users)
{
    Console.WriteLine($"Name: {user.userName}");
}
Console.ReadKey();

public record UserInfo(string userName);

The results of both code snippets are the same, as shown below:

img

Random Number Enhancements

  1. In version 8, the Random class provides a GetItems() method, which can randomly select items from a given set to generate a new collection based on a specified count:
ReadOnlySpan<string> colors = new[]{"Red","Green","Blue","Black"};

string[] t1 = Random.Shared.GetItems(colors, 10);
Console.WriteLine(JsonSerializer.Serialize(t1));

// Output: ["Black","Green","Blue","Blue","Green","Blue","Green","Black","Green","Blue"]
// Will be different each time
Console.ReadKey();
  1. The Shuffle() method provided by Random can shuffle the order of items in a collection:
string[] colors = new[]{"Red","Green","Blue","Black"};
Random.Shared.Shuffle(colors);

Console.WriteLine(JsonSerializer.Serialize(colors));

Console.ReadKey();

New Types for Improved Performance

  1. Added FrozenDictionary<TKey,TValue> and FrozenSet, which are in the System.Collections.Frozen namespace. Once these collection types are created, you cannot change any keys or values, thus enabling faster read operations.

Below is the code using BenchmarkDotNet to test FrozenDictionary vs Dictionary:

BenchmarkRunner.Run<FrozenDicTest>();
Console.ReadKey();

[SimpleJob(RunStrategy.ColdStart, iterationCount:5)]
public class FrozenDicTest
{
    public static Dictionary<string, string> dic = new() {
        { "name1","oec2003"},
        { "name2","oec2004"},
        { "name3","oec2005"}
    };

    public static FrozenDictionary<string, string> fdic = dic.ToFrozenDictionary();

    [Benchmark]
    public void TestDic()
    {
        for (int i = 0; i < 100000000; i++)
        {
            dic.TryGetValue("name", out _);
        }
    }

    [Benchmark]
    public void TestFDic()
    {
        for (int i = 0; i < 100000000; i++)
        {
            fdic.TryGetValue("name", out _);
        }
    }
}

The test results show a significant improvement:

img

  1. The new System.Buffers.SearchValues class can be used for string searching and matching. Compared to string operations, performance is greatly improved. Again, using BenchmarkDotNet for testing:
BenchmarkRunner.Run<SearchValuesTest>();
Console.ReadKey();

[SimpleJob(RunStrategy.ColdStart, iterationCount: 5)]
public class SearchValuesTest
{
    [Benchmark]
    public void TestString()
    {
        var str = "!@#$%^&*()_1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
        for (int i = 0; i < 100000000; i++)
        {
            str.Contains("z");
        }
    }

    [Benchmark]
    public void TestSearchValues()
    {
        var sv = SearchValues.Create("!@#$%^&*()_1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"u8);
        byte b = (byte)"z"[0];
        for (int i = 0; i < 100000000; i++)
        {
            sv.Contains(b);
        }
    }
}

The results show about a 5x improvement:

img

Dependency Injection Enhancements

In versions before 8, dependency injection was written as follows:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddTransient<IUser, UserA>();

var app = builder.Build();

app.MapGet("/user", (IUser user) =>
{
    return $"hello , {user.GetName()}";
});

app.Run();

internal interface IUser
{
    string GetName();
}
internal class UserA: IUser
{
    public string GetName() => "oec2003";
}

If the IUser interface has two implementations, the code above only gets an instance of the last registered class. To inject multiple implementations for a single interface, additional code is needed, which is cumbersome.

Version 8 adds a keyed injection feature, making it very convenient. See the code below:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddKeyedSingleton<IUser, UserA>("A");
builder.Services.AddKeyedSingleton<IUser, UserB>("B");

var app = builder.Build();

app.MapGet("/user1", ([FromKeyedServices("A")] IUser user) =>
{
    return $"hello , {user?.GetName()}";
});
app.MapGet("/user2", ([FromKeyedServices("B")] IUser user) =>
{
    return $"hello , {user?.GetName()}";
});

app.Run();

internal interface IUser
{
    string GetName();
}
internal class UserA: IUser
{
    public string GetName() => "oec2003";
}
internal class UserB : IUser
{
    public string GetName() => "oec2004";
}
Keep Exploring

Related Reading

More Articles
Same category / Same tag 1/5/2026

2025 Annual Summary for All .NET Developers

I believe everyone has seen many articles like "Sorry, C# has fallen out of the first tier" this year. How is the .NET ecosystem really? This article will systematically outline the technology trends and important events that .NET developers should pay the most attention to in 2025, covering the latest developments and trends in AI, .NET evolution, and the integration of the two, in order to help everyone find their positioning and meet future challenges and opportunities.

Continue Reading
Same category / Same tag 2/25/2025

.NET 10 Preview 1 Released

Today .NET 10 Preview 1 was released. I downloaded it immediately, upgraded the Avalonia UI project and blog website. The former passed functional testing and AOT publishing successfully, the latter debugging went fine, but Docker has not been successful yet.

Continue Reading