适用于 .NET 10 的 .NET 库的新增功能

本文介绍适用于 .NET 10 的 .NET 库中的新功能。

密码学

按 SHA-1 以外的指纹查找证书

通过指纹唯一查找证书是一个相当常见的操作,但 X509Certificate2Collection.Find(X509FindType, Object, Boolean) 方法(用于 FindByThumbprint 模式)仅搜索 SHA-1 指纹值。

使用 Find 此方法查找 SHA-2-256(“SHA256”)和 SHA-3-256 指纹存在一些风险,因为这些哈希算法的长度相同。

相反,.NET 10 引入了一 个新方法 ,该方法接受用于匹配的哈希算法的名称。

X509Certificate2Collection coll = store.Certificates.FindByThumbprint(HashAlgorithmName.SHA256, thumbprint);
Debug.Assert(coll.Count < 2, "Collection has too many matches, has SHA-2 been broken?");
return coll.SingleOrDefault();

在 ASCII/UTF-8 中查找 PEM 编码的数据

PEM 编码(最初是 隐私增强邮件,但现在在电子邮件外部广泛使用)被定义为“文本”,这意味着该 PemEncoding 类旨在运行 StringReadOnlySpan<char>。 类似于证书的东西通常是写在使用 ASCII(字符串)编码的文件中,这在 Linux 上尤其常见。 从历史上看,这意味着你需要打开文件并将字节转换为字符(或字符串),然后才能使用 PemEncoding

PemEncoding.FindUtf8(ReadOnlySpan<Byte>) 方法利用了 PEM 仅为 7 位 ASCII 字符定义的事实,并且 7 位 ASCII 与单字节 UTF-8 值完全重叠。 通过调用此新方法,可以跳过 UTF-8/ASCII 到字符转换并直接读取文件。

byte[] fileContents = File.ReadAllBytes(path);
-char[] text = Encoding.ASCII.GetString(fileContents);
-PemFields pemFields = PemEncoding.Find(text);
+PemFields pemFields = PemEncoding.FindUtf8(fileContents);

-byte[] contents = Base64.DecodeFromChars(text.AsSpan()[pemFields.Base64Data]);
+byte[] contents = Base64.DecodeFromUtf8(fileContents.AsSpan()[pemFields.Base64Data]);

用于 PKCS#12/PFX 导出的加密算法

新的 ExportPkcs12 方法使得 X509Certificate 允许调用方选择用于生成输出的加密和摘要算法:

如果想要更多控制权,可以使用重载来接受 PbeParameters

量子后加密 (PQC)

.NET 10 包括对三种新的非对称算法的支持:ML-KEM(FIPS 203)、ML-DSA(FIPS 204)和 SLH-DSA(FIPS 205)。 新类型包括:

因为它增加了很少的好处,所以这些新类型不会派生自 AsymmetricAlgorithmAsymmetricAlgorithm新类型不使用创建对象并导入密钥或生成新密钥的方法,而是使用静态方法生成或导入密钥:

using System;
using System.IO;
using System.Security.Cryptography;

private static bool ValidateMLDsaSignature(ReadOnlySpan<byte> data, ReadOnlySpan<byte> signature, string publicKeyPath)
{
    string publicKeyPem = File.ReadAllText(publicKeyPath);

    using (MLDsa key = MLDsa.ImportFromPem(publicKeyPem))
    {
        return key.VerifyData(data, signature);
    }
}

而不是设置对象属性并让键具体化,这些新类型的密钥生成将包含所有所需的选项。

using (MLKem key = MLKem.GenerateKey(MLKemAlgorithm.MLKem768))
{
    string publicKeyPem = key.ExportSubjectPublicKeyInfoPem();
    ...
}

这些算法都继续采用静态 IsSupported 属性模式,以指示当前系统上是否支持该算法。

.NET 10 包括 Windows 加密 API:下一代(CNG)对后量子加密(PQC)的支持,使这些算法在支持 PQC 的 Windows 系统上可用。 例如:

