.NET 6 预览版 5 发布

很高兴.NET 6 预览版5终于跟大家见面了。我们现在正处于.NET 6 的后半部分,开始整合一些重要的功能。 例如.NET SDK 工作负载,它是我们.NET 统一愿景的基础,可以支持更多类型的应用程序。 与其他功能一样,它也是为了更好地服务于端到端的用户体验。

您可以下载适用于Linux、macOS 和Windows 的.NET 6 预览版5

请参阅ASP.NET CoreEF Core以及.NET MAUI,了解有关Web、数据访问和跨平台UI 方案新增功能的更多详细信息。

Visual Studio 2022 预览版1 也在今天发布,.NET 6 预览版5包含其中。.NET 6 还在Visual Studio 16.11Visual Studio for Mac 8.9进行了测试。 如果您想在Visual Studio中试用.NET 6,我们建议您使用这些版本。

您也可以查看新的对话帖,深入了解工程师们对.NET新功能的想法。

.NET SDK:可选的工作负载改进

SDK 工作负载是一项新的.NET SDK 功能,它使我们能够在不增加SDK 大小的情况下添加对新应用程序类型(如移动WebAssembly)的支持。

工作负载添加了包括list 和update 指令。这些新功能提供了与我们期望的最终体验一致的使用感受。您将能够使用一些简单的指令快速构建您的环境,并一直将其保持最新状态。

  • dotnet workload list 会告诉您安装了哪些工作负载。
  • dotnet workload update 会将所有已安装的工作负载更新到最新的可用版本。

update 指令会查询nuget.org 以获取更新的工作负载清单、更新本地清单、下载已安装工作负载的新版本,然后删除工作负载的所有旧版本。这类似于apt update 和apt upgrade -y(用于基于Debian 的Linux 发行版)。

dotnet workload 命令集在给定SDK 的上下文中运行。假设您同时安装了.NET 6 和.NET 7。如果您同时使用两者,则工作负载命令将提供不同的结果,因为工作负载会有所不同(至少是相同工作负载的不同版本)。

如您所见,工作负载功能本质上是.NET SDK 的包管理器。工作负载最初是在.NET 6 预览版4版本中引入的。

.NET SDK:NuGet 包验证

包验证工具将使NuGet 库开发人员能够验证他们的包是否一致且格式良好的。

这包括:

  • 验证不同版本之间没有重大更改。
  • 验证包对于所有特定运行时具有相同的公共 API 集。
  • 识别任何与目标框架或运行时适用性之间的差距。

此工具可通过Microsoft.DotNet.PackageValidation获得。

有关此工具的帖子很快就会发布

.NET SDK:更多Roslyn 分析器

在.NET 5 中,我们随.NET SDK 提供了大约250 个分析器。 其中许多已经存在,但我们将其作为NuGet 包另行发布。 我们正在.NET 6 添加更多分析器

默认情况下,大多数新分析器应用在Info级别。您可以通过如下AnalysisMode的配置在Warning级别启用这些分析器:

\<AnalysisMode\>AllEnabledByDefault\</AnalysisMode\>

我们发布了我们想要的.NET 6 分析器集(加上一些额外的东西),然后将其中的大部分都公开了

预览版5 中包含了Newell ClarkMeik Tranel的以下实现。请注意,社区用户在之前的预览版中贡献了其他实现。

Contributor Issue Title
Newell Clark dotnet/runtime #33777 Use span-based string.Concat
Newell Clark dotnet/runtime #33784 Prefer string.AsSpan() over string.Substring() when parsing
Newell Clark dotnet/runtime #33789 Override Stream.ReadAsync/WriteAsync
Newell Clark dotnet/runtime #35343 Replace Dictionary<,>.Keys.Contains with ContainsKey
Newell Clark dotnet/runtime #45552 Use String.Equals instead of String.Compare
Meik Tranel dotnet/runtime #47180 Use String.Contains(char) instead of String.Contains(String)

.NET SDK:为平台兼容性分析器启用自定义防护

CA1416 平台兼容性分析器已经使用OperatingSystem/RuntimeInformation 中的方法识别平台保护,例如OperatingSystem.IsWindows 和OperatingSystem.IsWindowsVersionAtLeast。 但是,分析器不识别任何其他保护可能性,例如缓存在字段或属性中的平台检查结果,或者在辅助方法中定义了复杂的平台检查逻辑。

