What’s New in .NET 10 Preview: Top Features Developers Shouldn’t Miss

syncfusionsyncfusion
14 min read

TL;DR:.NET 10 introduces major runtime improvements, enhanced libraries, and SDK updates, focusing on performance, security, and developer efficiency.

Get ready for the future with .NET 10! Preview version 2 offers a sneak peek into Microsoft’s latest advancements in programming technology. With a strong emphasis on performance and reliability, the features preview what’s set to be an incredibly stable and feature-rich platform. The main release of .NET 10 promises some serious upgrades for your .NET 8 applications. Let’s stay ahead of the curve and explore the potential of .NET 10!

This blog will discuss the highlights of this latest preview release and how it’s reducing complexities and enhancing runtime efficiency. These updates cater to a wide array of applications, from system-level tasks to sophisticated web solutions.

Note: Syncfusion components are now compatible with .NET 10 Preview 2. This aligns with Microsoft’s .NET support policy.

Key features and Concepts

Libraries

Find certificates using SHA-256 thumbprints

In previous versions of .NET, if you needed to find a certificate by its thumbprint, you had to use the X509Certificate2Collection.Find() method with the FindByThumbprint option. This method only searches for thumbprints using SHA-1, an older hashing algorithm. There is a gap in the find method for finding SHA-2-256 (SHA256) and SHA-3-256 thumbprints, since these hash algorithms have the same length. .NET 10 adds a new method that allows you to search by thumbprint using hashing algorithms SHA-256 or SHA-3.

Now you can securely and accurately find SHA-256-based certificates, which is more in line with modern security standards.

// Search using SHA-256 instead of the default SHA-1.
X509Certificate2Collection coll = store.Certificates.FindByThumbprint(HashAlgorithmName.SHA256, thumbprint);

Find PEM-Encoded data in ASCII/UTF-8

PEM files (like certificates or keys) are often stored in ASCII/UTF-8, especially on Linux. But older versions of .NET required you to convert the bytes to a string before using PemEncoding.Find(), an extra step that added unnecessary overhead.

In .NET 10, you can now use PemEncoding.FindUtf8() to read them directly from bytes without the need to convert to string first.

Refer to the following code example.

string path = "certificate.pem";

// Old approach (pre .NET 10).
byte[] fileBytes = File.ReadAllBytes(path);
string text = Encoding.ASCII.GetString(fileBytes);
PemFields pemFieldsOld = PemEncoding.Find(text);

// New approach (in .NET 10).
byte[] fileBytesNew = File.ReadAllBytes(path);
PemFields pemFieldsNew = PemEncoding.FindUtf8(fileBytesNew);

Specify encryption algorithms when exporting certificates

Until .NET 10, you had limited control over the encryption and hashing algorithms used during export, typically defaulting to a legacy algorithm (3DES).

You can now control how certificates are encrypted when exporting them to a .pfx file using the new ExportPkcs12 methods on the X509Certificate2 class. This means you can pick which encryption and hash algorithms are used during export.

Refer to the following code example.

using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;

var certificateWithKey = /* your X509Certificate2 with private key */;

// Export using legacy-compatible encryption (3DES + SHA-1).
byte[] pfxLegacy = certificateWithKey.ExportPkcs12(
    Pkcs12ExportPbeParameters.Pkcs12TripleDesSha1,
    password: "securePassword"
);

// Export using stronger encryption (AES-256 + SHA-256).
byte[] pfxSecure = certificateWithKey.ExportPkcs12(
    Pkcs12ExportPbeParameters.Pbes2Aes256Sha256,
    password: "securePassword"
);

// Export using custom PBE settings.
var customPbeParams = new PbeParameters(
    encryptionAlgorithm: PbeEncryptionAlgorithm.Aes256Cbc,
    hashAlgorithm: HashAlgorithmName.SHA512,
    iterationCount: 100_000
);

byte[] pfxCustom = certificateWithKey.ExportPkcs12(
    customPbeParams,
    password: "securePassword"
);

ISOWeek now supports dateOnly

The ISOWeek class now includes overloads that work with the DateOnly type. Previously, it only supported DateTime.

Refer to the following code example.

using System;
using System.Globalization;

DateOnly date = ISOWeek.ToDateOnly(2025, 15, DayOfWeek.Monday);
int weekOfYear = ISOWeek.GetWeekOfYear(date);

New overload for timeSpan.fromMilliseconds

Previously, TimeSpan.FromMilliseconds (long milliseconds, long microseconds = 0) was introduced with the second parameter marked as optional. This creates compilation errors when using LINQ expressions.