using System;
using System.IO;
using System.Security.Cryptography;

private static bool ValidateMLDsaSignature(ReadOnlySpan<byte> data, ReadOnlySpan<byte> signature, string publicKeyPath)
{
    string publicKeyPem = File.ReadAllText(publicKeyPath);

    using MLDsa key = MLDsa.ImportFromPem(publicKeyPem);
    return key.VerifyData(data, signature);
}

PQC 算法适用于系统加密库为 OpenSSL 3.5(或更高版本)或具有 PQC 支持的 Windows CNG 的系统。 该 MLKem 类型未标记为 [Experimental],但其一些方法已标记(并将在底层标准最终确定之前如此)。 在开发完成之前,MLDsaSlhDsaCompositeMLDsa 类被标记为 [Experimental] 诊断 SYSLIB5006

ML-DSA

此类 MLDsa 包括简化常见代码模式的易于使用的功能:

private static byte[] SignData(string privateKeyPath, ReadOnlySpan<byte> data)
{
    using (MLDsa signingKey = MLDsa.ImportFromPem(File.ReadAllBytes(privateKeyPath)))
    {
-       byte[] signature = new byte[signingKey.Algorithm.SignatureSizeInBytes];
-       signingKey.SignData(data, signature);
+       return signingKey.SignData(data);
-       return signature;
    }
}

此外,.NET 10 添加了对 HashML-DSA 的支持,它称为“PreHash”,以帮助将其与“纯”ML-DSA 区分开来。 当基础规范与对象标识符 (OID) 值交互时,此 [Experimental] 类型的 SignPreHash 和 VerifyPreHash 方法将 dotted-decimal OID 作为字符串。 随着使用 HashML-DSA 的更多方案变得定义良好,这可能会演变。

private static byte[] SignPreHashSha3_256(MLDsa signingKey, ReadOnlySpan<byte> data)
{
    const string Sha3_256Oid = "2.16.840.1.101.3.4.2.8";
    return signingKey.SignPreHash(SHA3_256.HashData(data), Sha3_256Oid);
}

从 RC 1 开始,ML-DSA 还支持从“外部”mu 值创建和验证的签名,这为高级加密方案提供了额外的灵活性:

private static byte[] SignWithExternalMu(MLDsa signingKey, ReadOnlySpan<byte> externalMu)
{
    return signingKey.SignMu(externalMu);
}

private static bool VerifyWithExternalMu(MLDsa verifyingKey, ReadOnlySpan<byte> externalMu, ReadOnlySpan<byte> signature)
{
    return verifyingKey.VerifyMu(externalMu, signature);
}

复合 ML-DSA

.NET 10 引入了支持 ietf-lamps-pq-composite-sigs 的新类型(从 .NET 10 GA 的草稿 8 开始),包括 CompositeMLDsaCompositeMLDsaAlgorithm 类型,以及 RSA 变体基元方法的实现。

var algorithm = CompositeMLDsaAlgorithm.MLDsa65WithRSA4096Pss;
using var privateKey = CompositeMLDsa.GenerateKey(algorithm);

byte[] data = [42];
byte[] signature = privateKey.SignData(data);

using var publicKey = CompositeMLDsa.ImportCompositeMLDsaPublicKey(algorithm, privateKey.ExportCompositeMLDsaPublicKey());
Console.WriteLine(publicKey.VerifyData(data, signature)); // True

signature[0] ^= 1; // Tamper with signature
Console.WriteLine(publicKey.VerifyData(data, signature)); // False

AES KeyWrap 与填充 (IETF RFC 5649)

AES-KWP 是一种算法,在加密消息语法(CMS)EnvelopedData 等构造中偶尔使用,其中内容加密一次,但解密密钥需要分发到多个参与方,每一个采用不同的机密形式。

.NET 现在通过类上的 Aes 实例方法支持 AES-KWP 算法:

