9月14日,.NET5发布了(Release Candidate)RC1版本,RC的意思是指我们可以进行使用,并且RC版本得到了支持,该版本很接近.NET5.0的最终版本,也是11月正式版本之前两个RC版本中的其中一个。目前,开发团队正在寻找在.NET5发布之前剩余的bug,当然他们也希望我们的反馈以帮助他们顺利的完成.NET5的开发计划。

开发团队在今天还发布了ASP.NET CoreEF Core的RC1版本。

现在我们可以进行下载用于Windows、macOS和Linux的.NET5

如果要使用.NET5,我们需要使用最新的Visual Studio预览版(包括Visual Studio for Mac)

在.NET5中有许多的改进,特别是对单文件可执行应用程序、更小的容器映像、更强大的JsonSerializer api、BCL nullable reference type annotated、新target framework names,以及对Windows ARM64的支持。在.NET库中,GC和JIT的性能都得到了极大的提升,ARM64是性能优化的重点,它为我们带来了更好的吞吐量和更小的二进制文件。.NET5.0包含了新的语言版本,C#9F#5.0

下面还有他们最近发布的一些有关于.NET5.0新功能的文章,大家可以阅读一下:

其实就像在.NET5 Preview8中一样,在本章还是像上一章一样选择了一些特性来进行深入的研究介绍,在本章中将深入的讨论C#9中新特性records System.Text.Json.JsonSerializer,它们是独立的特性,但也是很好的一个组合,特别是在我们花费一些时间去为反序列化的JSON对象设计POCO类型时。

C# 9 — Records

Records可能是c#9中最重要的一个新特性,它们提供了一个广泛的特性集(对于一种语言类型),其中一些需要RC1或更高的版本(如record.ToString())。

records看作不可变类是最简单的方式,在特性方面,它们很接近元组(Tuple),可以将他们视为具有属性和不可变性的自定义元组。在今天使用元组的许多情况下,records可以更好的提供这些元组。

如果你正在使用C#,你会得到最好的体验,如果你使用命名类型(相对于像元组这样的特性)。静态类型是该语言主要的设计要点,records使小型类型更容易使用,并在整个应用程序中利用类型安全。

Records are immutable data types

Records使我们能够创建不可变的数据类型,这对于定义存储少量数据的类型非常有用。

下面是一个records的示例,它存储登录用户信息.

public record LoginResource(string Username, string Password, bool RememberMe);

在语义中与下面的几乎完全相同,当然下面将会很快的去介绍这些的差异性。

public class LoginResource
{
    public LoginResource(string username, string password, bool rememberMe)
    {
        Username = username;
        Password = password;
        RememberMe = rememberMe;
    }

    public string Username { get; init; }
    public string Password { get; init; }
    public bool RememberMe { get; init; }
}

init是一个新的关键字,它是set的代替,set允许我们在任何时候分配一个属性,init只允许在对象构建期间进行属性的赋值操作,它是records的不变性所依赖的基础,任何类型都可以使用init。正如我们在前面的定义中所看到的那样,它不是特定于records的。

private set看起来类似于init;private set防止其他代码(类型以外的代码)改变数据,当类型(在构建之后)意外的改变属性时,init将在编译器生成时返回错误。private set并非旨在为不可变数据建模,因此当类型在构造后使属性值发生冲突时,private set不会产生任何编辑器错误或者警告。

Records are specialized classes

正如上面提到的LoginResource的records的变量和类变量几乎是相同的,类定义是记录的一个语义相同的子集,records 提供了更多的、专门的行为。

下面是比较一个record和一个使用init而不是set作为属性类之间的比较。

有什么相同?

  • Construction
  • Immutability
  • Copy semantics (records are classes under the hood)

有什么不同?

  • records相等性是基于内容的。基于对象标识的类相等性
  • records提供了一个GetHashCode()实现,它基于record内容
  • records提供一个IEquatable实现。它使用唯一的GetHashCode()行为作为机制,为record提供基于内容的相等语义。
  • 覆盖Record ToString()以打印record内容。

record和类(使用init)之间的差异可以在LoginResource作为记录和LoginResource作为类的反汇编中看到。

下面代码片段中将演示这些差异

using System;
using System.Linq;
using static System.Console;