为了使自定义保护成为可能,我们添加了新属性SupportedOSPlatformGuard 和UnsupportedOSPlatformGuard,可以使用相应的平台名称和/或版本注释自定义保护对象。 平台兼容性分析器的流分析逻辑识别并尊重这些注释的内容。

用法

  [UnsupportedOSPlatformGuard("browser")] // The platform guard attribute
#if TARGET_BROWSER
    internal bool IsSupported => false;
#else
    internal bool IsSupported => true;
#endif

    [UnsupportedOSPlatform("browser")]
    void ApiNotSupportedOnBrowser() { }

    void M1()
    {
        ApiNotSupportedOnBrowser();  // Warns: This call site is reachable on all platforms.'ApiNotSupportedOnBrowser()' is unsupported on: 'browser'

        if (IsSupported)
        {
            ApiNotSupportedOnBrowser();  // Not warn
        }
    }

    [SupportedOSPlatform("Windows")]
    [SupportedOSPlatform("Linux")]
    void ApiOnlyWorkOnWindowsLinux() { }

    [SupportedOSPlatformGuard("Linux")]
    [SupportedOSPlatformGuard("Windows")]
    private readonly bool _isWindowOrLinux = OperatingSystem.IsLinux() || OperatingSystem.IsWindows();

    void M2()
    {
        ApiOnlyWorkOnWindowsLinux();  // This call site is reachable on all platforms.'ApiOnlyWorkOnWindowsLinux()' is only supported on: 'Linux', 'Windows'.

        if (_isWindowOrLinux)
        {
            ApiOnlyWorkOnWindowsLinux();  // Not warn
        }
    }
}

Windows Forms: 默认字体

您现在可以使用Application.SetDefaultFont 为应用程序设置默认字体。 您使用的模式类似于设置high dpi或视觉样式。

class Program
{
    [STAThread]
    static void Main()
    {
        Application.SetHighDpiMode(HighDpiMode.SystemAware);
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);

+       Application.SetDefaultFont(new Font(new FontFamily("Microsoft Sans Serif"), 8f));

        Application.Run(new Form1());
    }
}

下面是设置默认字体后的两个例子(使用不同的字体)。

Microsoft Sans Serif, 8pt:

file

Chiller, 12pt:

file

.NET Core 3.0 中更新了默认字体。这一变化为一些用户将.NET Framework 应用程序迁移到.NET Core 带来了很大的阻碍。 默认字体设置使为应用程序选择所需字体变得简单,消除了迁移的障碍。

库:放弃对旧框架的支持

从包中删除框架是一个破坏源代码的改动。但是,让它一直支持我们发布的所有框架也增加了包的复杂性和大小。过去,我们通过_harvesting_来解决这个问题,也就是:

  • 我们只为"当前"支持的框架做构建
  • 在构建期间,我们下载包的早期版本,并切除我们不再构建的早期框架的二进制文件

虽然这意味着您可以随时更新而不必担心我们会删除框架,但这也意味着如果您使用切割过的二进制文件,您将永远不会获得任何错误修复或新功能。换句话说,切割过的无法提供服务的资产,现在已经被隐藏了,因为从您的角度来看,您可以继续将包更新到更高版本,即使您正在使用我们不再更新的旧二进制文件。

从.NET 6 Preview 5 开始,我们计划不再执行任何形式的harvesting,以确保我们为交付的所有内容都得到更好的服务。这意味着我们将放弃对任何早于以下版本的支持:

  • .NET Framework 4.6.1
  • .NET Core 3.1
  • .NET Standard 2.0

如果您当前正在引用来自早期框架的受影响的包,您将无法再将引用的包更新到更高版本。您可以将您的项目重新指向到更高版本的框架或选择不更新引用的包(这通常不是一个大的回退,因为无论如何您已经在使用冻结的二进制文件)。

更多详细信息,包括受影响软件包的完整列表,请参阅dotnet/announcement:删除旧框架版本

库:Microsoft.Extensions

我们一直在改进此版本的Microsoft.Extensions API。 在预览版5 中,我们专注于托管和依赖注入。 在预览版4 中,我们添加了一个用于日志记录的编译时源生成器