private static byte[] DecryptContent(ReadOnlySpan<byte> kek, ReadOnlySpan<byte> encryptedKey, ReadOnlySpan<byte> ciphertext)
{
    using (Aes aes = Aes.Create())
    {
        aes.SetKey(kek);

        Span<byte> dek = stackalloc byte[256 / 8];
        int length = aes.DecryptKeyWrapPadded(encryptedKey, dek);

        aes.SetKey(dek.Slice(0, length));
        return aes.DecryptCbc(ciphertext);
    }
}

全球化和日期/时间

ISOWeek for DateOnly 类型的新方法重载

最初设计的 ISOWeek 类主要是为了专门使用 DateTime,因为它是在 DateOnly 类型出现之前引入的。 既然DateOnly现在可用了,那么ISOWeek也有必要支持它。 以下重载是新引入的:

用于字符串比较的数字排序

数值字符串比较是一项高度请求的功能,用于在数字上而不是按词法比较字符串。 例如,2小于10,因此"2"应按数字顺序在"10"前显示。 同样,"2""02" 在数值上是相等的。 使用新 NumericOrdering 选项,现在可以执行以下类型的比较:

StringComparer numericStringComparer = StringComparer.Create(CultureInfo.CurrentCulture, CompareOptions.NumericOrdering);

Console.WriteLine(numericStringComparer.Equals("02", "2"));
// Output: True

foreach (string os in new[] { "Windows 8", "Windows 10", "Windows 11" }.Order(numericStringComparer))
{
    Console.WriteLine(os);
}

// Output:
// Windows 8
// Windows 10
// Windows 11

HashSet<string> set = new HashSet<string>(numericStringComparer) { "007" };
Console.WriteLine(set.Contains("7"));
// Output: True

此选项不适用于以下基于索引的字符串操作:IndexOfLastIndexOfStartsWithEndsWithIsPrefixIsSuffix

含单个参数的新 TimeSpan.FromMilliseconds 重载

之前引入了 TimeSpan.FromMilliseconds(Int64, Int64) 方法,但未添加采用单个参数的重载。

尽管这是由于第二个参数是可选的,但在 LINQ 表达式中使用时会导致编译错误,例如:

Expression<Action> a = () => TimeSpan.FromMilliseconds(1000);

出现此问题的原因是 LINQ 表达式无法处理可选参数。 为了解决此问题,.NET 10 引入了 新的重载 采用单个参数。 它还修改 现有方法 ,使第二个参数是必需的。

字符串

使用字符范围的字符串规范化 API

Unicode 字符串规范化已长时间受支持,但现有 API 仅适用于字符串类型。 这意味着,具有以不同形式存储的数据(如字符数组或范围)的调用方必须分配一个新字符串才能使用这些 API。 此外,返回规范化字符串的 API 始终分配一个新字符串来表示规范化输出。

.NET 10 引入了处理字符范围的新 API,这些 API 将规范化扩展到字符串类型之外,并帮助避免不必要的分配:

对十六进制字符串转换的 UTF-8 支持

.NET 10 在 Convert 类中增加了对十六进制字符串转换操作的 UTF-8 支持。 这些新方法提供了在 UTF-8 字节序列和十六进制表示形式之间转换的有效方法,而无需中间字符串分配:

这些方法模仿了可与stringReadOnlySpan<char>一起使用的现有重载,但为了在您已处理 UTF-8 数据的情况下提高性能,它们直接作用于 UTF-8 编码字节。

Collections

TryAdd 的附加 TryGetValueOrderedDictionary<TKey, TValue> 重载

OrderedDictionary<TKey,TValue> 提供 TryAddTryGetValue 以进行添加和检索,就像任何其他 IDictionary<TKey, TValue> 实现一样。 但是,在某些情况下,你可能想要执行更多操作,因此会添加新的重载,以便返回一个条目的索引。