To solve this, .NET 10 introduces a new overload: FromMilliseconds (long milliseconds). The existing method now requires both parameters explicitly (the second is no longer optional).

Updated APIs

public readonly struct TimeSpan
{
    // New overload for simplicity when only milliseconds are needed.
    public static TimeSpan FromMilliseconds(long milliseconds);

    // Existing method now requires both parameters explicitly.
    public static TimeSpan FromMilliseconds(long milliseconds, long microseconds);
}

Normalize strings for span of characters

.NET 10 adds span-based string normalization APIs, so you can now normalize Span < char > or ReadOnlySpan < char > directly without needing to creating new strings.

New APIs

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

Refer to the following code example.

char[] buffer = new char[100];
ReadOnlySpan<char> input = "e\u0301".AsSpan(); // "e" + accent
Span<char> output = buffer;

if (!input.IsNormalized(NormalizationForm.FormC))
{
    if (input.TryNormalize(output, out int written, NormalizationForm.FormC))
    {
        Console.WriteLine($"Normalized to: '{new string(output[..written])}'");
    }
}

Numeric string comparison

Traditionally, string comparison in .NET is character-based, which leads to unintuitive results when dealing with numeric strings. For example:

  • 10 is considered less than 2 (because 1 < 2)

  • 2 and 02 are not equal (different lengths)

.NET 10 fixes that! With CompareOptions.NumericOrdering, you can now sort and compare strings like numbers—so 2 comes before 10 and 02 equals 2. It’s great for versioning, UI lists, and human-friendly sorting

Refer to the following code example.

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

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

// Numeric sort.
foreach (string os in new[] {"Windows 8", "Windows 10", "Windows 11" }.OrderBy(s => s, numericStringComparer))
{
    Console.WriteLine(os);
}
// Output:
// Windows 8
// Windows 10
// Windows 11

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

This option isn’t valid for the following index-based string operations: IndexOf, LastIndexOf, StartsWith, EndsWith, IsPrefix, and IsSuffix. These still operate purely on character-based comparison.

ZipArchive is now faster and more memory-efficient

Previously, all ZipArchiveEntry instances were loaded into memory and rewritten, which could lead to high memory usage and performance bottlenecks.

When updating .zip files in ZipArchive ,.NET 10 no longer loads all entries into memory, cutting down memory use and boosting performance. Extraction is now parallelized, too, making it faster when working with large files.

OrderedDictionary< TKey, TValue > overloads

While TryAdd and TryGetValue existed, they didn’t tell you where in the collection the item was, making it harder to use methods like GetAt() and SetAt() efficiently. .NET 10 introduces new overloads, TryAdd and TryGetValue, that return the index of the item. You can now easily use GetAt and SetAt for read/write access.

New APIs

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

Refer to the following code example.

OrderedDictionary<string, int> orderedDictionary = new OrderedDictionary<string, int>();
string key = "apple";

// Try to add "apple" with a value of 1.
if (!orderedDictionary.TryAdd(key, 1, out int index))
{
    // If a key exists, increment its value.
    int currentValue = orderedDictionary.GetAt(index).Value;
    orderedDictionary.SetAt(index, currentValue + 1);
}

Handle circular references in JSON source generators

In earlier versions, source generators in System.Text.Json lacked support for reference handling, especially when dealing with self-referencing or circular relationship objects. If you tried to serialize a self-referencing object with a source-generated context, you’d get an exception. That’s because source generation didn’t support the reference handler configuration.

With .NET 10, you can now specify a ReferenceHandler directly in the [JsonSourceGenerationOptions] attribute, enabling support for preserving or ignoring reference loops.

Refer to the following code example.

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

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

// Enable ReferenceHandler in the source generator context.
[JsonSourceGenerationOptions(ReferenceHandler = JsonKnownReferenceHandler.Preserve)]
[JsonSerializable(typeof(SelfReference))]
internal partial class ContextWithPreserveReference : JsonSerializerContext
{
}

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

More left-handed matrix transformation methods

If you’re using a left-handed coordinate system (common in DirectX), .NET 10 now provides CreateBillboardLeftHanded and CreateConstrainedBillboardLeftHanded methods in Matrix4x4 to make it easier to build billboard effects.

New APIs

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

SDK

Pruning framework-provided package references

In previous versions of .NET, when you created a project and built it, all the framework-provided NuGet package references, even the unused ones, were included during the restore process. This leads to longer restoration times and increased chances of false positives when using tools like NuGet Audit or other security scanners.

.NET 10 introduces automatic pruning of unused, framework-provided NuGet packages during restoration. This reduces unnecessary dependencies in the .deps.json file, speeds up build and restore times, and minimizes false positives from tools like NuGet Audit.

