官方盘点 .NET 7 新功能

2022-12-07 21:03:34 浏览数 (1)

(本文阅读时间:20分钟)

本文翻译于 Jeremy Likness, Angelos Petropoulos 和 Jon Douglas 的博客

.NET 7 为C# 11/F# 7、.NET MAUI、ASP.NET Core/Blazor、Web API、WinForms、WPF 等应用程序带来了更高的性能和新功能。使用 .NET 7,您还可以轻松地将 .NET 7 项目容器化,在 GitHub 操作中设置 CI/CD 工作流,并实现云原生可观察性。欢迎下载 .NET 7!

  • 欢迎下载 .NET 7 https://dotnet.microsoft.com/download/dotnet/7.0
  • 更高的性能: https://devblogs.microsoft.com/dotnet/performance_improvements_in_net_7/?ocid=AID3052907

.NET 7 中的新功能

在这篇博文中,我们将重点介绍 .NET 团队专注于交付的主要主题:

  • 统一:一个 BCL、新的 TFM、对 ARM64 的本机支持、Linux 上增强的 .NET 支持
  • 现代:持续的性能改进、开发人员生产力增强,例如容器优先的工作流程、从相同的代码库构建跨平台的移动和桌面应用程序
  • .NET 适用于云原生应用:易于构建和部署分布式云原生应用
  • 简单:使用 C# 11 简化和编写更少的代码、针对云原生应用程序的 HTTP/3 和最小 API 改进
  • 性能:多项性能改进

下面,我们将更详细地介绍这些主题,并分享更多关于这项工作为何如此重要的背景信息。

场景

.NET 7 用途广泛,您可以在任何平台上构建任何应用程序。

让我们重点介绍从今天开始可以使用 .NET 实现的一些场景:

  • 从在浏览器中运行的 React 代码调用现有.NET库,通过包含经过优化以在 WebAssembly 上运行的流线型 .NET运行时。
  • 使用强类型 C#访问存储在 SQL Server 数据库中的 JSON 文档的内容。
  • 只需编写几行代码,即可快速构建和部署使用 OpenAPI 自动记录的安全 REST 端点。
  • 使用 Ahead of Time (AOT) 编译从 C# 源代码生成简化的本机应用程序,并直接发布到容器映像。
  • 运行一个 .NET Core 应用程序,该应用程序使用内置 API 将内容压缩并存档到 Linux 友好的文件gz中。
  • 使用为每个目标平台创建本机代码和组件的单一代码库和设计,实现您对 Android、iOS 和 Windows 上的移动应用程序的愿景。
  • 通过使用升级助手自动迁移旧版应用程序并在 CoreWCF 的帮助下现代化您的 Windows Communication Foundation (WCF) Web 服务,获得.NET 7 的性能优势。
  • 使用反映您的架构和设计选择的样板模板,让开发人员比以往任何时候都更容易启动新应用程序。
  • 使用ReadKey 在 Unix/Linux 中更好地处理组合键和修饰键。

统一

▌一个基类库 (BCL)

.NET 7 版本是我们 .NET 统一之旅中的第三个主要版本(自 2016 年 .NET 5 以来)。

使用 .NET 7,您只需学习一次,就可以通过一个 SDK、一个运行时、一组基础库重复使用您的技能来构建多种类型的应用程序(云、Web、桌面、移动、游戏、IoT 和 AI)。

▌面向 .NET 7

当您以应用程序或库中的框架为目标时,您正在指定要提供的 API 集。要以 .NET 7 为目标,只需更改项目中的目标框架即可。

代码语言:javascript复制
<TargetFramework>net7.0</TargetFramework>

针对 net7.0 Target Framework Moniker (TFM) 的应用程序将在所有受支持的操作系统和 CPU 架构上运行。它们使您可以访问 .NET 7 中的所有 API 以及一堆特定于操作系统的 API,例如:

  • net7.0-android
  • net7.0-ios
  • net7.0-maccatalyst
  • net7.0-macos
  • net7.0-tvos
  • net7.0-windows

通过 net7.0 TFM 公开的 API 旨在随时随地工作。如果您怀疑 .NET 7 是否支持 API,您可以随时查看。这是一个新添加的接口 IJsonTypeInfoResolver 的示例,您可以看到它现在已内置到 .NET 7 中:

  • 随时查看 https://apisof.net/

▌ARM64

随着行业向 ARM 发展,.NET 也是如此。ARM CPU 的最大优势之一是电源效率。这以最低的功耗带来最高的性能。换句话说,您可以事半功倍。在 .NET 5 中,我们描述了我们针对 ARM64 所做的性能计划。现在,在两个版本之后,我们想与您分享我们已经走了多远。我们的持续目标是将 x64 的性能与 ARM64 相匹配,以帮助我们的客户将他们的 .NET 应用程序迁移到 ARM。

▌运行时改进

我们在调查 x64 和 ARM64 时遇到的一个挑战是发现无法从 ARM64 机器正确读取 L3 缓存大小。当无法从操作系统或机器的 BIOS 中获取 L3 缓存大小,我们通过更改启发式方法以返回近似大小。现在我们可以更好地估计每个 L3 缓存大小的内核数。

Core count

L3 cache size

1~4

4MB

5~16

8MB

17~64

16MB

65

32MB

接下来是我们对 LSE 原子的理解。如果您不熟悉,它提供了原子 API 来获得对关键区域的独占访问权限。在 CISC 架构 x86-x64 机器中,内存上的读-修改-写 (RMW) 操作可以通过添加锁定前缀的单个指令执行。

但是在 RISC 架构的机器上,RMW 操作是不允许的,所有的操作都是通过寄存器来完成的。因此,对于并发场景,它们有一对指令。“Load Acquire” (ldaxr) 获得对内存区域的独占访问权限,因此其他内核无法访问它,而“Store Release” (stlxr) 则释放对其他内核的访问权限。在这些对之间,执行关键操作。如果在使用 ldaxr 加载内容后,由于其他 CPU 在内存上操作而导致 stlxr 操作失败,则可以通过这个代码重试(cbnz jumps back to retry)该操作。