可以使用此索引与GetAtSetAt一起快速访问条目。 新 TryAdd 重载的示例用法是在有序字典中添加或更新键值对:

// Try to add a new key with value 1.
if (!orderedDictionary.TryAdd(key, 1, out int index))
{
    // Key was present, so increment the existing value instead.
    int value = orderedDictionary.GetAt(index).Value;
    orderedDictionary.SetAt(index, value + 1);
}

此新 API 已在 JsonObject 中使用,并将更新属性的性能提高了 10-20%。

序列化

允许在 JsonSourceGenerationOptions 中指定 ReferenceHandler

源生成器用于 JSON 序列化时,会在序列化或反序列化循环时引发生成的上下文。 现在,可以在 ReferenceHandler 中指定 JsonSourceGenerationOptionsAttribute 来自定义此行为。 以下是一个使用 JsonKnownReferenceHandler.Preserve 的例子:

public static void MakeSelfRef()
{
    SelfReference selfRef = new SelfReference();
    selfRef.Me = selfRef;

    Console.WriteLine(JsonSerializer.Serialize(selfRef, ContextWithPreserveReference.Default.SelfReference));
    // Output: {"$id":"1","Me":{"$ref":"1"}}
}

[JsonSourceGenerationOptions(ReferenceHandler = JsonKnownReferenceHandler.Preserve)]
[JsonSerializable(typeof(SelfReference))]
internal partial class ContextWithPreserveReference : JsonSerializerContext
{
}

internal class SelfReference
{
    public SelfReference Me { get; set; } = null!;
}

禁止重复 JSON 属性的选项

JSON 规范不指定在反序列化 JSON 有效负载时如何处理重复属性。 这可能会导致意外的结果和安全漏洞。 .NET 10 引入了 JsonSerializerOptions.AllowDuplicateProperties 禁止重复 JSON 属性的选项:

string json = """{ "Value": 1, "Value": -1 }""";
Console.WriteLine(JsonSerializer.Deserialize<MyRecord>(json).Value); // -1

JsonSerializerOptions options = new() { AllowDuplicateProperties = false };
JsonSerializer.Deserialize<MyRecord>(json, options);                // throws JsonException
JsonSerializer.Deserialize<JsonObject>(json, options);              // throws JsonException
JsonSerializer.Deserialize<Dictionary<string, int>>(json, options); // throws JsonException

JsonDocumentOptions docOptions = new() { AllowDuplicateProperties = false };
JsonDocument.Parse(json, docOptions);   // throws JsonException

record MyRecord(int Value);

通过检查反序列化期间同一属性是否被多次赋值来检测重复项。因此,该机制在处理大小写敏感度和命名策略等其他选项时,也能正常工作。

严格的 JSON 序列化选项

JSON 序列化程序接受许多选项来自定义序列化和反序列化,但对于某些应用程序而言,默认值可能过于宽松。 .NET 10 添加了遵循最佳做法的新 JsonSerializerOptions.Strict 预设,包括以下选项:

这些选项与 JsonSerializerOptions.Default 兼容读取 - 通过 JsonSerializerOptions.Default 序列化的对象可以使用 JsonSerializerOptions.Strict 进行反序列化。

有关 JSON 序列化的详细信息,请参阅 System.Text.Json 概述

对 JSON 序列化程序的 PipeReader 支持

JsonSerializer.Deserialize 现在支持 PipeReader,补充现有 PipeWriter 支持。 以前,从 PipeReader 所需的反序列化将其转换为 a Stream,但新的重载通过将直接集成到 PipeReader 序列化程序来消除该步骤。 作为奖金,不必从你已经持有的东西中转换可以产生一些效率的好处。

这显示了基本用法:

using System;
using System.IO.Pipelines;
using System.Text.Json;
using System.Threading.Tasks;

var pipe = new Pipe();

// Serialize to writer
await JsonSerializer.SerializeAsync(pipe.Writer, new Person("Alice"));
await pipe.Writer.CompleteAsync();