The feature is enabled by default for.NET 8.0, .NET 10, and .NET Standard 2.0+ target frameworks. You can opt out by setting RestoreEnablePackagePruning to false in your project file.

<PropertyGroup>
  <RestoreEnablePackagePruning>false</RestoreEnablePackagePruning>
</PropertyGroup>

Noun-first dotnet CLI commands

.NET 10 introduces new noun-first aliases for common dotnet CLI commands, making them more consistent with other CLI tools. These aliases improve readability and are ideal for scripts and documentation. Both formats are supported, but the new structure is recommended going forward.

New noun-first formOld equivalent (verb-first forms)
dotnet package adddotnet add package
dotnet package listdotnet list package
dotnet package removedotnet remove package
dotnet reference adddotnet add reference
dotnet reference listdotnet list reference
dotnet reference removedotnet remove reference

CLI commands now default to interactive mode

Previously, when running dotnet CLI commands that needed to authenticate (like accessing private NuGet feeds or Azure services), developers had to remember to manually add the –interactive flag. Forgetting this led to confusing errors or failed commands, especially for those unfamiliar with how credential prompts work.

Starting in .NET 10, the –interactive flag is automatically enabled when using the CLI in an interactive terminal. This means commands can prompt for credentials without any extra flags.

Previously:

dotnet restore --interactive

Now (in interactive terminals like your shell or terminal window):

dotnet restore
# Automatically prompts for credentials if needed.

If you want to disable it for automated builds or non-interactive environments (e.g., CI/CD), you can explicitly do so:

dotnet restore --interactive false

Console apps can natively create container images

Previously, if you wanted to publish a .NET console app as a container image using the dotnet publish command, you had to explicitly add the following setting to your .csproj file:

<EnableSdkContainerSupport>true</EnableSdkContainerSupport>

This was confusing for developers, especially since ASP.NET Core projects didn’t require this extra step.

Starting in .NET 10, you can publish a console app directly as a container image using the dotnet publish /t:PublishContainer command—no extra properties required in the project file!

dotnet publish /t:PublishContainer

Explicitly control the image format of containers

When publishing container images from .NET apps using the dotnet publish /t:PublishContainer, the format of the generated image—Docker or OCI (open container initiative)—was automatically selected based on the base image and architecture. While this works for most cases, it introduced uncertainty and made automation and registry compatibility checks more complex.

For example, some registries or tools may prefer a specific image format (e.g., OCI), and developers had no simple way to control it.

With .NET 10, you can now explicitly control the image format used when generating container images using the new < ContainerImageFormat > property.

Supported values:

  • Docker: Traditional Docker image format.

  • OCI: Open container initiative-compliant format.

<PropertyGroup>
  <ContainerImageFormat>OCI</ContainerImageFormat>
   <!-- <ContainerImageFormat>OCI</ContainerImageFormat> -->
</PropertyGroup>

Forcing OCI format example:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <ContainerImageName>myapp</ContainerImageName>
    <ContainerImageFormat>OCI</ContainerImageFormat>
  </PropertyGroup>
</Project>

Now, when you run the following, your image will be built explicitly in OCI format, regardless of the base image.

dotnet publish /t:PublishContainer

Support for microsoft testing platform in dotnet test

As projects grow and teams look to modernize their testing strategies, they may want to adopt newer test platforms like Microsoft.Testing.Platform —a next-generation test runner introduced by Microsoft. However, until now, using this platform required additional setup or custom tooling, which slowed down adoption and made integration tricky for everyday projects.

Starting with .NET 10 Preview, the dotnet test command natively supports the Microsoft.Testing.Platform test runner. You can opt into this new platform by adding a simple configuration entry in your dotnet.config file.

[dotnet.test:runner]
name = "Microsoft.Testing.Platform"

Note: This section key will change to [dotnet.test.runner] in .NET 10 Preview 4.

Runtime

One of the major goals of .NET 10 is to reduce the hidden performance costs of common language features. Arrays are at the heart of most applications, and how efficiently they are accessed, iterated, and allocated can make a big difference—especially in high-throughput or low-latency systems.

With the .NET 10 preview releases, Microsoft has made several powerful enhancements that make array operations faster and more memory efficient. Let’s explore these improvements in detail.

Array interface method devirtualization

When you loop over an array using a foreach and cast it to IEnumerable< T >, like in the following, it introduces virtual method calls under the hood, because arrays implement IEnumerable< T > through interface methods.

Refer to the following code example.