ARM 在 v8.1 中引入了 LSE 原子指令。有了这些指令,这些操作可以比传统版本用更少的代码和更快的速度完成。当我们为 Linux 启用此功能并随后将其扩展到 Windows 时,我们看到了大约 45% 的性能提升。

▌库改进

为了优化使用内在函数的库,我们添加了新的跨平台助手。其中包括Vector64、Vector128和Vector256 的助手。跨平台助手允许通过用与硬件无关的内在函数替换特定于硬件的内在函数来统一矢量化算法。这将使任何平台上的用户受益,但我们预计 ARM64 将受益最大,因为没有 ARM64 专业知识的开发人员仍然能够使用帮助程序来利用 Arm64 硬件内在函数。

将诸如 EncodeToUtf8 和 DecodeFromUtf8 等 API 从 SSE3 实现重写为基于 Vector 的 API 可以提供高达 60% 的改进。

转换 NarrowUtf16ToAscii() 和 GetIndexOfFirstNonAsciiChar() 等其他 API 可以实现高达 35% 的性能提升。

▌性能影响

通过我们在 .NET 7 中的工作,许多 MicroBenchmark 提高了 10-60%。当我们开始 .NET 7 时,ARM64 的每秒请求数 (RPS) 较低,但慢慢克服了 x64 的奇偶校验。

同样对于延迟(以毫秒为单位),我们将桥接 x64 的奇偶校验。

有关更多详细信息,请查看 .NET 7 中的 ARM64 性能改进。

  • .NET 7 中的 ARM64 性能改进 https://devblogs.microsoft.com/dotnet/arm64-performance-improvements-in-dotnet-7/?ocid=AID3052907

▌Linux 上增强的 .NET 支持

.NET 6 包含在 Ubuntu 22.04 (Jammy) 中,可以使用 apt install dotnet6 命令安装。此外,还有一个优化的、预构建的、开箱即用的超小型容器镜像。

代码语言:javascript复制
dotnetapp % docker run --rm dotnetapp-chiseled
         42
         42              ,d                             ,d
         42              42                             42
 ,adPPYb,42  ,adPPYba, MM42MMM 8b,dPPYba,   ,adPPYba, MM42MMM
a8"    `Y42 a8"     "8a  42    42P'   `"8a a8P_____42   42
8b       42 8b       d8  42    42       42 8PP"""""""   42
"8a,   ,d42 "8a,   ,a8"  42,   42       42 "8b,   ,aa   42,
 `"8bbdP"Y8  `"YbbdP"'   "Y428 42       42  `"Ybbd8"'   "Y428

.NET 7.0.0-preview.7.22375.6
Linux 5.10.104-linuxkit #1 SMP PREEMPT Thu Mar 17 17:05:54 UTC 2022

OSArchitecture: Arm64
ProcessorCount: 4
TotalAvailableMemoryBytes: 3.83 GiB

▌64 位 IBM Power 支持

除了 x64 架构(64 位 Intel/AMD)、ARM64(64 位 ARM)和 s390x(64 位 IBM Z)之外,.NET 现在也可用于针对 RHEL 的 ppc64le(64 位 IBM Power)架构8.7 和 RHEL 9.1。

超过 25,000 名 IBM Power 客户可以在 Windows x86 上整合现有的 .NET 应用程序,以便在与其 IBM i 和 AIX 业务应用程序和数据库相同的 Power 平台上运行。这样做极大地提高了可持续性,将碳足迹减少了多达 5 倍,并结合了 RHEL 和 OpenShift 容量的本地即用即付扩展,同时提供行业领先的端到端企业事务和数据安全性。

现代

.NET 7 专为现代云原生应用程序、移动客户端、边缘服务和桌面技术而构建。使用 .NET MAUI 在不影响本机性能的情况下,使用单个代码库创建移动体验。使用 C# 和 Razor 模板等熟悉的技术构建响应式单页应用程序 (SPA),这些应用程序在浏览器中运行并作为渐进式 Web 应用程序 (PWA) 脱机运行。这些更快、更现代的体验不仅仅适用于新应用。.NET 升级助手将提供有关兼容性的反馈,并在某些情况下将您的应用程序完全迁移到 .NET 6 和 .NET 7。

▌.NET MAUI

NET MAUI 现在是 .NET 7 的一部分,具有大量改进和新功能。通过阅读最新的.NET MAUI 博客公告,您可以了解 .NET MAUI 以及它使您能够为所有移动设备构建应用程序的方式。

  • 最新的 .NET MAUI 博客公告 https://devblogs.microsoft.com/dotnet/dotnet-maui-dotnet-7?ocid=AID3052907

▌Blazor

Blazor 不断发展,.NET 7 包括许多重大改进。Blazor 现在支持处理位置更改事件,改进了 WebAssembly 调试体验,以及对使用 OpenID Connect 进行身份验证的开箱即用支持。要了解更多信息,请阅读最新的 Blazor 团队博客文章。

  • 最新的 Blazor 团队博客文章: https://devblogs.microsoft.com/dotnet/category/blazor/?ocid=AID3052907

▌升级助手

.NET 升级助手提供分步指导、见解和自动化,将您的旧应用程序迁移到 .NET 6 和 .NET 7。在某些情况下,它可以为您执行迁移!在对旧代码库进行现代化改造时,它有助于减少时间和复杂性。例如,了解如何借助 CoreWCF 将 WCF 应用程序引入 .NET Core。使用 .NET 7,改进的体验包括:

  • NET 到 ASP.NET Core
  • Web 适配器(预览版)
  • 增量迁移(预览)
  • 为 WinForms、WPF 和控制台/类库添加了更多分析器和代码修复程序
  • 分析二进制文件的能力
  • UWP 到 Windows Appp SDK 和 WinUI 支持