var user = "Lion-O";
var password = "jaga";
var rememberMe = true;
LoginResourceRecord lrr1 = new(user, password, rememberMe);
var lrr2 = new LoginResourceRecord(user, password, rememberMe);
var lrc1 = new LoginResourceClass(user, password, rememberMe);
var lrc2 = new LoginResourceClass(user, password, rememberMe);

WriteLine($"Test record equality -- lrr1 == lrr2 : {lrr1 == lrr2}");
WriteLine($"Test class equality  -- lrc1 == lrc2 : {lrc1 == lrc2}");
WriteLine($"Print lrr1 hash code -- lrr1.GetHashCode(): {lrr1.GetHashCode()}");
WriteLine($"Print lrr2 hash code -- lrr2.GetHashCode(): {lrr2.GetHashCode()}");
WriteLine($"Print lrc1 hash code -- lrc1.GetHashCode(): {lrc1.GetHashCode()}");
WriteLine($"Print lrc2 hash code -- lrc2.GetHashCode(): {lrc2.GetHashCode()}");
WriteLine($"{nameof(LoginResourceRecord)} implements IEquatable<T>: {lrr1 is IEquatable<LoginResourceRecord>} ");
WriteLine($"{nameof(LoginResourceClass)}  implements IEquatable<T>: {lrr1 is IEquatable<LoginResourceClass>}");
WriteLine($"Print {nameof(LoginResourceRecord)}.ToString -- lrr1.ToString(): {lrr1.ToString()}");
WriteLine($"Print {nameof(LoginResourceClass)}.ToString  -- lrc1.ToString(): {lrc1.ToString()}");

public record LoginResourceRecord(string Username, string Password, bool RememberMe);

public class LoginResourceClass
{
    public LoginResourceClass(string username, string password, bool rememberMe)
    {
        Username = username;
        Password = password;
        RememberMe = rememberMe;
    }

    public string Username { get; init; }
    public string Password { get; init; }
    public bool RememberMe { get; init; }
}

注意:我们会注意到LoginResource类型以Record和Class结束。该模式并不是新的命名模式的规范,这样命名只是为了我们在代码片段中有相同类型的record和类变量。请不要这样命名我们的类型。

如下是上面代码的输出内容

rich@thundera records % dotnet run
Test record equality -- lrr1 == lrr2 : True
Test class equality  -- lrc1 == lrc2 : False
Print lrr1 hash code -- lrr1.GetHashCode(): -542976961
Print lrr2 hash code -- lrr2.GetHashCode(): -542976961
Print lrc1 hash code -- lrc1.GetHashCode(): 54267293
Print lrc2 hash code -- lrc2.GetHashCode(): 18643596
LoginResourceRecord implements IEquatable<T>: True
LoginResourceClass  implements IEquatable<T>: False
Print LoginResourceRecord.ToString -- lrr1.ToString(): LoginResourceRecord { Username = Lion-O, Password = jaga, RememberMe = True }
Print LoginResourceClass.ToString -- lrc1.ToString(): LoginResourceClass

Record syntax

有多种用于声明records的用例,在使用过每种方式后,我们就会对每一种模式的好处有所了解,我们还能看到不同方式,他们不是不同的语法而是多种选择。

第一个方式是最简单的,但是它的灵活性比较小,它适用于具有少量必需属性的records

下面是前面显示的LoginResource record,作为此模式的一个示例。这一行是的定义

public record LoginResource(string Username, string Password, bool RememberMe);

构造遵循具有参数的构造函数的要求(包括允许使用可选参数)。

var login = new LoginResource("Lion-O", "jaga", true);

还可以使用目标类型。

LoginResource login = new("Lion-O", "jaga", true);

下一个语法使所有属性都是可选的。为record提供了一个隐式无参数构造函数。

public record LoginResource
{
    public string Username {get; init;}
    public string Password {get; init;}
    public bool RememberMe {get; init;}
}

构造使用对象初始化器,看起来像下面这样

LoginResource login = new() 
{
    Username = "Lion-O", 
    TemperatureC = "jaga"
};

如果我们想让这两个属性是必须的,另一个是可选属性,那么我们可以通过如下方式实现

public record LoginResource(string Username, string Password)
{
    public bool RememberMe {get; init;}
}

构造可能如下所示,其中未指定RememberMe

LoginResource login = new("Lion-O", "jaga");

如果说要指定RememberMe可以通过如下方式来实现

