Although 8 has brought many enhancements, such as artificial intelligence, cloud native, performance, native AOT, etc., I am still most concerned about the C#language and some framework-level changes. Here are some new features in C#12 and the framework that I think are more practical.

At the. NET Conf 2023 conference,. NET 8 was officially released. NET 8 is a long-term support (LTS) release, which means you can get three years of support and patches. We also plan to upgrade the framework from. NET Core 3.1 to 8. We will share information on how to upgrade when the upgrade is complete.
To use. NET 8, you need to install the relevant SDK, which can be downloaded at this address: https://dotnet.microsoft.com/en-cn/download/dotnet/8.0, or upgrade VS2022 to 17.8.
Although 8 has brought many enhancements, such as artificial intelligence, cloud native, performance, native AOT, etc., I am still most concerned about the C#language and some framework-level changes. The following describes the new features I think are more practical in C#12 and the framework. You can see the official document for all update instructions: https://www.example.com. learn.microsoft.com/zh-cn/dotnet/core/whats-new/dotnet-8
Sequential enhancement
Other types of built-in support
- Additional types: Half, Int128, and UInt128 can be serialized. There will be no errors when serializing these types in. NET 7, but the content cannot be obtained normally.
- ReadOnlyMemory and Memory types can be serialized.
- When the type of T is byte, the serialization result is base64, otherwise it is a json array.
using System.Text.Json;
//输出:[65500,170141183460469231731687303715884105727,340282366920938463463374607431768211455]
Console.WriteLine(JsonSerializer.Serialize(new object[] { Half.MaxValue, Int128.MaxValue, UInt128.MaxValue }));
//输出:"AQIDBAUG"
Console.WriteLine(JsonSerializer.Serialize<ReadOnlyMemory<byte>>(new byte[] { 1,2,3,4,5,6}));
//输出:[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));
//输出:{"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; }
}
After the IDerived interface inherits the IBase interface in the above code, it has two attributes.
In previous versions (3.1, 6, and 7), the interface IDeried containing two attributes was used to receive instantiation of objects and then serialize them. The only result was: {Derived":1}, and the inherited attribute Base could not be recognized.
Improvements have been made in 8 and the expected results can be achieved. It is worth noting that if workarounds have been used before, targeted testing and adjustments will be needed after the upgrade.
named policy
The following figure shows the support for naming strategies during serialization in 8:

In previous versions: 3.1, 6, and 7, only CamelCase was supported. The new strategies in 8 are as follows:
- KebabCaseLower: Lower case middle line, for example: user-name.
- KebabCaseUpper: Capitalized middle line, for example: USER-NAME.
- SnakeCaseLower: Lower case underscore, for example: user_name.
- SnakeCaseUpper: Underword in capital, for example: 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 the API to directly obtain the object
Now there is an interface that returns data as shown in the following figure:

If you obtain the data of this interface in versions before 8, you need to first obtain the interface content and then deserialize it. The code is as follows:
const string RequestUri = "http://localhost:5145/user";
using var client = new HttpClient();
var stream =await client.GetStreamAsync(RequestUri);
//反序列化
var users = JsonSerializer.DeserializeAsyncEnumerable<UserInfo>(stream);
await foreach(UserInfo user in users)
{
Console.WriteLine($"姓名:{user.userName}");
}
Console.ReadKey();
public record UserInfo(string userName);
In version 8, you can directly call the GetFromJsonAsAsyncEnumerable method to get the object directly without deserialization:
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($"姓名: {user.userName}");
}
Console.ReadKey();
public record UserInfo(string userName);
The results of the above two codes are the same, as shown in the following figure:

random number enhancement
- In 8, the GetItems() method is provided for the random number class Random, which can randomly extract data items from a provided set based on the specified number to generate a new set:
ReadOnlySpan<string> colors = new[]{"Red","Green","Blue","Black"};
string[] t1 = Random.Shared.GetItems(colors, 10);
Console.WriteLine(JsonSerializer.Serialize(t1));
//输出:["Black","Green","Blue","Blue","Green","Blue","Green","Black","Green","Blue"]
//每次都会不一样
Console.ReadKey();
- Using the Shuffle() method provided by Random, you can order the data items in a collection:
string[] colors = new[]{"Red","Green","Blue","Black"};
Random.Shared.Shuffle(colors);
Console.WriteLine(JsonSerializer.Serialize(colors));
Console.ReadKey();
New types of performance improvements
- 新增了
FrozenDictionary<TKey,TValue>和FrozenSet,这两个类型在System.Collections.Frozen命名空间下,创建这两种类型的集合后,就不允许对键和值进行任何更改,因此可以实现更快的读取操作。
Here is the code to test FrozenDictionary and Dictionary using BenchmarkDotNet:
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 _);
}
}
}
Judging from the test results, the effect is still very obvious:

- 新增的
System.Buffers.SearchValues类,可以用来进行字符串的查找和匹配,相比较 string 类型的操作,性能有大幅提升,下面还是用 BenchmarkDotNet 进行测试:
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);
}
}
}
Judging from the operation results, there is an improvement of about 5 times:

Dependency injection enhancement
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 there are two implementations of the IUser interface, the writing in the above code can only obtain an instance of the last registered class. To inject multiple implementation classes into one interface, some additional code needs to be written, which is quite cumbersome.
The injection keyword was added in version 8, which can be easily implemented. See the following code:
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";
}