// Deserialize from reader
var result = await JsonSerializer.DeserializeAsync<Person>(pipe.Reader);
await pipe.Reader.CompleteAsync();

Console.WriteLine($"Your name is {result.Name}.");
// Output: Your name is Alice.

record Person(string Name);

下面是生成者的示例,它以区块生成令牌,以及接收和显示令牌的使用者:

using System;
using System.Collections.Generic;
using System.IO.Pipelines;
using System.Text.Json;
using System.Threading.Tasks;

var pipe = new Pipe();

// Producer writes to the pipe in chunks.
var producerTask = Task.Run(async () =>
{
    async static IAsyncEnumerable<Chunk> GenerateResponse()
    {
        yield return new Chunk("The quick brown fox", DateTime.Now);
        await Task.Delay(500);
        yield return new Chunk(" jumps over", DateTime.Now);
        await Task.Delay(500);
        yield return new Chunk(" the lazy dog.", DateTime.Now);
    }

    await JsonSerializer.SerializeAsync<IAsyncEnumerable<Chunk>>(pipe.Writer, GenerateResponse());
    await pipe.Writer.CompleteAsync();
});

// Consumer reads from the pipe and outputs to console.
var consumerTask = Task.Run(async () =>
{
    var thinkingString = "...";
    var clearThinkingString = new string("\b\b\b");
    var lastTimestamp = DateTime.MinValue;

    // Read response to end.
    Console.Write(thinkingString);
    await foreach (var chunk in JsonSerializer.DeserializeAsyncEnumerable<Chunk>(pipe.Reader))
    {
        Console.Write(clearThinkingString);
        Console.Write(chunk.Message);
        Console.Write(thinkingString);
        lastTimestamp = DateTime.Now;
    }

    Console.Write(clearThinkingString);
    Console.WriteLine($" Last message sent at {lastTimestamp}.");

    await pipe.Reader.CompleteAsync();
});

await producerTask;
await consumerTask;

record Chunk(string Message, DateTime Timestamp);

所有这些内容都序列化为 JSON( Pipe 此处格式化以便可读):

[
    {
        "Message": "The quick brown fox",
        "Timestamp": "2025-08-01T18:37:27.2930151-07:00"
    },
    {
        "Message": " jumps over",
        "Timestamp": "2025-08-01T18:37:27.8594502-07:00"
    },
    {
        "Message": " the lazy dog.",
        "Timestamp": "2025-08-01T18:37:28.3753669-07:00"
    }
]

System.Numerics

更多左侧矩阵变换方法

.NET 10 添加了其余用于为广告牌和受限广告牌矩阵创建左侧变换矩阵的 API。 当使用左手坐标系时,可以像使用现有右手坐标系那样使用这些方法,例如 CreateBillboard(Vector3, Vector3, Vector3, Vector3)

Tensor 增强功能

命名空间 System.Numerics.Tensors 现在包括一个非泛型接口, IReadOnlyTensor用于访问 LengthsStrides。 切片操作现在不再复制数据,从而提高了性能。 此外,当性能不是关键时,您可以通过将其装箱到 object 来访问非泛型数据。

张量 API 现在稳定,不再标记为实验性。 尽管 API 仍需要引用 System.Numerics.Tensors NuGet 包,但已对 .NET 10 版本进行了全面审查和最终确定。 当基础类型 T 支持该作时,这些类型利用 C# 14 扩展运算符来提供算术运算。 例如,如果 T 实现相关的 泛型数学 接口,例如 IAdditionOperators<TSelf, TOther, TResult>INumber<TSelf>,该操作是支持的。 例如, tensor + tensor 可用于 a Tensor<int>,但不适用于 a Tensor<bool>.

选项验证

ValidationContext 的新 AOT 安全构造函数

在选项验证期间使用的 ValidationContext 类包含一个新构造函数重载,该重载显式地接受 displayName 参数:

ValidationContext(Object, String, IServiceProvider, IDictionary<Object,Object>)

显示名称可确保 AOT 安全,并在本机生成中启用其使用,而不会出现警告。

诊断

ActivitySourceMeter中的遥测架构URL的支持

ActivitySourceMeter 现在在构造过程中支持指定遥测架构 URL,这与 OpenTelemetry 规范保持一致。 遥测架构可确保跟踪和指标数据的一致性和兼容性。 此外,.NET 10 引入了ActivitySourceOptions,它简化了使用多个配置选项(包括ActivitySource)创建实例。

新 API 包括:

Activity 类通过跟踪跨服务或组件的作流来启用分布式跟踪。 .NET 支持通过 Microsoft-Diagnostics-DiagnosticSource 事件源提供程序将此跟踪数据进行进程外序列化。 Activity 可以包含其他元数据,例如 ActivityLinkActivityEvent。 .NET 10 增加了对这些链接和事件的序列化支持,因此,外部进程跟踪数据现在包含该信息。 例如:

Events->"[(TestEvent1,​2025-03-27T23:34:10.6225721+00:00,​[E11:​EV1,​E12:​EV2]),​(TestEvent2,​2025-03-27T23:34:11.6276895+00:00,​[E21:​EV21,​E22:​EV22])]"
Links->"[(19b6e8ea216cb2ba36dd5d957e126d9f,​98f7abcb3418f217,​Recorded,​null,​false,​[alk1:​alv1,​alk2:​alv2]),​(2d409549aadfdbdf5d1892584a5f2ab2,​4f3526086a350f50,​None,​null,​false)]"

速率限制跟踪采样支持

当分布式跟踪数据通过 Microsoft-Diagnostics-DiagnosticSource 事件源提供程序在进程外序列化时,可以发出所有记录的活动,或者可以根据跟踪比率应用采样。

名为 “速率限制采样 ”的新采样选项限制每秒序列化的 根活动 数。 这有助于更准确地控制数据量。

进程外跟踪数据聚合器可以通过在FilterAndPayloadSpecs中指定选项来启用和配置此采样。 例如,以下设置将所有实例的序列化限制为每秒 ActivitySource 100 个根活动:

[AS]*/-ParentRateLimitingSampler(100)

ZIP 文件

ZipArchive 性能和内存改进

.NET 10 提高了 ZipArchive 的性能和内存使用率。

首先,优化了在 ZipArchive 模式下将条目写入 Update 的方式。 以前,所有 ZipArchiveEntry 实例都加载到内存中并重写,这可能会导致内存使用率和性能瓶颈较高。 优化可减少内存使用率,并避免需要将所有条目加载到内存中,从而提高性能。

其次,条目的 ZipArchive 提取现已并行化,并且内部数据结构经过优化,以便更好地使用内存。 这些改进解决了与性能瓶颈和高内存使用率相关的问题,提高了 ZipArchive 效率和速度更快,尤其是在处理大型存档时。

新异步 ZIP API

.NET 10 引入了新的异步 API,使读取或写入 ZIP 文件时执行非阻塞作更容易。 社区高度要求此功能。

async 方法可用于提取、创建和更新 ZIP 存档。 这些方法使开发人员能够有效地处理大型文件并提高应用程序响应能力,尤其是在涉及 I/O 绑定作的方案中。 这些方法包括:

有关使用这些 API 的示例,请参阅 预览版 4 博客文章

GZipStream 中连接流的性能改进

社区的一项贡献改进了 GZipStream 在处理串联 GZip 数据流时的性能。 以前,每个新流段都会处置并重新分配内部 ZLibStreamHandle,从而产生额外的内存分配和初始化开销。 通过此更改,该句柄现已重置并重复使用,以减少托管和非托管内存分配并提高执行时间。 处理大量小型数据流时,会看到影响最大(约快 35%)。 这项更改:

  • 消除每个串联流大约 64-80 字节内存的重复分配,同时节省额外的非托管内存。
  • 将每个连接流的执行时间减少约 400 纳秒。

