Library Improvements

Numeric Ordering for String Comparison

In .NET 10, the System.String class adds a CompareAsNumbers method for comparing strings as numbers. This is very useful for sorting strings that contain numbers, such as file names or version numbers.

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

UTF-8 Support for Hexadecimal String Conversion

.NET 10 adds UTF-8 support for hexadecimal string conversion operations in the Convert class. These new methods provide an efficient way to convert between UTF-8 byte sequences and hexadecimal representations without intermediate string allocations:

Convert.FromHexString(ReadOnlySpan<Byte>)
Convert.FromHexString(ReadOnlySpan<Byte>, Span<Byte>, Int32, Int32)
Convert.TryToHexString(ReadOnlySpan<Byte>, Span<Byte>, Int32)
Convert.TryToHexStringLower(ReadOnlySpan<Byte>, Span<Byte>, Int32)
Convert.TryToHexStringUpper(ReadOnlySpan<Byte>, Span<Byte>, Int32)

Option to Disallow Duplicate JSON Properties

The JSON specification does not define how to handle duplicate properties when deserializing a JSON payload. This can lead to unexpected results and security vulnerabilities. .NET 10 introduces the JsonSerializerOptions.AllowDuplicateProperties option to disallow duplicate JSON properties.

var options = new JsonSerializerOptions
{
    AllowDuplicateProperties = false
};

string json = @"{ ""Name"": ""Alice"", ""Name"": ""Bob"" }";

try
{
    var person = JsonSerializer.Deserialize<Person>(json, options);
}
catch (JsonException ex)
{
    Console.WriteLine($"JSON deserialization failed: {ex.Message}");
}

SDK Improvements

Single Tool Execution

You can now run a .NET tool with a command via dotnet tool exec, without installing the tool globally or locally.

dotnet tool exec dotnetsay  "Hello, World!"

The New dnx Tool Execution Script

The dnx script provides a simplified way to execute tools.

dnx dotnetsay "Hello, World!"

File-Based Applications

Console.WriteLine("Hello, World!");

You can run it by saving the code to a file named app.cs and running the following command:

dotnet run app.cs
#:package Colorful.Console@1.2.15
Colorful.Console.WriteAscii("Hello, World!");

You can run it by saving the code to a file named app.cs and running the following command:

dotnet run app.cs

See C# preprocessor directives

More Consistent Command Order

The .NET CLI commands now follow a more consistent ordering pattern. The following table shows the new preferred forms and their aliases:

New preferred formAlias
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

New Features in C# 14

Extension Members

Extension members let you add new methods, properties, or events to existing types without modifying the original type definition. This is especially useful for adding functionality to third-party libraries.

Extension Methods, Supported for a Long Time

public static class StringExtensions
{
    public static bool ContainsNumber(this string str)
    {
        return str.Any(char.IsDigit);
    }
}

The New Extension Member Syntax

public static class StringExtensions
{
    // Extension block for string
    extension(string source)
    {
        // Extension property:
        public bool ContainsNumber => source.Any(char.IsDigit);

        // Extension method:
        public string Repeat(int count)
        {
            return string.Concat(Enumerable.Repeat(source, count));
        }
    }

    // Static extension block for string
    extension(string)
    {
        // Static extension method:
        public static string Combine(string first, string second, string? separator = null)
        {
            return $"{first}{separator}{second}";
        }

        // Static extension property:
        public static string SpaceChar => " ";

        // Static user defined operator:
        public static string operator * (string str, int count)
        {
            return string.Concat(Enumerable.Repeat(str, count));
        }
    }
}

Usage examples:

string text = "Hello";

Console.WriteLine(text.ContainsNumber); // False
Console.WriteLine(text.Repeat(3)); // HelloHelloHello

string combined = string.Combine("Hello", "World", ", ");
Console.WriteLine(combined); // Hello, World

Console.WriteLine(string.SpaceChar); // " "

string repeated = "Hi" * 3;
Console.WriteLine(repeated); // HiHiHi

The New field Keyword

private string _msg;
public string Message
{
    get => _msg;
    set => _msg = value ?? throw new ArgumentNullException(nameof(value));
}

can be simplified to:

public string Message
{
    get => field;
    set => field = value ?? throw new ArgumentNullException(nameof(value));
}

Unbound Generic Types and nameof

In C# 14, the nameof operator now supports unbound generic types. This allows you to get the name of a generic type without specifying type arguments.

Console.WriteLine(nameof(List<>)); // Output: List
Console.WriteLine(nameof(List<int>)); // Output: List
Console.WriteLine(nameof(Dictionary<,>)); // Output: Dictionary

Custom Compound Assignment

C# 14 allows you to define compound assignment operators for custom types. This makes the code more concise and improves readability.

public class Point(int x, int y)
{
    public int X { get; set; } = x;

    public int Y { get; set; } = y;

    public static Point operator +(Point a, Point b)
    {
        return new Point(a.X + b.X, a.Y + b.Y);
    }

    public static Point operator ++(Point p)
    {
        return new Point(p.X + 1, p.Y + 1);
    }
}

Usage examples:

Point p1 = new Point { X = 1, Y = 2 };
Point p2 = new Point { X = 3, Y = 4 };

Point p3 = p1 + p2; // Output: p3.X = 4, p3.Y = 6
p1++; // Now p1.X = 5, p1.Y = 7

Null-Conditional Assignment

Before C# 14, you had to null-check a variable before assigning to a property:

if (obj != null)  // or obj is not null
{
    obj.Property = newValue();
}

can be simplified to:

obj?.Property = newValue();