.NET Libraries in .NET 10 Preview 1 - Release Notes

February 25, 2025 · View on GitHub

.NET 10 Preview 1 includes new .NET Libraries features & enhancements:

.NET Libraries updates in .NET 10:

Finding Certificates By Thumbprints Other Than SHA-1

Finding certificates uniquely by thumbprint is a fairly common operation, but the X509Certificate2Collection.Find method (for the FindByThumbprint mode) only searches for the SHA-1 Thumbprint value.

Since SHA-2-256 ("SHA256") and SHA-3-256 have the same lengths, we decided that making the Find method find any vaguely matching thumbprints was not the best option.

Instead, we introduced a new method that accepts the name of the hash algorithm that you want to use for matching.

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();

Finding PEM-encoded Data in ASCII/UTF-8

The PEM encoding (originally "Privacy Enhanced Mail", but now used widely outside of email) is defined for "text", which means that the PemEncoding class was designed to run on System.String and ReadOnlySpan<char>. But, it's quite common (especially on Linux) to have something like a certificate written in a file that uses the ASCII (string) encoding. Historically, that meant you needed to open the file, convert the bytes to chars (or a string), and then you can use PemEncoding.

Taking advantage of the fact that PEM is only defined for 7-bit ASCII characters, and that 7-bit ASCII has a perfect overlap with single-byte UTF-8 values, you can now skip the UTF-8/ASCII to char conversion and read the file directly.

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]);

New Method Overloads in ISOWeek for DateOnly Type

The ISOWeek class was originally designed to work exclusively with DateTime, as it was introduced before the DateOnly type existed. Now that DateOnly is available, it makes sense for ISOWeek to support it as well.

    public static class ISOWeek
    {
        // New overloads
        public static int GetWeekOfYear(DateOnly date);
        public static int GetYear(DateOnly date);
        public static DateOnly ToDateOnly(int year, int week, DayOfWeek dayOfWeek);
    }

String Normalization APIs to Work with Span of Characters

Unicode string normalization has been supported for a long time, but existing APIs have only worked with the string type. This means that callers with data stored in different forms, such as character arrays or spans, must allocate a new string to use these APIs. Additionally, APIs that return a normalized string always allocate a new string to represent the normalized output.

The change introduces new APIs that work with spans of characters, expanding normalization beyond string types and helping to avoid unnecessary allocations.

    public static class StringNormalizationExtensions
    {
        public static int GetNormalizedLength(this ReadOnlySpan<char> source, NormalizationForm normalizationForm = NormalizationForm.FormC);
        public static bool IsNormalized(this ReadOnlySpan<char> source, NormalizationForm normalizationForm = NormalizationForm.FormC);
        public static bool TryNormalize(this ReadOnlySpan<char> source, Span<char> destination, out int charsWritten, NormalizationForm normalizationForm = NormalizationForm.FormC);
    }

Numeric Ordering for String Comparison

