|
2 | 2 |
|
3 | 3 | Here's a summary of what's new in .NET Libraries in this preview release:
|
4 | 4 |
|
5 |
| -- [Feature](#feature) |
| 5 | +- [Launch Windows processes in new process group](#launch-windows-processes-in-new-process-group) |
| 6 | +- [AES KeyWrap with Padding (IETF RFC 5649)](#aes-keywrap-with-padding-ietf-rfc-5649) |
| 7 | +- Post-Quantum Cryptography Updates |
| 8 | + - [ML-DSA](#ml-dsa) |
| 9 | + - [Composite ML-DSA](#composite-ml-dsa) |
| 10 | +- [PipeReader support for JSON serializer](#pipereader-support-for-json-serializer) |
| 11 | +- Networking |
| 12 | + - [WebSocketStream](#websocketstream) |
| 13 | + - [TLS 1.3 for macOS (client)](#tls-13-for-macos-client) |
6 | 14 |
|
7 | 15 | .NET Libraries updates in .NET 10:
|
8 | 16 |
|
9 | 17 | - [What's new in .NET 10](https://learn.microsoft.com/dotnet/core/whats-new/dotnet-10/overview) documentation
|
10 | 18 |
|
11 |
| -## Feature |
| 19 | +## Launch Windows processes in new process group |
12 | 20 |
|
13 |
| -Something about the feature |
| 21 | +For Windows, you can now use `ProcessStartInfo.CreateNewProcessGroup` to launch a process in a separate PG. This allows you to send isolated signals to child processes which could otherwise take down the parent without proper handling. Sending signals is convenient to avoid forceful termination. |
| 22 | + |
| 23 | +```csharp |
| 24 | +using System; |
| 25 | +using System.Diagnostics; |
| 26 | +using System.IO; |
| 27 | +using System.Runtime.InteropServices; |
| 28 | +using System.Threading; |
| 29 | + |
| 30 | +class Program |
| 31 | +{ |
| 32 | + static void Main(string[] args) |
| 33 | + { |
| 34 | + bool isChildProcess = args.Length > 0 && args[0] == "child"; |
| 35 | + if (!isChildProcess) |
| 36 | + { |
| 37 | + var psi = new ProcessStartInfo |
| 38 | + { |
| 39 | + FileName = Environment.ProcessPath, |
| 40 | + Arguments = "child", |
| 41 | + CreateNewProcessGroup = true, |
| 42 | + }; |
| 43 | + |
| 44 | + using Process process = Process.Start(psi)!; |
| 45 | + Thread.Sleep(5_000); |
| 46 | + |
| 47 | + GenerateConsoleCtrlEvent(CTRL_C_EVENT, (uint)process.Id); |
| 48 | + process.WaitForExit(); |
| 49 | + |
| 50 | + Console.WriteLine("Child process terminated gracefully, continue with the parent process logic if needed."); |
| 51 | + } |
| 52 | + else |
| 53 | + { |
| 54 | + // 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). |
| 55 | + // see https://learn.microsoft.com/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessw#remarks |
| 56 | + SetConsoleCtrlHandler((IntPtr)null, false); |
| 57 | + |
| 58 | + Console.WriteLine("Greetings from the child process! I need to be gracefully terminated, send me a signal!"); |
| 59 | + |
| 60 | + bool stop = false; |
| 61 | + |
| 62 | + var registration = PosixSignalRegistration.Create(PosixSignal.SIGINT, ctx => |
| 63 | + { |
| 64 | + stop = true; |
| 65 | + ctx.Cancel = true; |
| 66 | + Console.WriteLine("Received CTRL+C, stopping..."); |
| 67 | + }); |
| 68 | + |
| 69 | + StreamWriter sw = File.AppendText("log.txt"); |
| 70 | + int i = 0; |
| 71 | + while (!stop) |
| 72 | + { |
| 73 | + Thread.Sleep(1000); |
| 74 | + sw.WriteLine($"{++i}"); |
| 75 | + Console.WriteLine($"Logging {i}..."); |
| 76 | + } |
| 77 | + |
| 78 | + // Clean up |
| 79 | + sw.Dispose(); |
| 80 | + registration.Dispose(); |
| 81 | + |
| 82 | + Console.WriteLine("Thanks for not killing me!"); |
| 83 | + } |
| 84 | + } |
| 85 | + |
| 86 | + private const int CTRL_C_EVENT = 0; |
| 87 | + private const int CTRL_BREAK_EVENT = 1; |
| 88 | + |
| 89 | + [DllImport("kernel32.dll", SetLastError = true)] |
| 90 | + [return: MarshalAs(UnmanagedType.Bool)] |
| 91 | + private static extern bool SetConsoleCtrlHandler(IntPtr handler, [MarshalAs(UnmanagedType.Bool)] bool Add); |
| 92 | + |
| 93 | + [DllImport("kernel32.dll", SetLastError = true)] |
| 94 | + [return: MarshalAs(UnmanagedType.Bool)] |
| 95 | + private static extern bool GenerateConsoleCtrlEvent(uint dwCtrlEvent, uint dwProcessGroupId); |
| 96 | +} |
| 97 | +``` |
| 98 | + |
| 99 | +## AES KeyWrap with Padding (IETF RFC 5649) |
| 100 | + |
| 101 | +AES-KWP is an algorithm that is occasionally used in constructions like Cryptographic Message Syntax (CMS) EnvelopedData, |
| 102 | +where content is encrypted once, but the decryption key needs to be distributed to multiple parties, each one in a distinct |
| 103 | +secret form. |
| 104 | + |
| 105 | +.NET now supports the AES-KWP algorithm via instance methods on the `System.Security.Cryptography.Aes` class: |
| 106 | + |
| 107 | +```csharp |
| 108 | +private static byte[] DecryptContent(ReadOnlySpan<byte> kek, ReadOnlySpan<byte> encryptedKey, ReadOnlySpan<byte> ciphertext) |
| 109 | +{ |
| 110 | + using (Aes aes = Aes.Create()) |
| 111 | + { |
| 112 | + aes.SetKey(kek); |
| 113 | + |
| 114 | + Span<byte> dek = stackalloc byte[256 / 8]; |
| 115 | + int length = aes.DecryptKeyWrapPadded(encryptedKey, dek); |
| 116 | + |
| 117 | + aes.SetKey(dek.Slice(0, length)); |
| 118 | + return aes.DecryptCbc(ciphertext); |
| 119 | + } |
| 120 | +} |
| 121 | +``` |
| 122 | + |
| 123 | +## Post-Quantum Cryptography Updates |
| 124 | + |
| 125 | +### ML-DSA |
| 126 | + |
| 127 | +The `System.Security.Cryptography.MLDsa` class gained ease-of-use updates in this release, allowing some common code patterns to be simplified: |
| 128 | + |
| 129 | +```diff |
| 130 | +private static byte[] SignData(string privateKeyPath, ReadOnlySpan<byte> data) |
| 131 | +{ |
| 132 | + using (MLDsa signingKey = MLDsa.ImportFromPem(File.ReadAllBytes(privateKeyPath))) |
| 133 | + { |
| 134 | +- byte[] signature = new byte[signingKey.Algorithm.SignatureSizeInBytes]; |
| 135 | +- signingKey.SignData(data, signature); |
| 136 | ++ return signingKey.SignData(data); |
| 137 | +- return signature; |
| 138 | + } |
| 139 | +} |
| 140 | +``` |
| 141 | + |
| 142 | +Additionally, this release added support for HashML-DSA, which we call "PreHash" to help distinguish it from "pure" ML-DSA. |
| 143 | +As the underlying specification interacts with the Object Identifier (OID) value, the SignPreHash and VerifyPreHash methods |
| 144 | +on this `[Experimental]` type take the dotted-decimal OID as a string. |
| 145 | +This may evolve as more scenarios using HashML-DSA become well-defined. |
| 146 | + |
| 147 | +```C# |
| 148 | +private static byte[] SignPreHashSha3_256(MLDsa signingKey, ReadOnlySpan<byte> data) |
| 149 | +{ |
| 150 | + const string Sha3_256Oid = "2.16.840.1.101.3.4.2.8"; |
| 151 | + return signingKey.SignPreHash(SHA3_256.HashData(data), Sha3_256Oid); |
| 152 | +} |
| 153 | +``` |
| 154 | + |
| 155 | +### Composite ML-DSA |
| 156 | + |
| 157 | +This release also introduces new types to support ietf-lamps-pq-composite-sigs (currently at draft 7), |
| 158 | +and an implementation of the primitive methods for RSA variants. |
| 159 | + |
| 160 | +```cs |
| 161 | +var algorithm = CompositeMLDsaAlgorithm.MLDsa65WithRSA4096Pss; |
| 162 | +using var privateKey = CompositeMLDsa.GenerateKey(algorithm); |
| 163 | + |
| 164 | +byte[] data = [42]; |
| 165 | +byte[] signature = privateKey.SignData(data); |
| 166 | + |
| 167 | +using var publicKey = CompositeMLDsa.ImportCompositeMLDsaPublicKey(algorithm, privateKey.ExportCompositeMLDsaPublicKey()); |
| 168 | +Console.WriteLine(publicKey.VerifyData(data, signature)); // True |
| 169 | +
|
| 170 | +signature[0] ^= 1; // Tamper with signature |
| 171 | +Console.WriteLine(publicKey.VerifyData(data, signature)); // False |
| 172 | +``` |
| 173 | + |
| 174 | +## PipeReader support for JSON serializer |
| 175 | + |
| 176 | +`JsonSerializer.Deserialize` now supports `PipeReader`, complementing the existing `PipeWriter` support. Previously, deserializing from a `PipeReader` required converting it to a `Stream`, but the new overloads eliminate that step by integrating `PipeReader` directly into the serializer. As a bonus, not having to convert from what you're already holding can yield some efficiency benefits. |
| 177 | + |
| 178 | +This shows the basic usage: |
| 179 | + |
| 180 | +```cs |
| 181 | +var pipe = new Pipe(); |
| 182 | + |
| 183 | +// Serialize to writer |
| 184 | +await JsonSerializer.SerializeAsync(pipe.Writer, new Person("Alice")); |
| 185 | +await pipe.Writer.CompleteAsync(); |
| 186 | + |
| 187 | +// Deserialize from reader |
| 188 | +var result = await JsonSerializer.DeserializeAsync<Person>(pipe.Reader); |
| 189 | +await pipe.Reader.CompleteAsync(); |
| 190 | + |
| 191 | +Console.WriteLine($"Your name is {result.Name}."); |
| 192 | +// Output: Your name is Alice. |
| 193 | +
|
| 194 | +record Person(string Name); |
| 195 | +``` |
| 196 | + |
| 197 | +Here is an example of a producer that produces tokens in chunks and a consumer that receives and displays them. |
| 198 | + |
| 199 | +```cs |
| 200 | +var pipe = new Pipe(); |
| 201 | + |
| 202 | +// Producer writes to the pipe in chunks |
| 203 | +var producerTask = Task.Run(async () => |
| 204 | +{ |
| 205 | + async static IAsyncEnumerable<Chunk> GenerateResponse() |
| 206 | + { |
| 207 | + yield return new Chunk("The quick brown fox", DateTime.Now); |
| 208 | + await Task.Delay(500); |
| 209 | + yield return new Chunk(" jumps over", DateTime.Now); |
| 210 | + await Task.Delay(500); |
| 211 | + yield return new Chunk(" the lazy dog.", DateTime.Now); |
| 212 | + } |
| 213 | + |
| 214 | + await JsonSerializer.SerializeAsync<IAsyncEnumerable<Chunk>>(pipe.Writer, GenerateResponse()); |
| 215 | + await pipe.Writer.CompleteAsync(); |
| 216 | +}); |
| 217 | + |
| 218 | +// Consumer reads from the pipe and outputs to console |
| 219 | +var consumerTask = Task.Run(async () => |
| 220 | +{ |
| 221 | + var thinkingString = "..."; |
| 222 | + var clearThinkingString = new string("\b\b\b"); |
| 223 | + var lastTimestamp = DateTime.MinValue; |
| 224 | + |
| 225 | + // Read response to end |
| 226 | + Console.Write(thinkingString); |
| 227 | + await foreach (var chunk in JsonSerializer.DeserializeAsyncEnumerable<Chunk>(pipe.Reader)) |
| 228 | + { |
| 229 | + Console.Write(clearThinkingString); |
| 230 | + Console.Write(chunk.Message); |
| 231 | + Console.Write(thinkingString); |
| 232 | + lastTimestamp = DateTime.Now; |
| 233 | + } |
| 234 | + |
| 235 | + Console.Write(clearThinkingString); |
| 236 | + Console.WriteLine($" Last message sent at {lastTimestamp}."); |
| 237 | + |
| 238 | + await pipe.Reader.CompleteAsync(); |
| 239 | +}); |
| 240 | + |
| 241 | +await producerTask; |
| 242 | +await consumerTask; |
| 243 | + |
| 244 | +record Chunk(string Message, DateTime Timestamp); |
| 245 | + |
| 246 | +// Output (500ms between each line): |
| 247 | +// The quick brown fox... |
| 248 | +// The quick brown fox jumps over... |
| 249 | +// The quick brown fox jumps over the lazy dog. Last message sent at 8/1/2025 6:41:35 PM. |
| 250 | +``` |
| 251 | + |
| 252 | +Note that all of this is serialized as JSON in the `Pipe` (formatted here for readability): |
| 253 | +``` |
| 254 | +[ |
| 255 | + { |
| 256 | + "Message": "The quick brown fox", |
| 257 | + "Timestamp": "2025-08-01T18:37:27.2930151-07:00" |
| 258 | + }, |
| 259 | + { |
| 260 | + "Message": " jumps over", |
| 261 | + "Timestamp": "2025-08-01T18:37:27.8594502-07:00" |
| 262 | + }, |
| 263 | + { |
| 264 | + "Message": " the lazy dog.", |
| 265 | + "Timestamp": "2025-08-01T18:37:28.3753669-07:00" |
| 266 | + } |
| 267 | +] |
| 268 | +``` |
| 269 | + |
| 270 | +## Networking |
| 271 | + |
| 272 | +### WebSocketStream |
| 273 | + |
| 274 | +This release introduces `WebSocketStream`, a new API designed to simplify some of the most common—and previously cumbersome—`WebSocket` scenarios in .NET. |
| 275 | + |
| 276 | +Traditional `WebSocket` APIs are low-level and require significant boilerplate: handling buffering and framing, reconstructing messages, managing encoding/decoding, and writing custom wrappers to integrate with streams, channels, or other transport abstractions. These complexities make it difficult to use WebSockets as a transport, especially for apps with streaming or text-based protocols, or event-driven handlers. |
| 277 | + |
| 278 | +**WebSocketStream** addresses these pain points by providing a Stream-based abstraction over a WebSocket. This enables seamless integration with existing APIs for reading, writing, and parsing data, whether binary or text, and reduces the need for manual plumbing. |
| 279 | + |
| 280 | +**Common Usage Patterns** |
| 281 | + |
| 282 | +Here are a few examples of how `WebSocketStream` simplifies typical WebSocket workflows: |
| 283 | + |
| 284 | +**1. Streaming text protocol (e.g., STOMP)** |
| 285 | + |
| 286 | +```csharp |
| 287 | +using Stream transportStream = WebSocketStream.Create( |
| 288 | + connectedWebSocket, |
| 289 | + WebSocketMessageType.Text, |
| 290 | + ownsWebSocket: true); |
| 291 | +// Integration with Stream-based APIs |
| 292 | +// Don't close the stream, as it's also used for writing |
| 293 | +using var transportReader = new StreamReader(transportStream, leaveOpen: true); |
| 294 | +var line = await transportReader.ReadLineAsync(cancellationToken); // Automatic UTF-8 and new line handling |
| 295 | +transportStream.Dispose(); // Automatic closing handshake handling on `Dispose` |
| 296 | +``` |
| 297 | + |
| 298 | +**2. Streaming binary protocol (e.g., AMQP)** |
| 299 | + |
| 300 | +```csharp |
| 301 | +Stream transportStream = WebSocketStream.Create( |
| 302 | + connectedWebSocket, |
| 303 | + WebSocketMessageType.Binary, |
| 304 | + closeTimeout: TimeSpan.FromSeconds(10)); |
| 305 | +await message.SerializeToStreamAsync(transportStream, cancellationToken); |
| 306 | +var receivePayload = new byte[payloadLength]; |
| 307 | +await transportStream.ReadExactlyAsync(receivePayload, cancellationToken); |
| 308 | +transportStream.Dispose(); |
| 309 | +// `Dispose` automatically handles closing handshake |
| 310 | +``` |
| 311 | + |
| 312 | +**3. Reading a single message as a stream (e.g., JSON deserialization)** |
| 313 | + |
| 314 | +```csharp |
| 315 | +using Stream messageStream = WebSocketStream.CreateReadableMessageStream(connectedWebSocket, WebSocketMessageType.Text); |
| 316 | +// JsonSerializer.DeserializeAsync reads until the end of stream. |
| 317 | +var appMessage = await JsonSerializer.DeserializeAsync<AppMessage>(messageStream); |
| 318 | +``` |
| 319 | + |
| 320 | +**4. Writing a single message as a stream (e.g., binary serialization)** |
| 321 | + |
| 322 | +```csharp |
| 323 | +public async Task SendMessageAsync(AppMessage message, CancellationToken cancellationToken) |
| 324 | +{ |
| 325 | + using Stream messageStream = WebSocketStream.CreateWritableMessageStream(_connectedWebSocket, WebSocketMessageType.Binary); |
| 326 | + foreach (ReadOnlyMemory<byte> chunk in message.SplitToChunks()) |
| 327 | + { |
| 328 | + await messageStream.WriteAsync(chunk, cancellationToken); |
| 329 | + } |
| 330 | +} // EOM sent on messageStream.Dispose() |
| 331 | +``` |
| 332 | + |
| 333 | +**WebSocketStream** enables high-level, familiar APIs for common WebSocket consumption and production patterns—reducing friction and making advanced scenarios easier to implement. |
| 334 | + |
| 335 | +### TLS 1.3 for macOS (client) |
| 336 | + |
| 337 | +This release adds client-side TLS 1.3 support on macOS by integrating Apple’s Network.framework into SslStream and HttpClient. Historically, macOS used Secure Transport which doesn’t support TLS 1.3; opting into Network.framework enables TLS 1.3. |
| 338 | + |
| 339 | +Scope and behavior |
| 340 | + |
| 341 | +- macOS only, client-side in this release. |
| 342 | +- Opt-in. Existing apps continue to use the current stack unless enabled. |
| 343 | +- When enabled, older TLS versions (TLS 1.0 and 1.1) may no longer be available via Network.framework. |
| 344 | + |
| 345 | +How to enable |
| 346 | + |
| 347 | +- AppContext switch in code: |
| 348 | + |
| 349 | +```csharp |
| 350 | +// Opt in to Network.framework-backed TLS on Apple platforms |
| 351 | +AppContext.SetSwitch("System.Net.Security.UseNetworkFramework", true); |
| 352 | + |
| 353 | +using var client = new HttpClient(); |
| 354 | +var html = await client.GetStringAsync("https://example.com"); |
| 355 | +``` |
| 356 | + |
| 357 | +- Or environment variable: |
| 358 | + |
| 359 | +```bash |
| 360 | +# Opt-in via environment variable (set for the process or machine as appropriate) |
| 361 | +DOTNET_SYSTEM_NET_SECURITY_USENETWORKFRAMEWORK=1 |
| 362 | +# or |
| 363 | +DOTNET_SYSTEM_NET_SECURITY_USENETWORKFRAMEWORK=true |
| 364 | +``` |
| 365 | + |
| 366 | +Notes |
| 367 | + |
| 368 | +- Applies to SslStream and APIs built on it (e.g., HttpClient/HttpMessageHandler). |
| 369 | +- Cipher suites are controlled by macOS via Network.framework. |
| 370 | +- Underlying stream behavior may differ when Network.framework is enabled (e.g., buffering, read/write completion, cancellation semantics). |
| 371 | +- Zero-byte reads: semantics may differ. Avoid relying on zero-length reads for detecting data availability. |
| 372 | +- Internationalized domain names (IDN): certain IDN hostnames may be rejected by Network.framework. Prefer ASCII/Punycode (A-label) hostnames or validate names against macOS/Network.framework constraints. |
| 373 | +- If your app relies on specific SslStream edge-case behavior, validate it under Network.framework. |
0 commit comments