感谢Martin Björkström](https://github.com/bjorkstromm) 的dotnet/runtime #51840 (AsyncServiceScope)

托管 – ConfigureHostOptions API

我们在IHostBuilder 上添加了一个新的ConfigureHostOptions API 以简化应用程序配置(例如配置超时关闭):

using HostBuilder host = new()
    .ConfigureHostOptions(o =>
    {
        o.ShutdownTimeout = TimeSpan.FromMinutes(10);
    })
    .Build();

host.Run();

在预览版5 之前,配置更加复杂一些:

using HostBuilder host = new()
    .ConfigureServices(services =>
    {
        services.Configure<HostOptions>(o =>
        {
            o.ShutdownTimeout = TimeSpan.FromMinutes(10);
        });
    })
    .Build();

host.Run();

依赖注入 – CreateAsyncScope API

您可能已经注意到,当服务提供者注册了IAsyncDisposable 服务时,其销毁会抛出InvalidOperationException。

新的CreateAsyncScope API 提供了一个很直接的解决方案,如以下示例所示:

await using (var scope = provider.CreateAsyncScope())
{
    var foo = scope.ServiceProvider.GetRequiredService<Foo>();
}

以下示例演示了现有的问题案例,并演示了此前建议的解决方法。

using System;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;

await using var provider = new ServiceCollection()
        .AddScoped<Foo>()
        .BuildServiceProvider();

// This using can throw InvalidOperationException
using (var scope = provider.CreateScope())
{
    var foo = scope.ServiceProvider.GetRequiredService<Foo>();
}

class Foo : IAsyncDisposable
{
    public ValueTask DisposeAsync() => default;
}

您可以通过将返回的scope强制转换为IAsyncDisposable 来绕过异常。

var scope = provider.CreateScope();
var foo = scope.ServiceProvider.GetRequiredService<Foo>();
await ((IAsyncDisposable)scope).DisposeAsync();

CreateAsyncScope 解决了这个问题,使您可以安全地使用using 语句。

库:JsonSerializer 源码生成器

几乎所有.NET 序列化工具的支柱都是反射机制。反射对于某些场景来说是一个很棒的功能,但不适用于高性能云原生应用程序(因其通常需要处理大量序列化反序列化工作和JSON 文档)。过多的反射使用会引起一些程序启动、内存使用和程序集修整上的问题。

运行时反射的替代方法是编译时源代码生成。源码生成器生成C# 源文件,这些文件可以作为库或应用程序构建的一部分进行编译。在编译时生成源代码可以为.NET 应用程序提供许多好处,包括性能的提升。

在.NET 6 中,我们在System.Text.Json 中引入了一个新的源码生成器。JSON 源码生成器与JsonSerializer 协同工作,并且可以配置成多种多样的工作方式。是否使用新的源码生成器由您决定。它可以提供以下好处:

  • 减少启动时间
  • 提高序列化吞吐量
  • 减少私有内存使用
  • 移除运行时System.Reflection 和System.Reflection.Emit 的使用
  • 允许兼容trim的JSON 序列化

例如,源码生成器可以生成对properties更简单有效的赋值/取值的代码,而不是在运行时通过Reflection.Emit生成get和set方法(这会用到私有内存并且影响启动速度),使得性能得到了更大的提高。

您可以使用System.Text.Json NuGet 包的最新预览版来试用源码生成器。我们正在提议在SDK 中包含源码生成器。

生成优化的序列化逻辑

默认情况下,JSON 源码生成器为给定的可序列化类型应用序列化逻辑。这比直接使用现有JsonSerializer 来生成使用 Utf8JsonWriter 的源码提供了更高的性能。 简而言之,源生成器提供了一种在编译时应用的不同实现方法,以优化程序的运行时体验。

宏观来讲,JsonSerializer 是一个强大的工具,它有许多可以改进.NET 类型和JSON 格式之间序列化和反序列化的功能(甚至还有更多即将实现的新功能!)。 它速度很快,但是当序列化例程只需要一个功能子集时,它也会产生一些性能开销。 展望未来,我们将同时更新JsonSerializer 和新的源码生成器。

给定一个简单类:

namespace Test
{
    internal class JsonMessage
    {
        public string Message { get; set; }
    }
}

源生成器可以配置为为JsonMessage 类的实例生成序列化逻辑。 请注意,类名JsonContext 是任意的。 您可以为生成的源码使用任何您想要的类名。

using System.Text.Json.Serialization;

namespace Test
{
    [JsonSerializable(typeof(JsonMessage)]
    internal partial class JsonContext : JsonSerializerContext
    {
    }
}

我们通过JsonSerializerOptionsAttribute 定义了一组JsonSerializer 功能,这些功能由提供最佳序列化吞吐量的源码生成模式提供。 这些功能可以提前指定给源码生成器,以避免在运行时进行额外检查。 如果该属性未声明,运行时会使用默认的JsonSerializationOptions。

作为构建的一部分,源码生成器用以下内容扩充JsonContext 部分类:

internal partial class JsonContext : JsonSerializerContext
{
    public static JsonContext Default { get; }

    public JsonTypeInfo<JsonMessage> JsonMessage { get; }

    public JsonContext(JsonSerializerOptions options) { }

    public override JsonTypeInfo GetTypeInfo(Type type) => ...;
}

使用此模式的序列化程序调用可能类似于以下示例。此示例提供了可能的最佳性能。

using MemoryStream ms = new();
using Utf8JsonWriter writer = new(ms);

JsonContext.Default.JsonMessage.Serialize(writer, new JsonMessage { "Hello, world!" });
writer.Flush();

// Writer contains:
// {"Message":"Hello, world!"}

或者,您可以继续使用JsonSerializer,并且使用JsonContext.Default.JsonMessage 将生成代码的实例传递给它。

JsonSerializer.Serialize(jsonMessage, JsonContext.Default.JsonMessage);

这是一个类似的用法,但重载方法不同。

JsonSerializer.Serialize(jsonMessage, typeof(JsonMessage), JsonContext.Default);

这两个重载之间的区别在于,第一个重载使用类型化元数据实现 — JsonTypeInfo&lt;T&gt; — 第二个使用更通用的无类型化实现,该实现执行类型测试以确定上下文实例中是否存在类型化实现。因此,它有点慢(由于类型测试)。如果给定类型没有源码生成的实现,则序列化程序会抛出NotSupportedException。但是它不会回退到基于反射的实现(我们的设计明确了这一点)。

基于Utf8JsonWriter的最快、最优化的源码生成模式目前仅可用于序列化。我们会根据您的反馈决定是否在未来提供基于Utf8JsonReader的反序列化实现。

但是,源码生成器还提供类型元数据初始化逻辑,这也有助于反序列化。要使用预先生成的类型元数据反序列化JsonMessage 的实例,您可以执行以下操作:

JsonSerializer.Deserialize(json, JsonContext.Default.JsonMessage);

类似于上面的序列化,你也可以这样写:

JsonSerializer.Deserialize(json, typeof(JsonMessage), JsonContext.Default);

补充笔记

  • 可以通过派生的部分JsonSerializerContext 实例上的[JsonSerializable] 包含多种类型以用于源生成,不止支持一个。
  • 源生成器还支持嵌套对象和集合成员,而不仅仅是原始类型。

库:WebSocket 压缩

压缩对于通过网络传输的任何数据都很重要。WebSockets 现在启用压缩。我们使用了WebSockets 的permessage-deflate 扩展实现,RFC 7692。它允许使用DEFLATE 算法压缩WebSockets 消息有效负载。

此功能是用户对GitHub 上Networking 的最高要求之一。您可以通过API 审查1API 审查2跟随我们提供该API 的过程。

归功于Ivan Zlatanov。谢谢Ivan!

我们意识到将压缩与加密结合使用可能会导致攻击,例如CRIMEBREACH。这意味着不能在单个压缩上下文中将秘密与用户生成的数据一起发送,否则可以提取该秘密。为了让用户注意这些影响并帮助他们权衡风险,我们将API 重命名为DangerousDeflateOptions。我们还添加了对特定消息关闭压缩的功能,因此如果用户想要发送秘密,他们可以在不压缩的情况下安全地发送。

Ivan还进行了一项后续工作,当禁用压缩时,WebSocket 的内存占用减少了约27%。

从客户端启用压缩很容易,请参见下面的示例。但是,请记住服务器可以协商设置,例如请求较小的窗口,或完全拒绝压缩。

var cws = new ClientWebSocket();
cws.Options.DangerousDeflateOptions = new WebSocketDeflateOptions()
{
    ClientMaxWindowBits = 10,
    ServerMaxWindowBits = 10
};

最近还添加了对ASP.NET Core 的WebSocket 压缩支持。 它将包含在即将发布的预览中。

库:Socks 代理支持

SOCKS是一种代理服务器实现,可以处理任何TCP 或UDP 流量,使其成为一个非常通用的系统。 这是一个长期存在的社区请求,已添加到.NET 6 中

此更改增加了对Socks4、Socks4a 和Socks5 的支持。 例如,它允许通过SSH 测试外部连接或连接到Tor 网络

WebProxy 类现在接受socks 方案,如下例所示。

var handler = new HttpClientHandler
{
    Proxy = new WebProxy("socks5://127.0.0.1", 9050)
};
var httpClient = new HttpClient(handler);

归功于Huo Yaoyuan。谢谢Huo!

库:支持OpenTelemetry 指标

作为我们关注可观察性的一部分,我们一直在为最近的几个.NET 版本添加对OpenTelemetry 的支持。在.NET 6 中,我们添加了对OpenTelemetry Metrics API支持。通过添加对OpenTelemetry的支持,您的应用程序可以与其他OpenTelemetry 系统无缝互操作。

System.Diagnostics.MetricsOpenTelemetry Metrics API 规范的.NET 实现。Metrics API 是专门为处理原始测量而设计的,通常旨在高效且同时地生成这些测量的连续摘要。

API 包括可用于创建仪器对象(例如计数器)的Meter 类。API 公开了四个仪器类:Counter、Histogram、ObservableCounter 和ObservableGauge,以支持不同的指标场景。此外,API 公开MeterListener 类以允许侦听仪器记录的测量以进行聚合和分组。

OpenTelemetry .NET 实现将扩展为使用这些新API,这些API 增加了对Metrics 可观察性场景的支持。

库测量记录示例

Meter meter = new Meter("io.opentelemetry.contrib.mongodb", "v1.0");
    Counter<int> counter = meter.CreateCounter<int>("Requests");
    counter.Add(1);
    counter.Add(1, KeyValuePair.Create<string, object>("request", "read"));

听力示例

  MeterListener listener = new MeterListener();
    listener.InstrumentPublished = (instrument, meterListener) =>
    {
        if (instrument.Name == "Requests" && instrument.Meter.Name == "io.opentelemetry.contrib.mongodb")
        {
            meterListener.EnableMeasurementEvents(instrument, null);
        }
    };
    listener.SetMeasurementEventCallback<int>((instrument, measurement, tags, state) =>
    {
        Console.WriteLine($"Instrument: {instrument.Name} has recorded the measurement {measurement}");
    });
    listener.Start();

库:BigInteger 性能

从十进制和十六进制字符串解析BigIntegers 已得到改进。 我们看到了高达89% 的改进,如下图所示。

file

归功于Joseph Da Silva。 谢谢Joseph!

库:Vector&lt;T&gt; 现在支持nint 和nuint

Vector&lt;T&gt; 现在支持在C# 9 中添加的nint 和nuint 基元类型。例如,此更改应该可以更轻松地使用具有指针或平台相关长度的SIMD 指令。

库:支持OpenSSL 3

.NET 加密API 支持使用OpenSSL 3作为Linux 上的首选本机加密提供程序。 如果可用,.NET 6 将使用OpenSSL 3。 否则,它将使用OpenSSL 1.x。

库:添加支持ChaCha20/Poly1305 加密算法

ChaCha20Poly1305 类已添加到System.Security.Cryptography。 要使用ChaCha20/Poly1305算法,必须得到底层操作系统的支持。 静态IsSupported 属性可用于确定在给定上下文中是否支持该算法。

  • Linux:需要OpenSSL 1.1 或更高版本。
  • Windows:构建20142 或更高版本(目前需要开发"内部人员"频道)

感谢Kevin Jones对Linux 的支持。 谢谢Kevin!

互操作性:Objective-C 互操作性支持

该团队一直在添加Objective-C 支持,目标是为.NET 提供单一的Objective-C 互操作实现。 到目前为止,Objective-C互操作系统是围绕Mono 嵌入API 构建的,但我们认为这不是跨运行时共享的正确方法。 因此,我们创建了一个新的.NET API,它将支持单一的Objective-C 互操作体验,最终将在两个运行时上运行。

这个用于Objective-C 互操作的新API 为NSAutoreleasePool的两个运行时带来了即时支持,从而支持Cocoa 的引用计数内存管理系统。 您现在可以配置是否希望每个托管线程都具有隐式NSAutoreleasePool。 这使得在每个线程的基础上释放Cocoa 对象成为可能。

诊断(EventPipe/DiagnosticsServer)——MonoVM

从.NET 6 开始,MonoVM 中添加了许多诊断功能。这启用了托管EventSource/EventListener、EventPipe 和DiagnosticsServer 等功能。 它支持使用诊断工具,如dotnet-trace、dotnet-counters、dotnet-stacks,用于在移动设备(iOS/Android) 和桌面上运行的应用程序。

这些新功能开启了在PrefView/SpeedScope/Chromium、dotnet-trace等工具中分析MonoVM 生成的nettrace 文件的能力,或使用TraceEvent等库编写自定义解析器

我们将继续包含更多功能,主要侧重于SDK 集成并将更多本地运行时事件(Microsoft-Windows-DotNETRuntime) 适配到MonoVM 中,从而在nettrace 文件中启用更多事件。

现已具备以下功能:

  • 在MonoVM 和CoreCLR 之间共享本机EventPipe/DiagnosticsServer 库
  • 将TCP/IP 支持添加到DiagnosticsServer 并利用该配置构建MonoVM iOS/Android 运行时包。需要以支持移动平台。
  • BCL EventSources 在MonoVM 上运行,将事件发送到EventPipe。
  • System.Diagnostics.Tracing.RuntimeEventSource 发出的BCL 运行时计数器连接到MonoVM,可从dotnet-counters 等工具消耗。
  • 自定义事件源在MonoVM 上运行,将自定义事件发送到EventPipe,可通过dotnet-trace 等工具使用。
  • 自定义事件计数器在MonoVM 上运行,将自定义计数器事件发送到EventPipe,可通过dotnet-counters 等工具使用。
  • 示例分析器在MonoVM 上实现,将事件发送到EventPipe。开启了使用dotnet-trace 在MonoVM 上进行CPU 分析的能力。
  • dotnet-dsrouter 诊断工具的实现,允许使用现有的诊断工具,如dotnet-trace、dotnet-counters、dotnet-stack 以及在移动目标上运行的MonoVM,无需更改现有工具。dotnet-dsrouter 运行本地IPC 服务器,将所有流量从诊断工具路由到在模拟器/设备上的MonoVM 中运行的DiagnosticsServer。
  • 使用基于组件的架构在MonoVM 中实现EventPipe/DiagnosticsServer。
  • 基于文件会话的诊断环境的实现/扩展。

iOS CPU 采样(SpeedScope)

下图演示了在SpeedScope 中查看的iOS 启动CPU 采样会话的一部分。

file

Android CPU 采样(PerfView)

下图演示了在PerfView(无限睡眠中的主线程)中查看的Android CPU 采样。
file

运行时:CodeGen

RyuJIT 中进行了以下更改。

社区贡献

感谢@SingleAccretion的这些贡献。

动态PGO

file

JIT 循环优化

LSRA

在"分配寄存器"表中包含寄存器选择启发式https://github.com/dotnet/runtime/pull/52513新旧表的差异:

file

保持结构在注册

优化调试经验

file

SIMD

涉及SIMD 或HWIintrinsics 的某些方法的内联现在应该具有改进的代码生成和性能。 我们看到了高达95% 的改进

file

结束

就功能的广度和数量而言,.NET 6 预览版5 可能是迄今为止最大的预览版。 您可以使用源生成器和分析器查看Roslyn 功能对低级库功能的影响程度。 未来真的到来了。 我们现在拥有一个非常强大的编译器工具链,使我们能够生成高度优化和正确的代码,并为您自己的项目提供完全相同的体验。

现在是开始测试.NET 6 的好时机。现在我们根据您的反馈采取行动还为时过早。 很难想象,虽然我们要到2021 年11 月才会发布,但反馈窗口很快就会缩小到仅针对高严重性问题。 该团队提前进行了大约一年半的预览,并将很快转向主要关注质量问题。 如果可以,请尝试.NET 6。

感谢您成为.NET 开发人员。

posted @ 2021-08-02 23:13  MicrosoftReactor  阅读(2603)  评论(10编辑  收藏  举报