Numerical string comparison is a highly requested feature (https://github.com/dotnet/runtime/issues/13979) for comparing strings numerically instead of lexicographically. For example, 2 is less than 10, so "2" should appear before "10" when ordered numerically. Similarly, "2" and "02" are equal numerically. With the new CompareOptions.NumericOrdering option, it is now possible to do these types of comparisons:

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

Note that this option is not valid for the following index based string operations: IndexOf, LastIndexOf, StartsWith, EndsWith, IsPrefix, and IsSuffix.

Adding TimeSpan.FromMilliseconds Overload with a Single Parameter

Previously, we introduced the following method without adding an overload that takes a single parameter:

public static TimeSpan FromMilliseconds(long milliseconds, long microseconds = 0);

Although this works since the second parameter is optional, it causes a compilation error when used in a LINQ expression like:

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

The issue arises because LINQ expressions cannot handle optional parameters. To address this, we are introducing an overload that takes a single parameter and modifying the existing method to make the second parameter mandatory:

public readonly struct TimeSpan
{
    public static TimeSpan FromMilliseconds(long milliseconds, long microseconds); // Second parameter is no longer optional
    public static TimeSpan FromMilliseconds(long milliseconds);  // New overload
}

ZipArchive performance and memory improvements

Two significant PRs have been made by contributor @edwardneal in .NET 10 Preview 1 to improve the performance and memory usage of ZipArchive:

  • dotnet/runtime #102704 optimizes the way entries are written to a ZipArchive when in Update mode. Previously, all ZipArchiveEntries would be loaded into memory and rewritten, which could lead to high memory usage and performance bottlenecks. The optimization reduces memory usage and improves performance by avoiding the need to load all entries into memory.

    Adding a 2GB zip file to an existing archive showed:

    • A 99.8% reduction in execution time.

    • A 99.9996% reduction in memory usage.

      Benchmarks:

      MethodJobRuntimeMeanErrorStdDevRatioRatioSDGen0Gen1Gen2AllocatedAlloc Ratio
      BenchmarkBaseline.NET 9.04.187 s83.3751 ms177.6792 ms1.0020.061000.00001000.00001000.00002 GB1.000
      BenchmarkCoreRun.NET 10.09.452 ms0.1583 ms0.1322 ms0.0020.00---7.01 KB0.000

    Additional details are provided in dotnet/runtime #102704.

  • dotnet/runtime #103153 enhances the performance of ZipArchive by parallelizing the extraction of entries and optimizing internal data structures for better memory usage. These improvements address issues related to performance bottlenecks and high memory usage, making ZipArchive more efficient and faster, especially when dealing with large archives.

    Reading a zip archive showed:

    • An 18% reduction in execution time.

    • An 18% reduction in memory usage.

      Benchmarks:

      MethodJobRuntimeNumberOfFilesMeanErrorStdDevRatioRatioSDGen0Gen1Gen2AllocatedAlloc Ratio
      BenchmarkBaseline.NET 9.021,178.6 ns23.23 ns22.81 ns1.000.030.3700--1.52 KB1.00
      BenchmarkCoreRun.NET 10.02821.6 ns12.45 ns11.65 ns0.700.020.2899--1.19 KB0.78
      BenchmarkBaseline.NET 9.0104,205.5 ns62.41 ns55.33 ns1.000.021.4954--6.13 KB1.00
      BenchmarkCoreRun.NET 10.0103,467.5 ns67.25 ns66.05 ns0.820.021.2054--4.93 KB0.80
      BenchmarkBaseline.NET 9.02510,201.5 ns190.59 ns187.18 ns1.000.023.5095--14.38 KB1.00
      BenchmarkCoreRun.NET 10.0258,210.2 ns152.35 ns142.51 ns0.810.022.8229--11.54 KB0.80
      BenchmarkBaseline.NET 9.05020,152.7 ns333.29 ns311.76 ns1.000.027.0496--28.91 KB1.00
      BenchmarkCoreRun.NET 10.05020,109.1 ns517.18 ns1,500.43 ns1.000.085.7068--23.34 KB0.81
      BenchmarkBaseline.NET 9.010046,986.2 ns923.08 ns1,906.33 ns1.000.0614.28220.1221-58.42 KB1.00
      BenchmarkCoreRun.NET 10.010037,767.2 ns752.51 ns1,554.06 ns0.810.0511.59670.0610-47.38 KB0.81
      BenchmarkBaseline.NET 9.0250115,159.8 ns2,211.52 ns2,271.07 ns1.000.0334.54590.1221-141.42 KB1.00
      BenchmarkCoreRun.NET 10.025094,148.7 ns1,842.33 ns3,414.87 ns0.820.0327.83200.3662-113.97 KB0.81
      BenchmarkBaseline.NET 9.0500241,338.5 ns4,726.33 ns7,896.64 ns1.000.0569.82420.4883-285.86 KB1.00
      BenchmarkCoreRun.NET 10.0500184,869.9 ns2,969.04 ns4,162.18 ns0.770.0356.15230.7324-231.06 KB0.81
      BenchmarkBaseline.NET 9.01000510,114.6 ns10,092.12 ns20,386.57 ns1.000.05114.257872.2656-577.2 KB1.00
      BenchmarkCoreRun.NET 10.01000404,349.3 ns7,289.88 ns16,153.88 ns0.790.0493.261752.7344-467.72 KB0.81
      BenchmarkBaseline.NET 9.01000013,950,239.9 ns273,372.39 ns345,728.57 ns1.000.031000.0000687.5000218.75005786.24 KB1.00
      BenchmarkCoreRun.NET 10.01000010,911,298.0 ns204,013.47 ns226,760.43 ns0.780.02843.7500609.3750250.00004692.19 KB0.81

      Creating an archive showed:

    • A 23-35% reduction in execution time.

    • A 2% reduction in memory usage.

      Benchmarks:

      MethodJobRuntimeNumberOfFilesMeanErrorStdDevMedianRatioRatioSDGen0Gen1AllocatedAlloc Ratio
      BenchmarkBaseline.NET 9.022.729 μs0.0538 μs0.0449 μs2.706 μs1.000.022.2697-9.28 KB1.00
      BenchmarkCoreRun.NET 10.021.665 μs0.0256 μs0.0239 μs1.659 μs0.610.012.2259-9.1 KB0.98
      BenchmarkBaseline.NET 9.01010.341 μs0.1988 μs0.2289 μs10.266 μs1.000.039.7046-39.76 KB1.00
      BenchmarkCoreRun.NET 10.0107.937 μs0.1514 μs0.2487 μs7.831 μs0.770.039.5215-39.02 KB0.98
      BenchmarkBaseline.NET 9.02524.677 μs0.4903 μs0.8842 μs24.563 μs1.000.0520.17213.356982.92 KB1.00
      BenchmarkCoreRun.NET 10.02518.247 μs0.3474 μs0.3412 μs18.192 μs0.740.0319.77543.265481.13 KB0.98
      BenchmarkBaseline.NET 9.05067.420 μs5.7447 μs16.9384 μs57.185 μs1.050.3540.527313.4888166.71 KB1.00
      BenchmarkCoreRun.NET 10.05041.443 μs0.7212 μs0.8306 μs41.493 μs0.650.1339.67290.0610163.16 KB0.98

      Additional benchmarking details provided in the PR description.

Additional TryAdd and TryGetValue overloads for OrderedDictionary<TKey, TValue>

OrderedDictionary<TKey, TValue> provides TryAdd and TryGetValue for addition and retrieval like any other IDictionary<TKey, TValue> implementation. However, there are scenarios where you might want to perform additional operations, so new overloads have been added which return an index to the entry:

public class OrderedDictionary<TKey, TValue>
{
    // New overloads
    public bool TryAdd(TKey key, TValue value, out int index);
    public bool TryGetValue(TKey key, out TValue value, out int index);
}

This index can then be used with GetAt/SetAt for fast access to the entry. An example usage of the new TryAdd overload is to add or update a key/value pair in the ordered dictionary:

public static void IncrementValue(OrderedDictionary<string, int> orderedDictionary, string key)
{
    // 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);
    }
}

This new API is now being used in JsonObject to improve the performance of updating properties by 10-20%.

Allow specifying ReferenceHandler in JsonSourceGenerationOptions

When using source generators for JSON serialization, the generated context will throw when cycles are serialized or deserialized. This behavior can now be customized by specifying the ReferenceHandler in the JsonSourceGenerationOptionsAttribute. Here is an example using JsonKnownReferenceHandler.Preserve:

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; }
}

More Left-Handed Matrix Transformation Methods

The remaining APIs for creating left-handed tranformation matrices have been added for billboard and constrained billboard matrices. These can be used like their existing right-handed counterparts when using a left-handed coordinate system instead.

public partial struct Matrix4x4
{
   public static Matrix4x4 CreateBillboardLeftHanded(Vector3 objectPosition, Vector3 cameraPosition, Vector3 cameraUpVector, Vector3 cameraForwardVector)
   public static Matrix4x4 CreateConstrainedBillboardLeftHanded(Vector3 objectPosition, Vector3 cameraPosition, Vector3 rotateAxis, Vector3 cameraForwardVector, Vector3 objectForwardVector)
}