Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
This article describes new features in the .NET libraries for .NET 11. It was last updated for Preview 5.
Diagnostics and process execution
Process API expansion
Process has a substantial set of new APIs that cover common scenarios where you previously had to wire up OutputDataReceived/ErrorDataReceived events manually or use P/Invoke.
Run-and-capture helpers
New one-shot APIs let you launch a process and get its result without manual setup:
// One-shot capture: stdout and stderr together, plus exit code.
ProcessTextOutput result = await Process.RunAndCaptureTextAsync(
"git", ["status", "--porcelain"]);
Console.WriteLine(result.StandardOutput);
Console.WriteLine($"Exit code: {result.ExitStatus.ExitCode}");
The full set of helpers includes:
- Process.Run and Process.RunAsync — launch a process and return an exit-status result.
- Process.RunAndCaptureText and Process.RunAndCaptureTextAsync — launch and capture stdout/stderr together with exit code.
- Process.ReadAllText(Nullable<TimeSpan>), Process.ReadAllBytes(Nullable<TimeSpan>), and their async variants — read a child process's standard output in a single call.
- Process.ReadAllLines(Nullable<TimeSpan>) — synchronously reads all lines of output from a child process, returning a sequence of ProcessOutputLine values that distinguish stdout from stderr.
- Process.ReadAllLinesAsync(CancellationToken) — returns a stream of ProcessOutputLine values that distinguish stdout from stderr without string parsing.
Fire-and-forget launches
- Process.StartAndForget — starts a child process when you don't intend to wait for it; the runtime detaches the handle automatically.
- ProcessStartInfo.StartDetached — detaches from the parent's session/console so the child can outlive a terminal exit.
- ProcessStartInfo.KillOnParentExit (Windows only) — the child is terminated when the parent process exits.
SafeProcessHandle lifecycle methods
SafeProcessHandle gains lifecycle methods for advanced scenarios:
- SafeProcessHandle.Start(ProcessStartInfo) and a new SafeProcessHandle.ProcessId property — launch and identify processes without going through Process itself.
- SafeProcessHandle.Kill() and SafeProcessHandle.Signal(PosixSignal) — terminate or signal a process by handle.
- SafeProcessHandle.WaitForExit() and SafeProcessHandle.WaitForExitAsync(CancellationToken) — wait for a process to exit by handle.
Tighter handle control
- ProcessStartInfo.InheritedHandles — specify exactly which OS handles a child process inherits, instead of using the all-or-nothing
UseShellExecute = falsedefault. - ProcessStartInfo.StandardInputHandle, ProcessStartInfo.StandardOutputHandle, and ProcessStartInfo.StandardErrorHandle — supply already-open Microsoft.Win32.SafeHandles.SafeFileHandle values for redirection without the framework opening new ones.
Console FORCE_COLOR support
.NET console output now honors the FORCE_COLOR standard alongside the existing NO_COLOR support. When FORCE_COLOR is set, Console.IsOutputRedirected no longer suppresses ANSI escape codes. This is useful when you pipe dotnet run output through tee, into a CI log viewer, or through less -R:
FORCE_COLOR=1 dotnet run | tee build.log
Text, serialization, and data handling
- String and character enhancements
- Base64 encoding improvements
- UTF validation and invalid-subsequence search
- System.Text.Json improvements
- Regular expression improvements
String and character enhancements
.NET 11 introduces significant enhancements to string and character manipulation APIs, making it easier to work with Unicode characters and runes.
Rune support in String methods
The String class now includes methods that accept Rune parameters, enabling you to search, replace, and manipulate strings using Unicode scalar values directly. These new methods include:
Contains- Check if a string contains a specific rune: String.Contains(Rune) and String.Contains(Rune, StringComparison).StartsWithandEndsWith- Check if a string starts or ends with a specific rune: String.StartsWith(Rune), String.StartsWith(Rune, StringComparison), String.EndsWith(Rune), and String.EndsWith(Rune, StringComparison).IndexOfandLastIndexOf- Find the position of a rune in a string: String.IndexOf(Rune), String.IndexOf(Rune, StringComparison), String.LastIndexOf(Rune), and String.LastIndexOf(Rune, StringComparison).Replace- Replace occurrences of one rune with another: String.Replace(Rune, Rune).Split- Split a string using a rune as the separator: String.Split(Rune, StringSplitOptions) and String.Split(Rune, Int32, StringSplitOptions).Trim,TrimStart, andTrimEnd- Trim runes from strings: String.Trim(Rune), String.TrimStart(Rune), and String.TrimEnd(Rune).
Many of these methods include overloads that accept a StringComparison parameter for culture-aware comparisons.
Char.Equals with StringComparison
The Char struct now includes an Char.Equals(Char, StringComparison) method that accepts a StringComparison parameter, allowing you to compare characters using culture-aware or ordinal comparisons.
Rune support in TextInfo
The TextInfo class now provides TextInfo.ToLower(Rune) and TextInfo.ToUpper(Rune) methods that accept Rune parameters, enabling you to perform case conversions on individual Unicode scalar values.
StringBuilder.MoveChunks
To transfer content without copying characters, use the new static StringBuilder.MoveChunks(StringBuilder) method, which moves all content from a source StringBuilder to a new StringBuilder. After the call, the source StringBuilder has a length of 0:
var source = new StringBuilder("Hello, ");
source.Append("World!");
// MoveChunks transfers all content from source to a new StringBuilder.
// After the call, source contains no characters.
StringBuilder dest = StringBuilder.MoveChunks(source);
Console.WriteLine(dest); // Hello, World!
Console.WriteLine(source.Length); // 0
Base64 encoding improvements
.NET 11 adds new APIs and overloads to the existing Base64 type, providing comprehensive support for Base64 encoding and decoding. These additions offer improved performance and flexibility compared to existing methods.
New Base64 APIs
The new APIs support encoding and decoding operations with various input and output formats:
- Encoding to chars: EncodeToChars and EncodeToString
- Encoding to UTF-8: EncodeToUtf8
- Decoding from chars: DecodeFromChars
- Decoding from UTF-8: DecodeFromUtf8
These methods provide both high-level convenience methods (that allocate and return arrays or strings) and low-level span-based methods (for zero-allocation scenarios).
UTF validation and invalid-subsequence search
System.Text.Unicode has two new complementary features. Utf16.IsValid(ReadOnlySpan<Char>) answers whether a sequence is well-formed UTF-16 without scanning twice, and Utf8.IndexOfInvalidSubsequence(ReadOnlySpan<Byte>) / Utf16.IndexOfInvalidSubsequence(ReadOnlySpan<Char>) return the position of the first ill-formed code-unit sequence (or -1 for valid input). Together, these methods let parsers, validators, and serializers report precise errors instead of generic encoding-error messages.
ReadOnlySpan<byte> bytes = [0xC3, 0x28]; // invalid UTF-8
int badIndex = Utf8.IndexOfInvalidSubsequence(bytes); // 0
ReadOnlySpan<char> chars = "valid \uD83D\uDC4D end"; // valid UTF-16 (👍 emoji)
bool ok = Utf16.IsValid(chars); // true
System.Text.Json improvements
Generic type info retrieval
A common pattern when working with System.Text.Json type metadata is to retrieve a JsonTypeInfo<T> from JsonSerializerOptions.
Previously, you had to manually downcast from the non-generic GetTypeInfo(Type) method.
New generic JsonSerializerOptions.GetTypeInfo<T>() and JsonSerializerOptions.TryGetTypeInfo<T>(JsonTypeInfo<T>) methods return strongly typed metadata directly, eliminating the cast.
JsonSerializerOptions options = new(JsonSerializerDefaults.Web);
options.MakeReadOnly();
// Before: manual downcast required
JsonTypeInfo<MyRecord> info1 = (JsonTypeInfo<MyRecord>)options.GetTypeInfo(typeof(MyRecord));
// After: generic method returns the right type directly
JsonTypeInfo<MyRecord> info2 = options.GetTypeInfo<MyRecord>();
// TryGetTypeInfo variant for cases where the type may not be registered
if (options.TryGetTypeInfo<MyRecord>(out JsonTypeInfo<MyRecord>? typeInfo))
{
// Use typeInfo
_ = typeInfo;
}
This is particularly useful when working with source generation, NativeAOT, and polymorphic serialization scenarios where type metadata access is common.
Naming and ignore defaults
The naming and ignore options available in System.Text.Json now include:
- JsonNamingPolicy.PascalCase: A new built-in naming policy that converts property names to PascalCase. It joins the existing camelCase, snake_case, and kebab-case policies.
- Per-member naming policy: The new System.Text.Json.Serialization.JsonNamingPolicyAttribute attribute lets you override the naming policy on individual properties or fields, giving you fine-grained control without a custom converter.
- Type-level ignore conditions: Applying System.Text.Json.Serialization.JsonIgnoreAttribute at the class or struct level sets the default ignore behavior for all members, so you no longer need to repeat the attribute on every nullable property.
// Type-level JsonIgnore: all members use WhenWritingNull by default
// Per-member JsonNamingPolicy: EventName uses camelCase even though the
// serializer options use PascalCase
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.PascalCase
};
var data = new EventData { EventName = "Launch", Notes = null };
string json = JsonSerializer.Serialize(data, options);
Console.WriteLine(json);
// {"eventName":"Launch"} -- Notes omitted (null), EventName camel-cased
F# discriminated union support
The serializer now understands F# discriminated unions out of the box. Apps that share types between F# producers and C# consumers no longer need a custom converter for the most common shapes:
type Shape =
| Circle of radius: float
| Square of side: float
let json = System.Text.Json.JsonSerializer.Serialize(Circle 1.5)
// {"$type":"Circle","radius":1.5}
Utf8JsonWriter.Reset with options
Reset now accepts a JsonWriterOptions parameter, so writer instances can be repooled with different options without allocating a new writer:
using var stream = new MemoryStream();
using var writer = new Utf8JsonWriter(stream, new JsonWriterOptions { Indented = true });
writer.WriteStartObject();
writer.WriteString("name", "example");
writer.WriteEndObject();
writer.Flush();
// Reset with different options for next use — no new allocation needed
stream.SetLength(0);
writer.Reset(stream, new JsonWriterOptions { Indented = false });
SerializeAsyncEnumerable improvements
JsonSerializer.SerializeAsyncEnumerable gains two new capabilities:
PipeWritertarget: For pipeline-based I/O scenarios, new overloads accept a PipeWriter as the output destination, making it easy to integrate JSON streaming directly.topLevelValuesparameter: For NDJSON output, set this parameter totrueto write each item as a top-level JSON value separated by newlines instead of wrapping all items in a JSON array. BothStreamandPipeWriteroverloads support the parameter.
static async IAsyncEnumerable<int> GenerateNumbers()
{
for (int i = 0; i < 5; i++)
{
yield return i;
await Task.Yield();
}
}
var pipe = new Pipe();
// Write a JSON array: [0,1,2,3,4]
await JsonSerializer.SerializeAsyncEnumerable(
pipe.Writer,
GenerateNumbers());
// Write NDJSON (one value per line): 0\n1\n2\n3\n4\n
await JsonSerializer.SerializeAsyncEnumerable(
pipe.Writer,
GenerateNumbers(),
topLevelValues: true);
Regular expression improvements
AnyNewLine option
A new RegexOptions.AnyNewLine flag makes ^, $, and . treat the full set of Unicode newline characters as line terminators—not just \n. This helps when parsing text that mixes Windows (\r\n), Unix (\n), and Unicode-specific (\u0085, \u2028, \u2029) line endings.
string text = "line1\r\nline2\u0085line3\u2028line4";
// RegexOptions.AnyNewLine makes ^, $, and . treat all Unicode newline
// sequences as line terminators, not just \n.
MatchCollection matches = Regex.Matches(
text,
@"^line\d$",
RegexOptions.Multiline | RegexOptions.AnyNewLine);
Console.WriteLine(matches.Count); // 4
Regex engine and source generator fixes
.NET 11 includes several regex correctness and code-quality fixes:
- The non-backtracking engine no longer takes super-linear time on certain nested-loop patterns and produces correct results for cases that previously diverged.
- The regex compiler and source generator handle
resumeAtcorrectly when a conditional appears inside a loop body. - The SYSLIB1045 code fixer no longer creates duplicate class names when applied across multiple partial declarations of the same class.
Compression and archive formats
Compression enhancements
.NET 11 includes several improvements to compression APIs.
ZIP archive entry access modes
The ZipArchiveEntry class now supports opening entries with specific file access modes through new overloads: ZipArchiveEntry.Open(FileAccess) and ZipArchiveEntry.OpenAsync(FileAccess, CancellationToken). These overloads accept a FileAccess parameter and allow you to open ZIP entries for read, write, or read-write access.
Additionally, a new CompressionMethod property exposes the compression method used for an entry through the ZipCompressionMethod enum, which includes values for Stored, Deflate, and Deflate64.
ZIP CRC32 validation
ZipArchive validates the CRC32 checksum when reading ZIP entries. Corrupted or truncated archives that previously passed without error now throw InvalidDataException, helping you detect data integrity issues early.
DeflateStream and GZipStream behavior change
Starting in .NET 11, DeflateStream and GZipStream always write format headers and footers to the output stream, even when no data is written. This ensures the output is a valid compressed stream according to the Deflate and GZip specifications.
Previously, these streams didn't produce any output if no data was written, resulting in an empty output stream. This change ensures compatibility with tools that expect properly formatted compressed streams.
For more information, see DeflateStream and GZipStream write headers and footers for empty payload.
Span-based Deflate, ZLib, and GZip APIs
System.IO.Compression now offers Span<byte>/ReadOnlySpan<byte> encode and decode entry points for the Deflate, ZLib, and GZip formats. The new APIs, on types such as DeflateEncoder, ZLibEncoder, and GZipEncoder, mirror the shape of BrotliEncoder/BrotliDecoder and the Zstandard primitives. You can compress and decompress buffers without allocating a Stream. This is useful for high-throughput scenarios such as protocol parsers, log shippers, and middleware that already operate on spans.
ReadOnlySpan<byte> source = [0x48, 0x65, 0x6C, 0x6C, 0x6F]; // "Hello"
byte[] buffer = new byte[source.Length + 32];
Span<byte> destination = buffer;
using ZLibEncoder encoder = new();
OperationStatus status = encoder.Compress(
source, destination, out int bytesConsumed, out int bytesWritten,
isFinalBlock: true);
Console.WriteLine($"Compressed {bytesConsumed} bytes into {bytesWritten} bytes. Status: {status}");
Zstandard compression
The Zstandard compression APIs, for example, ZstandardStream and ZstandardEncoder, are now part of the System.IO.Compression namespace, alongside DeflateStream, GZipStream, and BrotliStream. The API surface is otherwise unchanged.
Tar archive format selection
New overloads on CreateFromDirectory and CreateFromDirectoryAsync accept a TarEntryFormat parameter, giving you direct control over the archive format. Previously, CreateFromDirectory always produced Pax archives. The new overloads support all four tar formats—Pax, Ustar, GNU, and V7—for compatibility with specific tools and environments.
// Create a GNU format tar archive for Linux compatibility
TarFile.CreateFromDirectory("/source/dir", "/dest/archive.tar",
includeBaseDirectory: true, format: TarEntryFormat.Gnu);
// Create a Ustar format archive for broader compatibility
using Stream outputStream = File.OpenWrite("/dest/ustar.tar");
TarFile.CreateFromDirectory("/source/dir", outputStream,
includeBaseDirectory: false, format: TarEntryFormat.Ustar);
// Async version
CancellationToken cancellationToken = CancellationToken.None;
await TarFile.CreateFromDirectoryAsync("/source/dir", "/dest/archive.tar",
includeBaseDirectory: true, format: TarEntryFormat.Pax,
cancellationToken: cancellationToken);
TarReader can now also read entries that use the GNU sparse format 1.0 (PAX) representation. The earlier 0.1 representation was already supported. With 1.0 support in place, TarReader matches what modern tar implementations write by default for sparse files.
Collections, numerics, and low-level I/O
- LINQ join improvements
- BFloat16 support in BitConverter
- Floating-point hex formatting and parsing
- Numerics improvements
- Random generic methods
- Low-level I/O improvements
- Collections improvements
LINQ join improvements
LINQ adds join enhancements, including a new FullJoin operation and tuple-returning overloads for the existing Join and GroupJoin methods. These APIs are available on Enumerable, Queryable, and AsyncEnumerable.
LeftJoin, RightJoin, and FullJoin
Because the three outer-join operations return tuples directly, you can work with them easily using pattern matching and deconstruction:
- Enumerable.LeftJoin — Returns all elements from the left (outer) sequence. When there's no matching element in the right (inner) sequence, the inner element is
default(typicallynullfor reference types). - Enumerable.RightJoin — Returns all elements from the right (inner) sequence. When there's no matching element in the left (outer) sequence, the outer element is
default(typicallynullfor reference types). - Enumerable.FullJoin — Returns all elements from both sequences, using
defaulton either side when there's no match.
var products = new List<(int Id, string Name, string? Category)>
{
(1, "Laptop", "Electronics"),
(2, "Mouse", "Electronics"),
(3, "Orphan", null), // No matching category
};
var categories = new List<(string Name, string Description)>
{
("Electronics", "Electronic devices"),
("Furniture", "Office furniture"), // No matching product
};
// LeftJoin: all products, matched categories (null if none)
var leftJoined = products.LeftJoin(
categories,
p => p.Category,
c => c.Name);
foreach (var (product, category) in leftJoined)
Console.WriteLine($"{product.Name}: {category.Description ?? "(none)"}");
// Laptop: Electronic devices
// Mouse: Electronic devices
// Orphan: (none)
// FullJoin: all products and categories, paired where they match
var fullJoined = products.FullJoin(
categories,
p => p.Category,
c => c.Name);
foreach (var (product, category) in fullJoined)
Console.WriteLine(
$"{product.Name ?? "(none)"}: {category.Description ?? "(none)"}");
// Laptop: Electronic devices
// Mouse: Electronic devices
// Orphan: (none)
// (none): Office furniture
Tuple-returning Join and GroupJoin overloads
New overloads of Enumerable.Join and Enumerable.GroupJoin return tuples directly without requiring a result selector, and accept an optional IEqualityComparer<T> parameter. The optional comparer overload is also available on the FullJoin, LeftJoin, and RightJoin operations.
var products = new[] { (Id: 1, Name: "Laptop"), (Id: 2, Name: "Mouse") };
var orders = new[] { (ProductId: 1, Qty: 3), (ProductId: 1, Qty: 1) };
var result = products.Join(orders, p => p.Id, o => o.ProductId);
foreach (var (product, order) in result)
Console.WriteLine($"{product.Name}: qty={order.Qty}");
// Laptop: qty=3
// Laptop: qty=1
BFloat16 support in BitConverter
The BitConverter class now includes methods for converting between BFloat16 values and byte arrays or bit representations. These new methods include:
- BitConverter.GetBytes(BFloat16) - Convert a BFloat16 value to a byte array.
- BitConverter.ToBFloat16(Byte[], Int32) and BitConverter.ToBFloat16(ReadOnlySpan<Byte>) - Convert a byte array to a BFloat16 value.
- BitConverter.BFloat16ToInt16Bits(BFloat16), BitConverter.BFloat16ToUInt16Bits(BFloat16), BitConverter.Int16BitsToBFloat16(Int16), and BitConverter.UInt16BitsToBFloat16(UInt16) - Methods for converting between BFloat16 and its bit representation as
shortorushort.
BFloat16 (Brain Floating Point) is a 16-bit floating-point format that's commonly used in machine learning and scientific computing.
Floating-point hex formatting and parsing
double, float, and Half can now be formatted and parsed in their hexadecimal IEEE-754 form. The hex form preserves every bit of the underlying value, making it the right choice for golden-file tests, cross-language interop with C/C++ printf("%a", ...), and any scenario where round-tripping a double through decimal text is too lossy.
double value = Math.PI;
// Format as hexadecimal IEEE-754: preserves all bits exactly
string hex = value.ToString("X"); // e.g., "0X1.921FB54442D18P+1"
double roundTripped = double.Parse(hex, NumberStyles.HexFloat);
Console.WriteLine(roundTripped == value); // True — exact round-trip
Numerics improvements
Matrix4x4.GetDeterminant() now uses an SSE-vectorized implementation, improving performance by approximately 15%.
Random generic methods
Random gains two new generic methods that work with any numeric type implementing the appropriate generic math interfaces:
- Random.NextInteger — Generates a random integer of type
TwhereTimplementsIBinaryInteger<T>andIMinMaxValue<T>. Overloads accept an upper bound or both lower and upper bounds. - Random.NextBinaryFloat — Generates a random floating-point value of type
TwhereTimplementsIBinaryFloatingPointIeee754<T>.
// Generate a random integer of any binary integer type
int i = Random.Shared.NextInteger<int>();
long l = Random.Shared.NextInteger<long>(0L, 100L);
byte b = Random.Shared.NextInteger<byte>(maxValue: (byte)10);
// Generate a random floating-point value of any IEEE-754 type
float f = Random.Shared.NextBinaryFloat<float>();
double d = Random.Shared.NextBinaryFloat<double>();
Half h = Random.Shared.NextBinaryFloat<Half>();
Console.WriteLine($"int={i}, long={l}, byte={b}");
Console.WriteLine($"float={f}, double={d}, Half={h}");
Low-level I/O improvements
SafeFileHandle pipe support
SafeFileHandle gains two new members:
- SafeFileHandle.Type property: Reports whether a handle represents a file, pipe, socket, directory, or other OS object, without requiring platform-specific code.
- SafeFileHandle.CreateAnonymousPipe(SafeFileHandle, SafeFileHandle, Boolean, Boolean) method: Creates a pair of connected anonymous pipe handles with independent async behavior for each end.
SafeFileHandle.CreateAnonymousPipe(
out SafeFileHandle readEnd,
out SafeFileHandle writeEnd,
asyncRead: true,
asyncWrite: false);
using (readEnd)
using (writeEnd)
{
// SafeFileHandle.Type reports the kind of OS object the handle refers to
Console.WriteLine(readEnd.Type); // Pipe
Console.WriteLine(writeEnd.Type); // Pipe
}
RandomAccess pipe support
RandomAccess.Read and RandomAccess.Write now work with non-seekable handles such as pipes, in addition to regular file handles.
On Windows, Process now uses overlapped I/O for redirected stdout/stderr, which reduces thread-pool blocking in process-heavy applications.
Collections improvements
BitArray.PopCount
The BitArray class now includes a BitArray.PopCount() method that returns the number of bits set to true in the array. This provides an efficient way to count set bits without manually iterating through the array.
IReadOnlySet support in JSON serialization
The JsonMetadataServices class now includes a JsonMetadataServices.CreateIReadOnlySetInfo method, enabling JSON serialization support for IReadOnlySet<T> collections.
EqualityComparer<T>.Create
To create an equality comparer from a key selector function, use the new EqualityComparer<T>.Create factory method. You can pass an optional IEqualityComparer<T> for the key type itself:
// Create an equality comparer based on a key selector
var byName = EqualityComparer<(string Name, int Age)>.Create(p => p.Name);
var people = new HashSet<(string Name, int Age)>(byName)
{
("Alice", 30),
("Bob", 25),
("Alice", 40), // Duplicate by name — not added
};
Console.WriteLine(people.Count); // 2
Extensions and developer platform
- Discriminated-union scaffolding
- MetadataLoadContext additions
- Reflection improvements
- URI data scheme constant
- StringSyntax attribute enhancements
Discriminated-union scaffolding
Note
This is a preview feature in .NET 11.
.NET 11 introduces System.Runtime.CompilerServices.UnionAttribute and System.Runtime.CompilerServices.IUnion in System.Runtime.CompilerServices. These types are the runtime side of the C# discriminated-union design. They aren't directly user-facing yet—the C# compiler and source generators are the expected producers—but they ship in the framework so libraries can author against the surface now.
For the language-side design, see the C# unions proposal.
MetadataLoadContext additions
MetadataLoadContext.GetLoadContext(Assembly) returns the load context that produced a given Assembly, mirroring the long-existing API on AssemblyLoadContext. This closes a gap for tooling that reflects over assemblies in an isolated MetadataLoadContext and needs to walk back from an Assembly reference to the context that owns it:
using System.Reflection;
using System.Reflection.Metadata;
string[] paths = [typeof(object).Assembly.Location];
using var mlc = new MetadataLoadContext(new PathAssemblyResolver(paths));
Assembly asm = mlc.LoadFromAssemblyPath(typeof(object).Assembly.Location);
MetadataLoadContext owner = MetadataLoadContext.GetLoadContext(asm)!;
Console.WriteLine(ReferenceEquals(owner, mlc)); // true
URI data scheme constant
A new Uri.UriSchemeData constant has been added, representing the data: URI scheme. This constant provides a standardized way to reference data URIs.
Reflection improvements
Type.GetNullableUnderlyingType
System.Type gains a new virtual method, Type.GetNullableUnderlyingType(), that returns the underlying value type for a Nullable<T> type, or null for any non-nullable type. Implementations are provided on Type, TypeBuilder, EnumBuilder, GenericTypeParameterBuilder, and TypeDelegator:
Type nullableIntType = typeof(int?);
Type? underlying = nullableIntType.GetNullableUnderlyingType();
Console.WriteLine(underlying); // System.Int32
Type nonNullable = typeof(int);
Console.WriteLine(nonNullable.GetNullableUnderlyingType() is null); // True
ConstructorInfo.GetGenericArguments
ConstructorInfo.GetGenericArguments() now has an override, providing a consistent way to retrieve generic type arguments for constructor definitions, matching the behavior already available on other MethodBase subclasses.
StringSyntax attribute enhancements
The StringSyntaxAttribute class now includes constants for common programming languages:
- CSharp - Indicates C# syntax.
- FSharp - Indicates F# syntax.
- VisualBasic - Indicates Visual Basic syntax.
These constants can be used with the StringSyntax attribute to provide better tooling support for string literals containing code in these languages.
Caching and configuration
Configuration binding
Microsoft.Extensions.Configuration adds Microsoft.Extensions.Configuration.ConfigurationIgnoreAttribute, so models can opt individual properties out of binding declaratively without relying on BindNonPublicProperties toggles or custom converters:
public sealed class AppOptions
{
public string Endpoint { get; set; } = "";
[ConfigurationIgnore]
public string ComputedKey => Endpoint + ":default";
}
ConfigurationBinder now also binds an empty array to a constructor parameter instead of throwing.
PhysicalFilesWatcher no longer throws when its root directory doesn't yet exist, and InMemoryDirectoryInfo resolves .. and other relative segments consistently with the physical provider.
MemoryCache OpenTelemetry metrics
MemoryCache now emits a built-in set of OpenTelemetry (OTel)-compatible metrics without an extra adapter package. To opt in, set MemoryCacheOptions.TrackStatistics to true:
var cache = new MemoryCache(new MemoryCacheOptions
{
TrackStatistics = true
});
The new Microsoft.Extensions.Caching.Memory.MemoryCache meter publishes four observable instruments:
dotnet.cache.requests(with adotnet.cache.request.typetag that distinguisheshitfrommiss)dotnet.cache.evictionsdotnet.cache.entriesdotnet.cache.estimated_size
Pass an System.Diagnostics.Metrics.IMeterFactory to the new MemoryCache.MemoryCache(IOptions<MemoryCacheOptions>, ILoggerFactory, IMeterFactory) constructor overload for per-instance metrics. Without one, the instruments are aggregated process-wide on a shared meter.
Options builder validation improvements
OptionsBuilder<TOptions> gains a new generic OptionsBuilder<TOptions>.Validate overload that accepts a type parameter instead of a factory delegate. The type must implement IValidateOptions<TOptions> and be registered in the dependency-injection container. This aligns options validation with the standard DI pattern:
services.AddSingleton<IValidateOptions<MyOptions>, MyOptionsValidator>();
services.AddOptions<MyOptions>()
.Bind(configuration.GetSection("MyOptions"))
.Validate<MyOptionsValidator>();
Cryptography
X25519 Diffie-Hellman key exchange
The new System.Security.Cryptography.X25519DiffieHellman abstract class provides a clean API for the X25519 elliptic-curve Diffie-Hellman algorithm, which is widely used in TLS 1.3, SSH, and Signal-protocol implementations. Platform-specific implementations are provided through X25519DiffieHellmanCng (Windows) and X25519DiffieHellmanOpenSsl (Linux and macOS), but the portable GenerateKey() factory method picks the right one automatically:
// Generate key pairs for Alice and Bob
using X25519DiffieHellman alice = X25519DiffieHellman.GenerateKey();
using X25519DiffieHellman bob = X25519DiffieHellman.GenerateKey();
// Each party derives the shared secret using the other's public key
byte[] aliceShared = alice.DeriveRawSecretAgreement(bob);
byte[] bobShared = bob.DeriveRawSecretAgreement(alice);
// Both parties arrive at the same secret
Console.WriteLine(aliceShared.SequenceEqual(bobShared)); // True
The class supports the full key lifecycle: key generation, PKCS#8 and SubjectPublicKeyInfo import/export, PEM serialization, and raw private/public key access.
CryptographicOperations.FixedTimeEquals overload
For secret-comparison scenarios where the expected value is a single known byte, use the new CryptographicOperations.FixedTimeEquals overload that compares a ReadOnlySpan<T> with a single byte value in constant time:
bool equal = CryptographicOperations.FixedTimeEquals(receivedSpan, 0x42);
Networking and transport security
- TLS handshake hardening
- HTTP/2 automatic downgrade for Windows authentication
- QUIC stream priority
- Video MIME type constants
TLS handshake hardening
Two System.Net.Security items improve TLS (Transport Layer Security) reliability:
- SslStream server-side handshake bounds-checking fixes in
TlsFrameHelperclose several edge cases that could surface asIOExceptionon malformed ClientHello records. - On Linux, certificate-validation failures now surface as standard TLS alerts to the peer, matching Windows behavior. Connecting clients receive an actionable handshake error instead of a connection drop.
HTTP/2 automatic downgrade for Windows authentication
HttpClient automatically downgrades to HTTP/1.1 when a request requires Windows authentication (NTLM/Negotiate) over HTTP/2. The HTTP/2 specification disallows the connection-bound authentication schemes that NTLM and Kerberos rely on, so these requests previously failed. With the downgrade in place, applications targeting mixed-authentication environments—common in enterprise intranets—work without explicit HttpRequestMessage.Version overrides.
QUIC stream priority
QuicStream gains a QuicStream.Priority property and a QuicStream.DefaultPriority constant that exposes HTTP/3 stream prioritization (RFC 9218). Priority values range from 0 (highest) to 255 (lowest), with a default of 127:
// Increase priority for interactive streams
stream.Priority = 64; // Higher priority than the default 127
Video MIME type constants
A new System.Net.Mime.MediaTypeNames.Video class provides string constants for common video media types:
MediaTypeNames.Video.Mp4—"video/mp4"MediaTypeNames.Video.Mpeg—"video/mpeg"MediaTypeNames.Video.Ogg—"video/ogg"MediaTypeNames.Video.QuickTime—"video/quicktime"MediaTypeNames.Video.WebM—"video/webm"
These join the existing MediaTypeNames.Application, MediaTypeNames.Image, MediaTypeNames.Text, and MediaTypeNames.Multipart classes.
Unsafe API accessibility
.NET 11 removes the [RequiresUnsafe] attribute from a large set of APIs that take pointer parameters. Previously, calling these methods from code that used unsafe blocks still required a project-level <AllowUnsafeBlocks>true</AllowUnsafeBlocks> setting because the attribute enforced that requirement independently. Now, only the standard unsafe block or method modifier is required.
Affected APIs include:
- Buffer.MemoryCopy
- ReadOnlySpan<T>.ReadOnlySpan<T>(Void*, Int32) and Span<T>.Span<T>(Void*, Int32)
- System.Runtime.CompilerServices.Unsafe pointer methods such as
AsRef,Read,Write, andCopy - System.Runtime.InteropServices.NativeMemory methods
- System.Text.Encoding pointer overloads (all encoding classes)
- System.Numerics.Vector pointer-based
LoadandStoremethods - Interop marshalling types in System.Runtime.InteropServices.Marshalling
This change reduces friction for advanced scenarios such as interop authoring, memory-mapped files, and low-level buffer manipulation, where unsafe context is already required by the calling code.