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.

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
- 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. - Serialization is now supported for
ReadOnlyMemory<T>andMemory<T>types. - When
Tisbyte, 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:

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:

Call API to Directly Get an Object
Now suppose an interface returns data as shown in the following image:

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:

Random Number Enhancements
- In version 8, the
Randomclass provides aGetItems()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();
- The
Shuffle()method provided byRandomcan 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
- Added
FrozenDictionary<TKey,TValue>andFrozenSet, which are in theSystem.Collections.Frozennamespace. 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:

- The new
System.Buffers.SearchValuesclass can be used for string searching and matching. Compared tostringoperations, 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:

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";
}