Windows 进程管理

在新进程组中启动 Windows 进程

对于 Windows,你现在可以用于 ProcessStartInfo.CreateNewProcessGroup 在单独的进程组中启动进程。 这样,就可以将隔离的信号发送到子进程,否则无需正确处理即可关闭父进程。 发送信号十分方便,以避免强制终止。

using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;

class Program
{
    static void Main(string[] args)
    {
        bool isChildProcess = args.Length > 0 && args[0] == "child";
        if (!isChildProcess)
        {
            var psi = new ProcessStartInfo
            {
                FileName = Environment.ProcessPath,
                Arguments = "child",
                CreateNewProcessGroup = true,
            };

            using Process process = Process.Start(psi)!;
            Thread.Sleep(5_000);

            GenerateConsoleCtrlEvent(CTRL_C_EVENT, (uint)process.Id);
            process.WaitForExit();

            Console.WriteLine("Child process terminated gracefully, continue with the parent process logic if needed.");
        }
        else
        {
            // If you need to send a CTRL+C, the child process needs to re-enable CTRL+C handling, if you own the code, you can call SetConsoleCtrlHandler(NULL, FALSE).
            // see https://learn.microsoft.com/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessw#remarks
            SetConsoleCtrlHandler((IntPtr)null, false);

            Console.WriteLine("Greetings from the child process!  I need to be gracefully terminated, send me a signal!");

            bool stop = false;

            var registration = PosixSignalRegistration.Create(PosixSignal.SIGINT, ctx =>
            {
                stop = true;
                ctx.Cancel = true;
                Console.WriteLine("Received CTRL+C, stopping...");
            });

            StreamWriter sw = File.AppendText("log.txt");
            int i = 0;
            while (!stop)
            {
                Thread.Sleep(1000);
                sw.WriteLine($"{++i}");
                Console.WriteLine($"Logging {i}...");
            }

            // Clean up
            sw.Dispose();
            registration.Dispose();

            Console.WriteLine("Thanks for not killing me!");
        }
    }

    private const int CTRL_C_EVENT = 0;
    private const int CTRL_BREAK_EVENT = 1;