LoginResource login = new("Lion-O", "jaga")
{
    RememberMe = true
};

如果说我们不认为record只用于不可变数据,那么我们可以选择公开可变属性,如下代码片段所示,该片段展示了关于电池的信息。Model和TotalCapacityAmpHours属性是不可变的,而剩余的容量百分比是可变的。

using System;

Battery battery = new Battery("CR2032", 0.235)
{
    RemainingCapacityPercentage = 100
};

Console.WriteLine (battery);

for (int i = battery.RemainingCapacityPercentage; i >= 0; i--)
{
    battery.RemainingCapacityPercentage = i;
}

Console.WriteLine (battery);

public record Battery(string Model, double TotalCapacityAmpHours)
{
    public int RemainingCapacityPercentage {get;set;}
}

输出结果如下所示:

rich@thundera recordmutable % dotnet run
Battery { Model = CR2032, TotalCapacityAmpHours = 0.235, RemainingCapacityPercentage = 100 }
Battery { Model = CR2032, TotalCapacityAmpHours = 0.235, RemainingCapacityPercentage = 0 }

Non-destructive record mutation

不变性是给我们带来了很多的好处,但是我们也很快的发现了需要修改record的情况,在不放弃record的情况下,我们该如何处理这种情况呢?with表达式可以满足这些需求,它可以根据相同类型的现有record来创建新record,我们可以指定想要的不同的新值,并从现有的record中复制所有其他属性.

现在我们有个需求就是将用户名转换为小写,这样的情况下我们才可以将其保存到我们的数据库中,如果说处理这个需求我们可能会像如下代码片段中这样去处理:

LoginResource login = new("Lion-O", "jaga", true);
LoginResource loginLowercased = lrr1 with {Username = login.Username.ToLowerInvariant()};

登录record没有被更改,事实上,这是不可能的,转换只影响了loginLowercased,除了小写转换为loginLowercased之外其他与登录相同。

我们可以使用内置的ToString()覆盖检查with是否完成了预期的工作。

Console.WriteLine(login);
Console.WriteLine(loginLowercased);

下面代码是输出

LoginResource { Username = Lion-O, Password = jaga, RememberMe = True }
LoginResource { Username = lion-o, Password = jaga, RememberMe = True }

我们可以进一步的了解with的工作原理,它将所有的值从一条record复制到另一条record。这不是一个record依赖于另一个record的委托模型。事实上with操作完成后,两个record之间就没有关系了,只对record的构建有意义,这就意味着对于引用类型,副本只是引用副本。对于值类型,复制值.

您可以使用以下代码查看该语义。

Console.WriteLine($"Record equality: {login == loginLowercased}");
Console.WriteLine($"Property equality: Username == {login.Username == loginLowercased.Username}; Password == {login.Password == loginLowercased.Password}; RememberMe == {login.RememberMe == loginLowercased.RememberMe}");

输出:

Record equality: False
Property equality: Username == False; Password == True; RememberMe == True

Record inheritance

扩展record很容易,假设一个新的LastLoggedIn属性,可以将其直接添加到LoginResource,record不像传统的接口那样脆弱,除非我们想创建需要构造函数参数的新属性.

这个新的record可以基于如下的LoginResource

public record LoginResource(string Username, string Password)
{
    public bool RememberMe {get; init;}
}

新的record可能就是如下这样

public record LoginWithUserDataResource(string Username, string Password, DateTime LastLoggedIn) : LoginResource(Username, Password)
{
    public int DiscountTier {get; init};
    public bool FreeShipping {get; init};
}

现在已经将LastLoggedIn设置为一个必须的属性,并且也增加了可选的属性

Modeling record construction helpers

我们一起来看另一个例子,测量体重,体重的测量来自一个互联网的秤,重量是以公斤来指定的,但是某些情况下,重点需要以磅来提供。

可以通过如下代码片段进行声明

public record WeightMeasurement(DateTime Date, int Kilograms)
{
    public int Pounds {get; init;}

    public static int GetPounds(int kilograms) => kilograms * 2.20462262;
}

这就是构造的样子

var weight = 200;
WeightMeasurement measurement = new(DateTime.Now, weight)
{
    Pounds = WeightMeasurement.GetPounds(weight)
};

