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 form | Alias |
|---|---|
| dotnet package add | dotnet add package |
| dotnet package list | dotnet list package |
| dotnet package remove | dotnet remove package |
| dotnet reference add | dotnet add reference |
| dotnet reference list | dotnet list reference |
| dotnet reference remove | dotnet 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();