    [DllImport("kernel32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool SetConsoleCtrlHandler(IntPtr handler, [MarshalAs(UnmanagedType.Bool)] bool Add);

    [DllImport("kernel32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool GenerateConsoleCtrlEvent(uint dwCtrlEvent, uint dwProcessGroupId);
}

WebSocket 增强功能

WebSocketStream

.NET 10 引入了 WebSocketStream一个新的 API,旨在简化 .NET 中一些最常见的(以前很繁琐)WebSocket 的方案。

传统 WebSocket API 级别较低,需要大量的样本:处理缓冲和框架、重建消息、管理编码/解码以及编写自定义包装器以与流、通道或其他传输抽象集成。 这些复杂性使得很难将 WebSocket 用作传输,尤其是对于具有流式处理或基于文本的协议或事件驱动的处理程序的应用。

WebSocketStream 通过提供 Stream基于 WebSocket 的抽象来解决这些难题。 这样就可以与现有 API 无缝集成,以便读取、写入和分析数据,无论是二进制数据还是文本,并减少了手动管道的需求。

WebSocketStream 为常见的 WebSocket 消耗和生产模式启用高级熟悉的 API。 这些 API 可减少摩擦,并使高级方案更易于实现。

常见使用模式

下面是简化 WebSocketStream 典型 WebSocket 工作流的一些示例:

流式处理文本协议(例如 STOMP)
using System.IO;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;

// Streaming text protocol (for example, STOMP).
using Stream transportStream = WebSocketStream.Create(
    connectedWebSocket, 
    WebSocketMessageType.Text,
    ownsWebSocket: true);
// Integration with Stream-based APIs.
// Don't close the stream, as it's also used for writing.
using var transportReader = new StreamReader(transportStream, leaveOpen: true); 
var line = await transportReader.ReadLineAsync(cancellationToken); // Automatic UTF-8 and new line handling.
transportStream.Dispose(); // Automatic closing handshake handling on `Dispose`.
流式处理二进制协议(例如 AMQP)
using System;
using System.IO;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;

// Streaming binary protocol (for example, AMQP).
Stream transportStream = WebSocketStream.Create(
    connectedWebSocket,
    WebSocketMessageType.Binary,
    closeTimeout: TimeSpan.FromSeconds(10));
await message.SerializeToStreamAsync(transportStream, cancellationToken);
var receivePayload = new byte[payloadLength];
await transportStream.ReadExactlyAsync(receivePayload, cancellationToken);
transportStream.Dispose();
// `Dispose` automatically handles closing handshake.
以流形式读取单个消息(例如 JSON 反序列化)
using System.IO;
using System.Net.WebSockets;
using System.Text.Json;

// Reading a single message as a stream (for example, JSON deserialization).
using Stream messageStream = WebSocketStream.CreateReadableMessageStream(connectedWebSocket, WebSocketMessageType.Text);
// JsonSerializer.DeserializeAsync reads until the end of stream.
var appMessage = await JsonSerializer.DeserializeAsync<AppMessage>(messageStream);
将单个消息编写为流(例如,二进制序列化)
using System;
using System.IO;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;

// Writing a single message as a stream (for example, binary serialization).
public async Task SendMessageAsync(AppMessage message, CancellationToken cancellationToken)
{
    using Stream messageStream = WebSocketStream.CreateWritableMessageStream(_connectedWebSocket, WebSocketMessageType.Binary);
    foreach (ReadOnlyMemory<byte> chunk in message.SplitToChunks())
    {
        await messageStream.WriteAsync(chunk, cancellationToken);
    }
} // EOM sent on messageStream.Dispose().

TLS 增强功能

适用于 macOS 的 TLS 1.3 (客户端)

.NET 10 通过将 Apple 的 Network.framework 集成到 SslStreamHttpClientmacOS 中添加客户端 TLS 1.3 支持。 从历史上看,macOS 使用了不支持 TLS 1.3 的安全传输;选择加入 Network.framework 会启用 TLS 1.3。

范围和行为

  • 仅 macOS,此版本中的客户端。
  • 选择加入。 除非启用,否则现有应用将继续使用当前堆栈。
  • 启用后,较旧的 TLS 版本(TLS 1.0 和 1.1)可能不再通过 Network.framework 可用。

如何启用

在代码中使用 AppContext 开关:

// Opt in to Network.framework-backed TLS on Apple platforms.
AppContext.SetSwitch("System.Net.Security.UseNetworkFramework", true);

using var client = new HttpClient();
var html = await client.GetStringAsync("https://example.com");

或使用环境变量:

# Opt-in via environment variable (set for the process or machine as appropriate)
DOTNET_SYSTEM_NET_SECURITY_USENETWORKFRAMEWORK=1
# or
DOTNET_SYSTEM_NET_SECURITY_USENETWORKFRAMEWORK=true

注释

  • TLS 1.3 适用于SslStream和基于它的 API(例如)。 HttpClient/HttpMessageHandler
  • 密码套件通过 Network.framework 由 macOS 控制。
  • 启用 Network.framework 时,基础流行为可能会有所不同(例如缓冲、读/写完成、取消语义)。
  • 对于零字节读取,语义可能有所不同。 避免依赖零长度读取来检测数据可用性。
  • Network.framework 可能会拒绝某些国际化域名 (IDN) 主机名。 首选 ASCII/Punycode (A-label) 主机名,或根据 macOS/Network.framework 约束验证名称。
  • 如果应用依赖于特定的 SslStream 边缘情况行为,请在 Network.framework 下对其进行验证。