在本例中,有必要将权重指定为local。不可能在对象初始化器中访问公斤属性。还需要将GetPounds定义为静态方法。不可能在对象初始化器中调用实例方法(对于正在构造的类型)。

Records and Nullability

一切都是不可变的,那么空值从何而来?不完全是。不可变属性可以是null,并且在这种情况下将始终是null。

让我们看看另一个没有启用可空性的程序。

using System;
using System.Collections.Generic;

Author author = new(null, null);

Console.WriteLine(author.Name.ToString());

public record Author(string Name, List<Book> Books)
{
    public string Website {get; init;}
    public string Genre {get; init;}
    public List<Author> RelatedAuthors {get; init;}
}

public record Book(string name, int Published, Author author);

这个程序将编译并抛出一个NullReference异常,这是由于取消引用author.Name为空。

为了进一步说明这一点,将不编译以下内容。author.Name 初始化为null,然后不能更改,因为属性是不可变的。

Author author = new(null, null);
author.Name = "Colin Meloy";

下面启动可空性

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net5.0</TargetFramework>
    <LangVersion>preview</LangVersion>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>

下面我们能看到一堆这样的警告

/Users/rich/recordsnullability/Program.cs(8,21): warning CS8618: Non-nullable property 'Website' must contain a non-null value when exiting constructor. Consider declaring the property as nullable. [/Users/rich/recordsnullability/recordsnullability.csproj]

用null注释更新了Author record,这些注释描述了我打算使用的record。

public record Author(string Name, List<Book> Books)
{
    public string? Website {get; init;}
    public string? Genre {get; init;}
    public List<Author>? RelatedAuthors {get; init;}
}

仍然得到了对null的警告,null构造的Author之前看到。

/Users/rich/recordsnullability/Program.cs(5,21): warning CS8625: Cannot convert null literal to non-nullable reference type. [/Users/rich/recordsnullability/recordsnullability.csproj]

很好,因为我们想避免这种情况。 现在,下面展示该程序的更新版本,该版本可以很好地运行并享有可空性的好处。

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;


Author lord = new Author("Karen Lord")
{
    Website = "https://karenlord.wordpress.com/",
    RelatedAuthors = new()
};

lord.Books.AddRange(
    new Book[] 
    {
        new Book("The Best of All Possible Worlds", 2013, lord),
        new Book("The Galaxy Game", 2015, lord)
    }
);

lord.RelatedAuthors.AddRange(
    new Author[]
    {
        new ("Nalo Hopkinson"),
        new ("Ursula K. Le Guin"),
        new ("Orson Scott Card"),
        new ("Patrick Rothfuss")    
    }
);

Console.WriteLine($"Author: {lord.Name}");
Console.WriteLine($"Books: {lord.Books.Count}");
Console.WriteLine($"Related authors: {lord.RelatedAuthors.Count}");


public record Author(string Name)
{
    private List<Book> _books = new();

    public List<Book> Books => _books;

    public string? Website {get; init;}
    public string? Genre {get; init;}
    public List<Author>? RelatedAuthors {get; init;}
}

public record Book(string name, int Published, Author author);

该程序在编译时不会出现可空的警告。

大家可能对下面这句有疑惑