准备好将您的应用程序迁移到迄今为止最新且性能最快的 .NET 上了吗?请立即下载升级助手!

  • 下载升级助手 https://dotnet.microsoft.com/platform/upgrade-assistant

.NET 适用于云原生应用

.NET 7 使开箱即用地构建云原生应用程序变得前所未有的容易。使用 Visual Studio 的连接服务安全地连接到数据服务并安全地加密用户机密文件或 Azure Key Vault 中的连接字符串。将您的应用程序直接构建到容器映像中。使用 Entity Framework 7 编写强类型语言集成查询 (LINQ) 查询,这些查询使用 SQL Server 的 JSON 支持从存储在关系数据库中的 JSON 文档中快速提取内容。只需几行代码,即可通过经过身份验证的端点交付安全的 JSON 文档,并提供最少的 API 体验。使用 Open Telemetry 收集有关您正在运行的应用程序的见解。

▌Azure 支持

.NET 7 不仅非常适合构建云原生应用程序;Azure 的 PaaS 服务,例如适用于 Windows 和 Linux 的应用服务、Static Web 应用、Azure Functions 和 Azure 容器应用,已经都支持 .NET 7 了 就像之前的 .NET 5.0 和 6.0一样。在发布的第一周,您可能会遇到 .NET 7 应用程序的启动时间稍长一些,因为 .NET 7 SDK 将及时安装,以便客户使用 .NET 7 创建新的应用程序服务。此外,如果您正在运行 .NET 7 预览版,只需重新启动应用服务即可将您更新到 GA。

▌内置容器支持

容器的普及和实际使用正在上升,对于许多公司来说,它们代表了部署到云的首选方式。但是,使用容器会为团队的积压工作增加新的工作,包括构建和发布镜像、检查安全性和合规性以及优化镜像的性能。我们相信有机会使用 .NET 容器创建更好、更简化的体验。

现在,您只需使用 dotnet publish 即可创建应用程序的容器化版本。我们构建此解决方案的目标是与现有构建逻辑无缝集成,利用我们自己丰富的 C# 工具和运行时性能,并直接内置到 .NET SDK 的盒子中以进行定期更新。

容器图像现在是 .NET SDK 支持的输出类型:

代码语言:javascript复制
# create a new project and move to its directory
dotnet new mvc -n my-awesome-container-app
cd my-awesome-container-app
# add a reference to a (temporary) package that creates the container
dotnet add package Microsoft.NET.Build.Containers
# publish your project for linux-x64
dotnet publish --os linux --arch x64 -p:PublishProfile=DefaultContainer

要了解有关内置容器支持的更多信息,请参阅官宣对 .NET SDK 的内置容器支持。

  • 官宣对 .NET SDK 的内置容器支持 https://devblogs.microsoft.com/dotnet/announcing-builtin-container-support-for-the-dotnet-sdk/?ocid=AID3052907

▌Microsoft Orleans

Microsoft Orleans 7.0 将使用“plain old CLR object”(POCO) Grains 提供更简单的编程模型,提供比 3.x 高 150% 的性能,并引入新的序列化和不变性改进。ASP.NET Core 开发人员可以使用 Orleans 简单地添加分布式状态,并确信他们的应用程序将在不增加复杂性的情况下水平扩展。我们将继续投资以使 Orleans 功能更接近 ASP.NET 堆栈,以确保您的 Web 和 API 应用程序为云规模、分布式托管方案甚至多云部署做好准备。Orleans 支持大多数流行的存储机制和数据库,并且能够在 ASP.NET Core 可以运行的任何地方运行,Orleans 是让您的 .NET 应用程序具有云原生分布式功能的绝佳选择,而无需学习新的框架或工具集。了解有关 Orleans 7 的更多信息。

  • 了解有关 Orleans 7 的更多信息 https://devblogs.microsoft.com/dotnet/whats-new-in-orleans-7/?ocid=AID3052907

▌可观察性

可观察性的目标是帮助您更好地了解应用程序在扩展和技术复杂性增加时的状态。.NET 接受了 OpenTelemetry,同时还在 .NET 7 中进行了以下改进。

▌介绍 Activity.Current 更改事件

分布式跟踪的典型实现使用 AsyncLocal<T> 来跟踪托管线程的“跨度上下文”。通过使用采用 valueChangedHandler 参数的 AsyncLocal<T> 构造函数来跟踪跨度上下文的更改。但是,随着 Activity 成为代表spans 的标准被OpenTelemetry 所使用,因此不可能设置值更改处理程序,但是 上下文可以通过 Activity.Current 来跟踪。可以使用新的更改事件来接收所需的通知。

代码语言:javascript复制
Activity.CurrentChanged  = CurrentChanged;
void CurrentChanged(object? sender, ActivityChangedEventArgs e)
{
  Console.WriteLine($"Activity.Current value changed from Activity:  {e.Previous.OperationName} to Activity: {e.Current.OperationName}");
}

▌公开高性能活动属性枚举器方法

以下新公开的方法可以在性能关键场景中用于枚举 Activity 的标签、链接和事件属性,而无需任何额外的分配和高性能项访问:

代码语言:javascript复制
Activity a = new Activity("Root");
a.SetTag("key1", "value1");
a.SetTag("key2", "value2");
foreach (ref readonly KeyValuePair<string, object?> tag in a.EnumerateTagObjects())
{
  Console.WriteLine($"{tag.Key}, {tag. Value}");

▌公开高性能 ActivityEvent 和 ActivityLink 标签枚举器方法

与上述类似,ActivityEvent 和 ActivityLink Tag 对象也被公开,以减少对高性能项目访问的任何额外分配:

代码语言:javascript复制
var tags = new List<KeyValuePair<string, object?>>()
{
  new KeyValuePair<string, object?>("tag1", "value1"),
  new KeyValuePair<string, object?>("tag2", "value2"),
};
ActivityLink link = new ActivityLink(default, new ActivityTagsCollection(tags));
foreach (ref readonly KeyValuePair<string, object?> tag in link.EnumerateTagObjects())
{
// Consume the link tags without any extra allocations or value copying.
}
ActivityEvent e = new ActivityEvent("SomeEvent", tags: new ActivityTagsCollection(tags));
foreach (ref readonly KeyValuePair<string, object?> tag in e.EnumerateTagObjects())
{
// Consume the event's tags without any extra allocations or value copying.
}

简单

▌C# 11 & F# 7

C# 和 F# 语言的最新成员是 C# 11 和 F# 7。C# 11 使通用数学等新功能成为可能,同时通过对象初始化改进、原始字符串文字等简化了代码。

▌通用数学

.NET 7 为基类库引入了新的数学相关通用接口。这些接口的可用性意味着您可以将泛型类型或方法的类型参数约束为“类似数字”。此外,C# 11 及更高版本允许您定义静态虚拟接口成员。因为运算符必须声明为静态,所以这个新的 C# 功能允许在新接口中为类似数字的类型声明运算符。

总之,这些创新让您可以通用地执行数学运算——也就是说,无需知道您正在使用的确切类型。例如,如果您想编写一个将两个数字相加的方法,之前您必须为每种类型添加方法的重载(例如,static int Add(int first, int second) 和 static float Add(float first, float second). 现在您可以编写一个单一的泛型方法,其中类型参数被限制为类似数字的类型。

代码语言:javascript复制
static T Add<T>(T left, T right) where T : INumber<T>
{
    return left   right;
}

在此方法中,类型参数 T 被约束为实现新 INumber<TSelf> 接口的类型。INumber<TSelf> 实现了 IAdditionOperators<TSelf,TOther,TResult> 接口,其中包含 运算符。这通常允许该方法添加两个数字。此方法可用于 .NET 的任何内置数字类型,因为它们都已更新为在 .NET 7 中实现 INumber<TSelf>。

库作者将从通用数学接口中受益最多,因为他们可以通过删除“冗余”重载来简化他们的代码库。其他开发人员将间接受益,因为他们使用的 API 可能会开始支持更多类型。

▌原始字符串文字

现在有一种新的字符串文字格式。原始字符串文字可以包含任意文本,包括空格、换行符、嵌入引号和其他特殊字符,而无需转义序列。原始字符串文字至少以三个双引号 (""") 字符开头,并以相同数量的双引号字符结尾。

代码语言:javascript复制
string longMessage = """
    This is a long message.
    It has several lines.
        Some are indented
                more than others.
    Some should start at the first column.
    Some have "quoted text" in them.
    """;

▌.NET 库

许多 .NET 的第一方库在 .NET 7 版本中得到了显着改进。您将看到对 Microsoft.Extensions.* 包的可为空注释的支持、System.Text.Json 的合同自定义和类型层次结构,以及帮助您以磁带存档 (TAR) 格式编写数据的新 Tar API 等等。

▌Microsoft.Extensions 的可空注释

所有 Microsoft.Extensions.* 库现在都包含 C# 8 选择加入功能,该功能允许编译器跟踪引用类型可空性以捕获潜在的空取消引用。这有助于您将代码导致运行时抛出 System.NullReferenceException 的可能性降至最低。

▌System.Composition.Hosting

添加了一个新 API 以允许 System.Composition.Hosting 容器中的单个对象实例通过 API ComposeExportedValue(CompositionContainer, T) 提供与 System.ComponentModel.Composition.Hosting 的旧接口类似的功能。

代码语言:javascript复制
namespace System.Composition.Hosting
{
  public class ContainerConfiguration
  {
      public ContainerConfiguration WithExport<TExport>(TExport exportedInstance);
      public ContainerConfiguration WithExport<TExport>(TExport exportedInstance, string contractName = null, IDictionary<string, object> metadata = null);
      public ContainerConfiguration WithExport(Type contractType, object exportedInstance);
      public ContainerConfiguration WithExport(Type contractType, object exportedInstance, string contractName = null, IDictionary<string, object> metadata = null);
  }
}

▌将微秒和纳秒添加到 TimeStamp、DateTime、DateTimeOffset 和 TimeOnly

在 .NET 7 之前,各种日期和时间结构中可用的最小时间增量是 Ticks 属性中的“tick”。作为参考,一个tick是 100ns。传统上,开发人员必须对“tick”值执行计算以确定微秒和纳秒值。在 .NET 7 中,我们在日期和时间实现中引入了微秒和纳秒:

代码语言:javascript复制
namespace System
{
  public struct DateTime
  {
    public DateTime(int year, int month, int day, int hour, int minute, int second, int millisecond, int microsecond);
    public DateTime(int year, int month, int day, int hour, int minute, int second, int millisecond, int microsecond, System.DateTimeKind kind);
    public DateTime(int year, int month, int day, int hour, int minute, int second, int millisecond, int microsecond, System.Globalization.Calendar calendar);
    public int Microsecond { get; }
    public int Nanosecond { get; }
    public DateTime AddMicroseconds(double value);
  }
  public struct DateTimeOffset
  {
    public DateTimeOffset(int year, int month, int day, int hour, int minute, int second, int millisecond, int microsecond, System.TimeSpan offset);
    public DateTimeOffset(int year, int month, int day, int hour, int minute, int second, int millisecond, int microsecond, System.TimeSpan offset, System.Globalization.Calendar calendar);
    public int Microsecond { get; }
    public int Nanosecond { get; }
    public DateTimeOffset AddMicroseconds(double microseconds);
  }
  public struct TimeSpan
  {
    public const long TicksPerMicrosecond = 10L;
    public const long NanosecondsPerTick = 100L;
    public TimeSpan(int days, int hours, int minutes, int seconds, int milliseconds, int microseconds);
    public int Microseconds { get; }
    public int Nanoseconds { get; }
    public double TotalMicroseconds { get; }
    public double TotalNanoseconds { get; }
    public static TimeSpan FromMicroseconds(double microseconds);
  }
  public struct TimeOnly
  {
    public TimeOnly(int hour, int minute, int second, int millisecond, int microsecond);
    public int Microsecond { get; }
    public int Nanosecond { get; }
  }
}
Microsoft.Extensions.Caching
We added metrics support for IMemoryCache, which is a new API of MemoryCacheStatistics that holds cache hits, misses, and estimated size for IMemoryCache. You can get an instance of MemoryCacheStatistics by calling GetCurrentStatistics() when the flag TrackStatistics is enabled.
The GetCurrentStatistics() API allows app developers to use event counters or metrics APIs to track statistics for one or more memory cache.
// when using `services.AddMemoryCache(options => options.TrackStatistics = true);` to instantiate
[EventSource(Name = "Microsoft-Extensions-Caching-Memory")]
internal sealed class CachingEventSource : EventSource
{
  public CachingEventSource(IMemoryCache memoryCache)
  {
    _memoryCache = memoryCache;
  }
  protected override void OnEventCommand(EventCommandEventArgs command)
  {
    if (command.Command == EventCommand.Enable)
    {
      if (_cacheHitsCounter == null)
      {
          _cacheHitsCounter = new PollingCounter("cache-hits", this, () => _memoryCache.GetCurrentStatistics().CacheHits)
          {
            DisplayName = "Cache hits",
          };
      }
    }
  }
}

▌Microsoft.Extensions.Caching

我们添加了对 IMemoryCache 的指标支持,这是 MemoryCacheStatistics 的一个新 API,用于保存 IMemoryCache 的缓存命中、未命中和估计大小。当启用标志 TrackStatistics 时,您可以通过调用 GetCurrentStatistics() 来获取 MemoryCacheStatistics 的实例。

GetCurrentStatistics() API 允许应用程序开发人员使用事件计数器或指标 API 来跟踪一个或多个内存缓存的统计信息。

代码语言:javascript复制
// when using `services.AddMemoryCache(options => options.TrackStatistics = true);` to instantiate
[EventSource(Name = "Microsoft-Extensions-Caching-Memory")]
internal sealed class CachingEventSource : EventSource
{
  public CachingEventSource(IMemoryCache memoryCache)
  {
    _memoryCache = memoryCache;
  }
  protected override void OnEventCommand(EventCommandEventArgs command)
  {
    if (command.Command == EventCommand.Enable)
    {
      if (_cacheHitsCounter == null)
      {
          _cacheHitsCounter = new PollingCounter("cache-hits", this, () => _memoryCache.GetCurrentStatistics().CacheHits)
          {
            DisplayName = "Cache hits",
          };
      }
    }
  }
}

然后,您可以使用 dotnet-counters 工具查看以下统计信息:

代码语言:javascript复制
Press p to pause, r to resume, q to quit.
    Status: Running
[System.Runtime]
    CPU Usage (%)                                      0
    Working Set (MB)                                  28
[Microsoft-Extensions-Caching-MemoryCache]
    cache-hits                                       269

▌System.Formats.Tar APIs

我们添加了一个新的 System.Formats.Tar 程序集,其中包含允许读取、写入、归档和提取 Tar 档案的跨平台 API。SDK 甚至使用这些 API 来创建容器作为发布目标。

代码语言:javascript复制
// Generates a tar archive where all the entry names are prefixed by the root directory 'SourceDirectory'
TarFile.CreateFromDirectory(sourceDirectoryName: "/home/dotnet/SourceDirectory/", destinationFileName: "/home/dotnet/destination.tar", includeBaseDirectory: true);
// Extracts the contents of a tar archive into the specified directory, but avoids overwriting anything found inside
TarFile.ExtractToDirectory(sourceFileName: "/home/dotnet/destination.tar", destinationDirectoryName: "/home/dotnet/DestinationDirectory/", overwriteFiles: false);

▌类型转换器

现在有针对新添加的原始类型 DateOnly、TimeOnly、Int128、UInt128 和 Half 的公开类型转换器。

代码语言:javascript复制
namespace System.ComponentModel
{
  public class DateOnlyConverter : System.ComponentModel.TypeConverter
  {
    public DateOnlyConverter() { }
  }
  public class TimeOnlyConverter : System.ComponentModel.TypeConverter
  {
    public TimeOnlyConverter() { }
  }
  public class Int128Converter : System.ComponentModel.BaseNumberConverter
  {
    public Int128Converter() { }
  }
  public class UInt128Converter : System.ComponentModel.BaseNumberConverter
  {
    public UInt128Converter() { }
  }
  public class HalfConverter : System.ComponentModel.BaseNumberConverter
  {
    public HalfConverter() { }
  }
}

这些是有用的转换器,可以轻松转换为更原始的类型。

代码语言:javascript复制
TypeConverter dateOnlyConverter = TypeDescriptor.GetConverter(typeof(DateOnly));
// produce DateOnly value of DateOnly(1940, 10, 9)
DateOnly? date = dateOnlyConverter.ConvertFromString("1940-10-09") as DateOnly?;
TypeConverter timeOnlyConverter = TypeDescriptor.GetConverter(typeof(TimeOnly));
// produce TimeOnly value of TimeOnly(20, 30, 50)
TimeOnly? time = timeOnlyConverter.ConvertFromString("20:30:50") as TimeOnly?;
TypeConverter halfConverter = TypeDescriptor.GetConverter(typeof(Half));
// produce Half value of -1.2
Half? half = halfConverter.ConvertFromString(((Half)(-1.2)).ToString()) as Half?;
TypeConverter Int128Converter = TypeDescriptor.GetConverter(typeof(Int128));
// produce Int128 value of Int128.MaxValue which equal 170141183460469231731687303715884105727
Int128? int128 = Int128Converter.ConvertFromString("170141183460469231731687303715884105727") as Int128?;
TypeConverter UInt128Converter = TypeDescriptor.GetConverter(typeof(UInt128));
// produce UInt128 value of UInt128.MaxValue Which equal 340282366920938463463374607431768211455
UInt128? uint128 = UInt128Converter.ConvertFromString("340282366920938463463374607431768211455") as UInt128?;

▌System.Text.Json 合约定制

System.Text.Json 通过为该类型构造 JSON 契约来确定给定 .NET 类型如何被序列化和反序列化。契约派生自类型的形状——例如其可用的构造函数、属性和字段,以及它是否实现 IEnumerable 或 IDictionary——在运行时使用反射或在编译时使用源生成器。在以前的版本中,假设用户能够修改类型声明,他们可以使用 System.Text.Json 属性注释对派生合约进行有限的调整。

给定类型 T 的合约元数据使用 JsonTypeInfo<T> 表示,在以前的版本中,它用作源生成器 API 中专用的不透明令牌。从 .NET 7 开始,JsonTypeInfo 合同元数据的大多数方面都已公开并可供用户修改。合约定制允许用户使用 IJsonTypeInfoResolver 接口的实现编写自己的 JSON 合约解析逻辑:

代码语言:javascript复制
public interface IJsonTypeInfoResolver
{
  JsonTypeInfo? GetTypeInfo(Type type, JsonSerializerOptions options);
}

合同解析器为给定的 Type 和 JsonSerializerOptions 组合返回配置的 JsonTypeInfo 实例。如果解析器不支持指定输入类型的元数据,它可以返回 null。

默认的基于反射的序列化程序执行的合同解析现在通过实现 IJsonTypeInfoResolver 的 DefaultJsonTypeInfoResolver 类公开。此类允许用户通过自定义修改扩展默认的基于反射的解析,或将其与其他解析器(例如源生成的解析器)结合使用。

从 .NET 7 开始,源生成中使用的 JsonSerializerContext 类也实现了 IJsonTypeInfoResolver。要了解有关源生成器的更多信息,请参阅如何在 System.Text.Json 中使用源生成。

可以使用新的 TypeInfoResolver 属性为 JsonSerializerOptions 实例配置自定义解析器:

代码语言:javascript复制
// configure to use reflection contracts
var reflectionOptions = new JsonSerializerOptions
{
    TypeInfoResolver = new DefaultJsonTypeInfoResolver()
};
// configure to use source generated contracts
var sourceGenOptions = new JsonSerializerOptions
{
    TypeInfoResolver = EntryContext.Default
};
[JsonSerializable(typeof(MyPoco))]
public partial class EntryContext : JsonSerializerContext { }

▌System.Text.Json 类型层次结构

System.Text.Json 现在支持用户定义类型层次结构的多态序列化和反序列化。这可以通过使用新的 JsonDerivedTypeAttribute 装饰类型层次结构的基类来启用:

代码语言:javascript复制
[JsonDerivedType(typeof(Derived))]
public class Base
{
    public int X { get; set; }
}
public class Derived : Base
{
    public int Y { get; set; }
}

此配置为 Base 启用多态序列化,特别是在运行时类型为 Derived 时:

代码语言:javascript复制
Base value = new Derived();
JsonSerializer.Serialize<Base>(value); // { "X" : 0, "Y" : 0 }

请注意,这不会启用多态反序列化,因为有效负载将作为 Base 往返:

代码语言:javascript复制
Base value = JsonSerializer.Deserialize<Base>(@"{ ""X"" : 0, ""Y"" : 0 }");
value is Derived; // false

查看 .NET 7 中 System.Text.Json 的新增功能博客文章,了解有关类型层次结构的更多详细信息。

  • .NET 7 中 System.Text.Json 的新增功能博客文章 https://devblogs.microsoft.com/dotnet/system-text-json-in-dotnet-7/#type-hierarchies?ocid=AID3052907

▌.NET SDK

.NET SDK 继续添加新功能,让您比以往任何时候都更有效率。在 .NET 7 中,我们改进了您使用 .NET CLI、创作模板和在中央位置管理包的体验。

▌CLI 解析器和选项卡完成

dotnet new 命令为用户熟悉和喜爱的许多子命令提供了更加一致和直观的界面。还支持模板选项和参数的制表符完成。CLI 现在会在用户键入时提供有关有效参数和选项的反馈。

以下是新的帮助输出示例:

代码语言:javascript复制
❯ dotnet new --help
Description:
  Template Instantiation Commands for .NET CLI.
Usage:
  dotnet new [<template-short-name> [<template-args>...]] [options]
  dotnet new [command] [options]
Arguments:
  <template-short-name>  A short name of the template to create.
  <template-args>        Template specific options to use.
Options:
  -?, -h, --help  Show command line help.
Commands:
  install <package>       Installs a template package.
  uninstall <package>     Uninstalls a template package.
  update                  Checks the currently installed template packages for update, and install the updates.
  search <template-name>  Searches for the templates on NuGet.org.
  list <template-name>    Lists templates containing the specified template name. If no name is specified, lists all templates.

一直以来,dotnet CLI 都支持 tab 补全,其中包括 PowerShell、bash、zsh 和 fish 等流行的 shell。然而,实现有意义的补全取决于单独的 dotnet 命令。对于 .NET 7,dotnet new 命令学习了如何提供制表符补全。

代码语言:javascript复制
❯ dotnet new angular
angular              grpc                 razor                viewstart            worker               -h
blazorserver         mstest               razorclasslib        web                  wpf                  /?
blazorwasm           mvc                  razorcomponent       webapi               wpfcustomcontrollib  /h
classlib             nugetconfig          react                webapp               wpflib               install
console              nunit                reactredux           webconfig            wpfusercontrollib    list
editorconfig         nunit-test           sln                  winforms             xunit                search
gitignore            page                 tool-manifest        winformscontrollib   --help               uninstall
globaljson           proto                viewimports          winformslib          -?                   update

这有助于您在创建新的 .NET 项目时做出选择,以了解哪些选项和参数可供您使用。

代码语言:javascript复制
❯ dotnet new web --dry-run
--dry-run                  --language                 --output                   -lang
--exclude-launch-settings  --name                     --type                     -n
--force                    --no-https                 -?                         -o
--framework                --no-restore               -f                         /?
--help                     --no-update-check          -h                         /h

此外,给定命令通常错误或不支持哪些常见选项和参数。相反,您只会看到当前版本的 .NET CLI 支持的内容。

代码语言:javascript复制
❯ dotnet new blazorserver --auth Individual
Individual     IndividualB2C  MultiOrg       None           SingleOrg      Windows

▌模板创作

.NET 7 将约束的概念添加到 .NET 模板。约束允许您定义允许模板的上下文,这有助于模板引擎确定应在 dotnet new list 等命令中显示哪些模板。在此版本中,我们添加了对三种类型约束的支持:

  • 操作系统:根据用户的操作系统限制模板
  • 模板引擎主机:根据执行模板引擎的主机来限制模板。这通常是 .NET CLI 本身,或者是嵌入式场景,如 Visual Studio 或 Visual Studio for Mac 中的“新建项目”对话框。
  • 已安装的工作负载:要求在模板可用之前安装指定的 .NET SDK 工作负载。

在所有情况下,描述这些约束就像在模板的配置文件中添加一个新的约束部分一样简单:

代码语言:javascript复制
"constraints": {
       "web-assembly": {
           "type": "workload",
           "args": "wasm-tools"
       },
   }

我们还添加了选择参数的新功能。这是用户在单个选择中指定多个值的能力。这可以以与使用标志样式枚举相同的方式使用。此类参数的常见示例可能是:

  • 在 Web 模板上选择多种形式的身份验证。
  • 在 MAUI 模板中一次选择多个目标平台(iOS、Android、Web)。

选择加入此行为就像在模板配置中的参数定义中添加 "allowMultipleValues": true 一样简单。完成后,您将可以访问多个帮助函数以在模板内容中使用,并帮助检测用户选择的特定值。

▌中央包管理

依赖管理是 NuGet 的核心功能。管理单个项目的依赖关系很容易。管理多项目解决方案的依赖关系可能会变得很困难,因为它们的规模和复杂性开始扩大。在您管理许多不同项目的公共依赖项的情况下,您可以利用 NuGet 的中央包管理功能从一个位置轻松完成所有这些工作。

要开始集中包管理,您可以在解决方案的根目录中创建一个 Directory.Packages.props 文件,并将 MSBuild 属性 ManagePackageVersionsCentrally 设置为 true。

在内部,您可以使用定义包 ID 和版本的 <PackageVersion /> 元素定义解决方案所需的各个包版本。

代码语言:javascript复制
<Project>
  <PropertyGroup>
    <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
  </PropertyGroup>
  <ItemGroup>
    <PackageVersion Include="Newtonsoft.Json" Version="13.0.1" />
  </ItemGroup>
</Project>

在解决方案的项目中,您可以使用您熟悉和喜爱的相应 <PackageReference /> 语法,但没有 Version 属性来推断集中管理的版本。

代码语言:javascript复制
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Newtonsoft.Json" />
  </ItemGroup>
</Project>

性能

性能一直是每个 .NET 版本的重要组成部分。每年,.NET 团队都会发布一篇关于最新改进的博客。以下是最新性能帖子的简要概述:

TL;DR:.NET 7 速度很快。在这个版本中,一千多个影响性能的 PR 进入了运行时和核心库,更不用说 ASP.NET Core 和 Windows Forms 和 Entity Framework 及其他方面的所有改进。这是有史以来最快的 .NET。如果你的经理问你为什么你的项目应该升级到 .NET 7,你可以说“除了发行版中的所有新功能之外,.NET7超级快”。– Stephen Toub

▌On Stack Replacement (OSR)

堆栈上替换 (OSR) 允许运行时在方法执行过程中更改当前正在运行的方法执行的代码,尽管这些方法在“堆栈上”处于活动状态。它作为分层编译的补充。

OSR 允许长时间运行的方法在执行过程中切换到更优化的版本,因此运行时可以首先快速 JIT 所有方法,然后在通过分层编译频繁调用这些方法或通过长时间运行循环时过渡到更优化的版本 操作系统。

OSR 提高了启动时间。几乎所有的方法现在最初都是由快速 JIT 进行的。我们已经看到像 Avalonia “IL” spy 这样的 jitting-heavy 应用程序的启动时间缩短了 25%,我们跟踪的各种 TechEmpower 基准测试显示,首次请求的时间缩短了 10-30%。

OSR 还可以提高应用程序的性能,尤其是使用动态 PGO 的应用程序,因为现在可以更好地优化带有循环的方法。例如,启用 OSR 后,Array2 微基准测试显示出显着的改进。

▌Profile-Guided Optimization (PGO)

配置文件引导优化(PGO) 在许多语言和编译器中已经存在了很长时间。基本思想是编译应用程序,要求编译器将检测注入应用程序以跟踪各种有趣的信息。然后,你让你的应用程序通过它的步伐,运行各种常见的场景,使该仪器“分析”应用程序执行时发生的情况,然后保存结果。然后重新编译应用程序,将这些检测结果反馈给编译器,并允许它优化应用程序以准确地使用它。

这种 PGO 方法被称为“静态 PGO”,因为所有信息都是在实际部署之前收集的,而这也是 .NET 多年来以各种形式所做的事情。.NET 中有趣的发展是“动态 PGO”,它是在 .NET 6 中引入的,但默认情况下是关闭的。

动态 PGO 利用分层编译。JIT 检测第 0 层代码以跟踪方法被调用的次数,或者在循环的情况下,循环执行了多少次。分层编译可以提供多种可能性。例如,它可以准确地跟踪哪些具体类型被用作接口调度的目标,然后在第 1 层中,专门化代码以期望最常见的类型(这被称为“受保护的去虚拟化”或 GDV )。你可以在这个小例子中看到这一点。将 DOTNET_TieredPGO 环境变量设置为 1,然后在 .NET 7 上运行它:

代码语言:javascript复制
class Program
{
  static void Main()
  {
    IPrinter printer = new Printer();
    for (int i = 0; ; i  )
    {
      DoWork(printer, i);
    }
  }
  static void DoWork(IPrinter printer, int i)
  {
    printer.PrintIfTrue(I == int.MaxValue);
  }
  interface iPrinter
  {
    void PrintIfTrue(bool condition);
  }
  class Printer : iPrinter
  {
    public void PrintIfTrue(bool condition)
    {
      if (condition) Console.WriteLine“"Print”");
    }
  }
}

使用 PGO 获得的主要改进是它现在可以与 .NET 7 中的 OSR 一起使用。这意味着执行接口调度的热运行方法可以获得这些去虚拟化/内联优化。

禁用 PGO 后,您可以获得相同的 .NET 6 和 .NET 7 性能吞吐量。

方法

运行时

均值

比率

DelegatePGO

.NET 6.0

1.665 ns

1.00

DelegatePGO

.NET 7.0

1.659 ns

1.00

但是,当您通过 <TieredPGO>true</TieredPGO> 或 DOTNET_TieredPGO=1 的环境变量在 .csproj 中启用动态 PGO 时,情况会发生变化。.NET 6 的速度提高了约 14%,但 .NET 7 的速度提高了约 3 倍。

方法

运行时

均值

比率

DelegatePGO

.NET 6.0

1.427.7 ns

1.00

DelegatePGO

.NET 7.0

539.0

0.38

▌Native AOT

对于许多人来说,软件上下文中的“性能”一词与吞吐量有关。它可以执行多快?它每秒可以处理多少数据?它每秒可以处理多少个请求?还有很多。但性能还有很多其他方面。它消耗多少内存?它以多快的速度启动并开始做一些有用的事情?它在磁盘上消耗多少空间?下载需要多长时间?

然后是相关的担忧。实现这些目标需要哪些依赖项?它需要执行哪些类型的操作来实现这些目标,以及所有这些操作在目标环境中是否允许?如果这一段中的任何一段引起了您的共鸣,那么您就是现在在 .NET 7 中提供的本机 AOT 支持的目标受众。

.NET 长期以来一直支持 AOT 代码生成。例如,.NET Framework 有 ngen 的形式,.NET Core 有 crossgen 的形式。这两种解决方案都涉及一个标准的 .NET 可执行文件,其中一些 IL 已经编译为汇编代码,但并非所有方法都会为它们生成汇编代码,各种事情都可能使生成的汇编代码无效,外部 .NET 程序集没有任何本机汇编代码 加载等等,在所有这些情况下,运行时还是继续使用 JIT 编译器。而原生 AOT 不同,它是 CoreRT 的演变,CoreRT 本身是 .NET Native 的演变,完全脱离于 JIT。

发布构建生成的二进制文件是目标平台特定于平台的文件格式(例如,Windows 上的 COFF、Linux 上的 ELF、macOS 上的 Mach-O)的完全独立的可执行文件,除了标准之外没有任何外部依赖项 到该平台(例如,libc)。而且它完全是原生的:看不到 IL,没有 JIT,什么都没有。所有必需的代码都被编译和/或链接到可执行文件中,包括与标准 .NET 应用程序和服务一起使用的相同 GC,以及围绕线程等提供服务的最小运行时。

所有这些都带来了巨大的好处:超快的启动时间、小型且完全独立的部署,以及在不允许 JIT 编译器的地方运行的能力(因为无法执行可写内存页面)。它也带来了限制:没有 JIT 意味着没有动态加载任意程序集(例如,Assembly.LoadFile)和没有反射发射(例如,DynamicMethod),并且所有内容都被编译并链接到应用程序中,这意味着使用更多功能(或可能使用 ) 并且您的部署可以更大。即使有这些限制,对于某些类别的应用程序,Native AOT 仍然是 .NET 7 中令人难以置信的、令人兴奋和受欢迎的补充。

今天,Native AOT 专注于控制台应用,那么我们来创建一个控制台应用:

代码语言:javascript复制
dotnet new console -o nativeaotexample

您现在有一个“Hello World”控制台应用程序。要启用使用Native AOT发布应用程序,请编辑 .csproj 以在现有 <PropertyGroup> 中包含以下内容:

代码语言:javascript复制
<PublishAot>true</PublishAot>

该应用程序现在已完全配置为能够以Native AOT为目标。剩下的就是发布了。如果要发布到 Windows x64 运行时,可以使用以下命令:

代码语言:javascript复制
dotnet publish -r win-x64 -c Release

会在输出发布目录中生成一个可执行文件:

代码语言:javascript复制
Directory: C:nativeaotexamplebinReleasenet7.0win-x64publish
Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a---           8/27/2022  6:18 PM        3648512 nativeaotexample.exe
-a---           8/27/2022  6:18 PM       14290944 nativeaotexample.pdb

大约 3.5MB 的 .exe 是可执行文件,它旁边的 .pdb 是调试信息,部署应用程序不需要这些信息。现在,您可以将该 nativeaotexample.exe 复制到任何 64 位 Windows 机器上,应用程序都会运行。

▌支持 .NET 7

.NET 7 由 Microsoft 正式支持。它被标记为标准期限支持 (STS) 版本,将获得 18 个月的支持。奇数 .NET 版本是 STS 版本,在随后的 STS 或 LTS 版本之后的六个月内获得免费支持和补丁。有关更多详细信息,请参阅我们的 .NET 和 .NET Core 支持生命周期文档。

  • .NET 和 .NET Core 支持生命周期文档 https://dotnet.microsoft.com/platform/support/policy/dotnet-core

最后,我们想说:

没有社区,.NET 就无法存在。.NET 项目是一个通过每个人的独特和创造性贡献的项目。这些伟大的成就和慷慨来自于我们社区每个人的支持和关怀。感谢您的参与、分享以及您对 .NET 社区的贡献。

*未经授权请勿私自转载此文章及图片。

0 人点赞