static int Sum(int[] array)
{
    int sum = 0;
    IEnumerable<int> temp = array;

    foreach (var num in temp)
    {
        sum += num;
    }
    return sum;
}

The JIT compiler (just-In-time) couldn’t optimize these calls previously, meaning there was no inlining, no loop unrolling, and slower execution.

.NET 10 introduces the devirtualization of array interface methods. This means the JIT can now:

  • Recognize that temp is actually an array.

  • Remove the virtual method call.

  • Inline the logic and apply loop-level optimizations.

Foreach loops over arrays now perform almost as efficiently as classic for loops in terms of speed and memory usage.

Arrary enumeration de-abstraction

In addition to devirtualizing method calls, .NET 10 also reduces the overhead of enumerators themselves by optimizing the enumerator creation.

Enumerator structs (like ArrayEnumerator< T >) are now more likely to be:

  • Stack-allocated (no heap allocation).

  • Inlined.

  • Loop-cloned for better performance.

This is achieved via improvements to escape analysis, where the JIT analyzes if the enumerator is only used locally and short-lived—allowing stack allocation instead of heap allocation.

Example: In .NET 10, the following code becomes more efficient, because the underlying enumerator doesn’t need to go through slow interface calls or cause heap allocations.

static void PrintAll(string[] words)
{
    foreach (var word in words)
    {
        Console.WriteLine(word);
    }
}

Stack allocation of small arrays

In earlier versions of .NET, even small arrays, no matter how short-lived, were typically allocated on the heap. This meant extra work for the garbage collector (GC), even when the arrays were only used temporarily inside a method.

In performance-sensitive code, these allocations could add up and degrade responsiveness.

Refer to the following code example.

// Value type array.
static void Sum()
{
    int[] numbers = {1, 2, 3};
    int sum = 0;
    for (int i = 0; i < numbers.Length; i++)
    {
        sum += numbers[i];
    }
    Console.WriteLine(sum);
}

// Reference type array.
static void Print()
{
    string[] words = { "Hello", "World!"};
    foreach (var word in words)
    {
        Console.WriteLine(word);
    }
}

Both these array examples are:

  • Small and fixed in size.

  • Not returned or passed outside the method (not GC-referenced).

  • Only used locally.

Now, the JIT compiler in .NET 10 can allocate small arrays on the stack, improving performance by avoiding GC overhead and reducing memory pressure.

This optimization applies to both value type arrays (like int[]) and reference type arrays (like string[]), provided:

  • The array size is small and known at compiled time.

  • The array is not shared, returned, assigned to a field, or passed outside the method.

Support for casting and negation in nativeAOT’s type pre-initializer

In .NET, when you write C# code, it gets compiled into Intermediate Language (IL) instructions. These IL instructions are then processed by the .NET runtime or compiled ahead of time (like with NativeAOT). Two common types of IL instructions are:

  • Conv.* Opcodes – Casting operations: These opcodes handle type conversions, converting one data type to another.

  • Neg Opcode – Negation operation: This opcode is used to negate numeric values.

NativeAOT tries to precompute as much logic as possible at compilation time—this is called pre-initialization. Before .NET 10, if your method used conv.* or neg, it couldn’t be pre-initialized, so the runtime had to compute it later.

Now, with support for these opcodes in .NET 10:

  • The compiler can preprocess methods with casting or negation.

  • These methods are included as native code directly.

  • Your app starts faster and uses fewer runtime resources.

Conclusion

.NET 10 will be laying a robust groundwork for modern application development, with its extensive array of new features and improvements. Its runtime performance enhancements and the JIT optimizations help applications run more efficiently. Developers are encouraged to explore .NET 10 Preview 1 and Preview 2 through Microsoft’s extensive documentation and community channels to gain a thorough understanding of its capabilities for successful adoption.

The Syncfusion® Blazor component suite is now compatible with .NET 10 Preview 2 and offers over 90 responsive, lightweight components for building modern web apps. Please share your thoughts and experiences in the comments section below to provide valuable insights to fellow developers and contribute to a richer understanding of .NET 10’s impact on the programming landscape.

For our existing Syncfusion customers, the newest version of Essential Studio is available from the license and downloads page. If you are not a customer, try our 30-day free trial to check out these new features.

If you have questions, contact us through our support forums, feedback portal, or support portal. We are always happy to assist you!

0
Subscribe to my newsletter

Read articles from syncfusion directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

syncfusion
syncfusion

Syncfusion provides third-party UI components for React, Vue, Angular, JavaScript, Blazor, .NET MAUI, ASP.NET MVC, Core, WinForms, WPF, UWP and Xamarin.