lord.RelatedAuthors.AddRange(

Author.RelatedAuthors可以为null。 编译器可以看到,RelatedAuthors属性的设置只是前面几行,因此它知道RelatedAuthors引用将为非null。

但是,想象一下这个程序看起来是这样的。

Author GetAuthor()
{
    return new Author("Karen Lord")
    {
        Website = "https://karenlord.wordpress.com/",
        RelatedAuthors = new()
    };
}

Author lord = GetAuthor();

编译器没有流程分析技巧,无法知道当类型构造在单独的方法中时,RelatedAuthor将为非空。 在这种情况下,将需要以下两种模式之一

lord.RelatedAuthors!.AddRange(

or

if (lord.RelatedAuthors is object)
{
    lord.RelatedAuthors.AddRange( ...
}

这是一个关于记录可空性的冗长演示,只是为了说明它不会改变使用可空引用类型的任何体验。

另外,您可能已经注意到,我将Author record上的Books属性移动为初始化的get-only属性,而不是记录构造函数中的必需参数。 这是由于作者与书籍之间存在循环关系。 不变性和循环引用可能会引起头痛。 在这种情况下可以,并且仅表示需要在Book对象之前创建所有Author对象。 结果,无法提供完全初始化的Book对象集作为Author结构的一部分。 作为Author结构的一部分,我们可以期望的最好的是一个空的List 。 结果,初始化空的List 作为Author结构的一部分似乎是最佳选择。 没有规则要求所有这些属性都必须是init样式。这样做只是为了演示该行为。

我们将过渡到谈论JSON序列化。 这个带有循环引用的示例与不久之后的在JSON对象图中保存引用有关。 JsonSerializer支持带有循环引用的对象图,但不支持带有参数化构造函数的类型。 您可以将Author对象序列化为JSON,但不能序列化为当前定义的Author对象。 如果Author不是记录或没有循环引用,那么JsonSerializer可以同时进行序列化和反序列化。

System.Text.Json

.NET 5.0中对System.Text.Json进行了显着改进,以提高性能,可靠性,当然如果熟悉Newtonsoft.Json那么用起来更容易, 它还包括对将JSON对象反序列化为记录的支持,本文前面已介绍了新的C#功能

如果要使用System.Text.Json替代Newtonsoft.Json,则应查看迁移指南。 该指南阐明了这两个API之间的关系。 System.Text.Json旨在涵盖与Newtonsoft.Json相同的许多场景,但并不旨在替代流行的JSON库或与流行的JSON库实现功能对等。 我们尝试在性能和可用性之间保持平衡,并在设计选择中偏向性能。

HttpClient extension methods

JsonSerializer扩展方法现在在HttpClient上公开,并且极大地简化了同时使用这两个api。这些扩展方法消除了复杂性,并为您处理各种场景,包括处理内容流和验证内容媒体类型。Steve Gordon很好地解释了使用带有System.Net.Http.Json的HttpClient发送和接收JSON的好处

下面的示例使用新的GetFromJsonAsync()扩展方法将天气预报JSON数据反序列化为预报记录。

using System;
using System.Net.Http;
using System.Net.Http.Json;

string serviceURL = "https://localhost:5001/WeatherForecast";
HttpClient client = new();
Forecast[] forecasts = await client.GetFromJsonAsync<Forecast[]>(serviceURL);

foreach(Forecast forecast in forecasts)
{
    Console.WriteLine($"{forecast.Date}; {forecast.TemperatureC}C; {forecast.Summary}");
}

// {"date":"2020-09-06T11:31:01.923395-07:00","temperatureC":-1,"temperatureF":31,"summary":"Scorching"}            
public record Forecast(DateTime Date, int TemperatureC, int TemperatureF, string Summary);

这段代码非常紧凑!它依赖于来自c#9的顶级程序和record,以及新的GetFromJsonAsync()扩展方法。在foreach和await的使用中可能大家会怀疑是否对流JSON对象的支持,在未来版本中是支持的。

大家可以在自己的机器上试试。下面的.NET SDK命令将使用WebAPI模板创建一个天气预报服务。默认情况下,它将在以下URL公开服务:https://localhost:5001/WeatherForecast。这与示例中使用的URL相同。

rich@thundera ~ % dotnet new webapi -o webapi
rich@thundera ~ % cd webapi 
rich@thundera webapi % dotnet run

确保已经运行dotnet dev-certs https——首先信任,否则客户端和服务器之间的握手将不起作用。如果有问题,请参见信任ASP.NET Core HTTPS开发证书

然后可以运行前面的示例。

rich@thundera ~ % git clone https://gist.github.com/3b41d7496f2d8533b2d88896bd31e764.git weather-forecast
rich@thundera ~ % cd weather-forecast
rich@thundera weather-forecast % dotnet run
9/9/2020 12:09:19 PM; 24C; Chilly
9/10/2020 12:09:19 PM; 54C; Mild
9/11/2020 12:09:19 PM; -2C; Hot
9/12/2020 12:09:19 PM; 24C; Cool
9/13/2020 12:09:19 PM; 45C; Balmy

Improved support for immutable types

其实定义不可变类型有多种方式,records只是最新的一种,JsonSerializer现在支持不可变类型

在下面示例中,我们将看到带有不可变结构的序列化

using System;
using System.Text.Json;
using System.Text.Json.Serialization;

var json = "{\"date\":\"2020-09-06T11:31:01.923395-07:00\",\"temperatureC\":-1,\"temperatureF\":31,\"summary\":\"Scorching\"} ";           
var options = new JsonSerializerOptions()
{
    PropertyNameCaseInsensitive = true,
    IncludeFields = true,
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
var forecast = JsonSerializer.Deserialize<Forecast>(json, options);

Console.WriteLine(forecast.Date);
Console.WriteLine(forecast.TemperatureC);
Console.WriteLine(forecast.TemperatureF);
Console.WriteLine(forecast.Summary);

var roundTrippedJson = JsonSerializer.Serialize<Forecast>(forecast, options);

Console.WriteLine(roundTrippedJson);

public struct Forecast{
    public DateTime Date {get;}
    public int TemperatureC {get;}
    public int TemperatureF {get;}
    public string Summary {get;}
    [JsonConstructor]
    public Forecast(DateTime date, int temperatureC, int temperatureF, string summary) => (Date, TemperatureC, TemperatureF, Summary) = (date, temperatureC, temperatureF, summary);
}

注意:JsonConstructor属性需要指定与struct一起使用的构造函数,对于类,如果只有一个构造函数,那么属性就不是必须的,与records相同。

输出内容:

rich@thundera jsonserializerimmutabletypes % dotnet run
9/6/2020 11:31:01 AM
-1
31
Scorching
{"date":"2020-09-06T11:31:01.923395-07:00","temperatureC":-1,"temperatureF":31,"summary":"Scorching"}

Support for records

JsonSerializer对records的支持与上面展示的不可变类型的支持几乎相同,我想在这里显示的区别是将JSON对象反序列化为一条records,该records公开了参数化的构造函数和可选的init属性。

在下面代码片段中包含了对records的定义:

using System;
using System.Text.Json;

Forecast forecast = new(DateTime.Now, 40)
{
    Summary = "Hot!"
};

string forecastJson = JsonSerializer.Serialize<Forecast>(forecast);
Console.WriteLine(forecastJson);
Forecast? forecastObj = JsonSerializer.Deserialize<Forecast>(forecastJson);
Console.Write(forecastObj);

public record Forecast (DateTime Date, int TemperatureC)
{
    public string? Summary {get; init;}
};

输出如下所示:

rich@thundera jsonserializerrecords % dotnet run
{"Date":"2020-09-12T18:24:47.053821-07:00","TemperatureC":40,"Summary":"Hot!"}
Forecast { Date = 9/12/2020 6:24:47 PM, TemperatureC = 40, Summary = Hot! }

Improved Dictionary<K,V> support

JsonSerializer现在支持具有非字符串键的字典。我们可以在下面的示例中看到它的样子。在.NET Core 3.0中,这段代码可以编译,但会抛出NotSupportedException异常。

using System;
using System.Collections.Generic;
using System.Text.Json;

Dictionary<int, string> numbers = new ()
{
    {0, "zero"},
    {1, "one"},
    {2, "two"},
    {3, "three"},
    {5, "five"},
    {8, "eight"},
    {13, "thirteen"},
    {21, "twenty one"},
    {34, "thirty four"},
    {55, "fifty five"},
};

var json = JsonSerializer.Serialize<Dictionary<int, string>>(numbers);

Console.WriteLine(json);

var dictionary = JsonSerializer.Deserialize<Dictionary<int, string>>(json);

Console.WriteLine(dictionary[55]);

输出内容:

rich@thundera jsondictionarykeys % dotnet run
{"0":"zero","1":"one","2":"two","3":"three","5":"five","8":"eight","13":"thirteen","21":"twenty one","34":"thirty four","55":"fifty five"}
fifty five

Support for fields

JsonSerializer现在支持字段。

我们可以在下面的示例中看到它的样子。在.NET Core 3.0中,JsonSerializer无法对使用字段的类型进行序列化或反序列化。对于具有字段且无法更改的现有类型来说,这是一个问题。有了这个支持,这不再是一个问题。

using System;
using System.Text.Json;

var json = "{\"date\":\"2020-09-06T11:31:01.923395-07:00\",\"temperatureC\":-1,\"temperatureF\":31,\"summary\":\"Scorching\"} ";           
var options = new JsonSerializerOptions()
{
    PropertyNameCaseInsensitive = true,
    IncludeFields = true,
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
var forecast = JsonSerializer.Deserialize<Forecast>(json, options);

Console.WriteLine(forecast.Date);
Console.WriteLine(forecast.TemperatureC);
Console.WriteLine(forecast.TemperatureF);
Console.WriteLine(forecast.Summary);

var roundTrippedJson = JsonSerializer.Serialize<Forecast>(forecast, options);

Console.WriteLine(roundTrippedJson);

public class Forecast{
    public DateTime Date;
    public int TemperatureC;
    public int TemperatureF;
    public string Summary;
}

输出内容:

rich@thundera jsonserializerfields % dotnet run
9/6/2020 11:31:01 AM
-1
31
Scorching
{"date":"2020-09-06T11:31:01.923395-07:00","temperatureC":-1,"temperatureF":31,"summary":"Scorching"}

Preserving references in JSON object graphs

JsonSerializer增加了对在JSON对象图中保存(循环)引用的支持。它通过存储在将JSON字符串反序列化回对象时可以重新构建的id来实现这一点。

using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;

Employee janeEmployee = new()
{
    Name = "Jane Doe",
    YearsEmployed = 10
};

Employee johnEmployee = new()
{
    Name = "John Smith"
};

janeEmployee.Reports = new List<Employee> { johnEmployee };
johnEmployee.Manager = janeEmployee;

JsonSerializerOptions options = new()
{
    // NEW: globally ignore default values when writing null or default
    DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault,
    // NEW: globally allow reading and writing numbers as JSON strings
    NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString,
    // NEW: globally support preserving object references when (de)serializing
    ReferenceHandler = ReferenceHandler.Preserve,
    IncludeFields = true, // NEW: globally include fields for (de)serialization
    WriteIndented = true,};

string serialized = JsonSerializer.Serialize(janeEmployee, options);
Console.WriteLine($"Jane serialized: {serialized}");

Employee janeDeserialized = JsonSerializer.Deserialize<Employee>(serialized, options);
Console.Write("Whether Jane's first report's manager is Jane: ");
Console.WriteLine(janeDeserialized.Reports[0].Manager == janeDeserialized);

public class Employee
{
    // NEW: Allows use of non-public property accessor.
    // Can also be used to include fields "per-field", rather than globally with JsonSerializerOptions.
    [JsonInclude]
    public string Name { get; internal set; }

    public Employee Manager { get; set; }

    public List<Employee> Reports;

    public int YearsEmployed { get; set; }

    // NEW: Always include when (de)serializing regardless of global options
    [JsonIgnore(Condition = JsonIgnoreCondition.Never)]
    public bool IsManager => Reports?.Count > 0;
}

Performance

在.NET 5.0中,JsonSerializer的性能得到了显着改善。 Stephen Toub在.NET 5中的Performance Improvements中涵盖了JsonSerializer的一些改进。 我会在这里再介绍几个。

Collections (de)serialization

本次对大型集合做了显著的改进(反序列化时为1.15x-1.5x,序列化时为1.5x-2.4x+)。我们可以在dotnet/runtime #2259中更详细地看到这些改进。

将.NET 5.0与.NET Core 3.1进行比较,对List(反序列化)的改进特别令人印象深刻。 这些变化将在高性能应用程序中非常有意义。

Method Mean Error StdDev Median Min Max Gen 0 Gen 1 Gen 2 Allocated
Deserialize before 76.40 us 0.392 us 0.366 us 76.37 us 75.53 us 76.87 us 1.2169 8.25 KB
After ~1.5x faster 50.05 us 0.251 us 0.235 us 49.94 us 49.76 us 50.43 us 1.3922 8.62 KB
Serialize before 29.04 us 0.213 us 0.189 us 29.00 us 28.70 us 29.34 us 1.2620 8.07 KB
After ~2.4x faster 12.17 us 0.205 us 0.191 us 12.15 us 11.97 us 12.55 us 1.3187 8.34 KB

Property lookups — naming convention

使用JSON最常见的问题之一是命名规范与.NET设计准则不匹配。JSON属性通常是camelCase, .NET属性和字段通常是PascalCase。我们使用的json序列化器负责在命名约定之间架桥。这不是免费的,至少对.NET Core 3.1来说不是。在.NET5中,这种成本现在可以忽略不计了。

.NET 5.0中大大改进了允许缺少属性和不区分大小写的代码。 在某些情况下,速度快约1.75倍

下面是一个简单的4个属性测试类的基准测试,它的属性名为>7 bytes。

3.1 performance
|                            Method |       Mean |   Error |  StdDev |     Median |        Min |        Max |  Gen 0 | Gen 1 | Gen 2 | Allocated |
|---------------------------------- |-----------:|--------:|--------:|-----------:|-----------:|-----------:|-------:|------:|------:|----------:|
| CaseSensitive_Matching            |   844.2 ns | 4.25 ns | 3.55 ns |   844.2 ns |   838.6 ns |   850.6 ns | 0.0342 |     - |     - |     224 B |
| CaseInsensitive_Matching          |   833.3 ns | 3.84 ns | 3.40 ns |   832.6 ns |   829.4 ns |   841.1 ns | 0.0504 |     - |     - |     328 B |
| CaseSensitive_NotMatching(Missing)| 1,007.7 ns | 9.40 ns | 8.79 ns | 1,005.1 ns |   997.3 ns | 1,023.3 ns | 0.0722 |     - |     - |     464 B |
| CaseInsensitive_NotMatching       | 1,405.6 ns | 8.35 ns | 7.40 ns | 1,405.1 ns | 1,397.1 ns | 1,423.6 ns | 0.0626 |     - |     - |     408 B |

5.0 performance
|                            Method |     Mean |   Error |  StdDev |   Median |      Min |      Max |  Gen 0 | Gen 1 | Gen 2 | Allocated |
|---------------------------------- |---------:|--------:|--------:|---------:|---------:|---------:|-------:|------:|------:|----------:|
| CaseSensitive_Matching            | 799.2 ns | 4.59 ns | 4.29 ns | 801.0 ns | 790.5 ns | 803.9 ns | 0.0985 |     - |     - |     632 B |
| CaseInsensitive_Matching          | 789.2 ns | 6.62 ns | 5.53 ns | 790.3 ns | 776.0 ns | 794.4 ns | 0.1004 |     - |     - |     632 B |
| CaseSensitive_NotMatching(Missing)| 479.9 ns | 0.75 ns | 0.59 ns | 479.8 ns | 479.1 ns | 481.0 ns | 0.0059 |     - |     - |      40 B |
| CaseInsensitive_NotMatching       | 783.5 ns | 3.26 ns | 2.89 ns | 783.5 ns | 779.0 ns | 789.2 ns | 0.1004 |     - |     - |     632 B |

TechEmpower improvement

开发团队在TechEmpower基准测试中花费了大量的精力来改进.NET的性能。使用TechEmpower JSON基准来验证这些JsonSerializer改进是很有意义的。现在性能提高了~ 19%,一旦我们将条目更新到.NET5,这将提高.NET5在基准测试中的位置。这个版本的目标是与netty相比更具竞争力,netty是一种常见的Java web服务器。

dotnet/runtime #37976中详细介绍了这些更改和性能度量。这里有两套基准。第一个是使用团队维护的JsonSerializer性能基准测试来验证性能。观察到有~8%的改善。下一部分是关于技术授权的。它测量了满足TechEmpower JSON基准测试要求的三种不同方法。SerializeWithCachedBufferAndWriter是我们在官方基准测试中使用的

Method Mean Error StdDev Median Min Max Gen 0 Gen 1 Gen 2 Allocated
SerializeWithCachedBufferAndWriter (before) 155.3 ns 1.19 ns 1.11 ns 155.5 ns 153.3 ns 157.3 ns 0.0038 24 B
SerializeWithCachedBufferAndWriter (after) 130.8 ns 1.50 ns 1.40 ns 130.9 ns 128.6 ns 133.0 ns 0.0037 24 B

如果我们看一下Min列,我们可以做一些简单的数学计算:153.3/128.6 = ~1.19。提高了19%。

Closing

本文对records和JsonSerializer有了一个更好的认识。它们只是.NET 5.0众多改进中的两个。preivew 8的文章涵盖了更大的特性集,这为5.0的价值提供了更广阔的视角。

正如我们所知道的,他们现在没有在.NET 5.0中添加任何新特性。这些后期的预览和RC的文章来涵盖开发团队已经建立的所有功能。当然大家可以在原文中进行留言,说一下在期望RC2中开发团队这边需要详细介绍的特性。

原文:https://devblogs.microsoft.com/dotnet/announcing-net-5-0-rc-1/