Whats-New-in-.NET-9

.NET 9 brings a bunch of exciting updates that make it faster, safer, and easier to use. It improves performance, handles memory better, and makes working with collections simpler. You’ll find helpful features like tools for trimming code, better ways to manage collections, and new LINQ methods. Plus, it adds safer ways to handle cryptography, makes working with time more convenient, and processes data more efficiently. Let’s dive in to see how these new features make .NET 9 a great choice for developers!

Key Features:

1.  Feature Switches with Trimming Support: .NET 9 introduces the FeatureSwitchDefinitionAttribute and FeatureGuardAttribute to help reduce app size when trimming, making it easier to exclude dead code based on feature flags.

2. UnsafeAccessorAttribute for Generics: The UnsafeAccessorAttribute now supports generic parameters, enabling unsafe access to private members of generic types.

3. Garbage Collection (DATAS): Dynamic Adaptation to Application Sizes (DATAS) adjusts the heap size automatically to optimize memory usage, which is now enabled by default.

4. Performance Enhancements: .NET 9 brings various optimizations, including loop, inlining, and PGO improvements, along with ARM64 vectorization and faster exceptions.

New Libraries:

1. Base64Url Encoding: A new Base64Url class simplifies working with URL-safe Base64 encoding.]

2. Removal of BinaryFormatter: The BinaryFormatter has been removed to encourage safer serialization practices.

3. Improved Collections: New methods and types like OrderedDictionary<TKey, TValue>, ReadOnlySet<T>, and updates to PriorityQueue improve performance and flexibility.

4. Cryptography: Enhanced encryption algorithms, including the introduction of the KMAC algorithm, along with improvements in key management and support for OpenSSL providers.

5. TimeSpan Class Enhancements: .NET 9 adds new overloads to TimeSpan that allow creating instances from integers, improving precision and avoiding errors that arise from floating-point values.

6. LINQ Enhancements: New methods like CountBy, AggregateBy, and Index streamline collection operations and improve efficiency by eliminating the need for intermediate groupings.

1. Attribute model for feature switches with trimming support

The Attribute model for feature switches with trimming support in .NET allows you to define feature switches that can enable or disable functionality, helping reduce app size when trimming or compiling with Native AOT (Ahead-of-Time compilation).

Key Attributes:

1. FeatureSwitchDefinitionAttribute: Used to treat a feature-switch property as a constant during trimming, allowing dead code guarded by the switch to be removed. For example, if a feature is disabled in the project settings, related code will be removed:

				
					if (Feature.IsSupported) 
    Feature.Implementation();
				
			

2. FeatureGuardAttribute: Used for guarding code that requires attributes like RequiresUnreferencedCode, RequiresAssemblyFiles, or RequiresDynamicCode.

This ensures that feature-related code is properly handled when trimming:

				
					[FeatureGuard(typeof(RequiresDynamicCodeAttribute))] 
internal static bool IsSupported => RuntimeFeature.IsDynamicCodeSupported;
				
			

Example:

If the feature Feature.IsSupported is set to false in the project file, the corresponding implementation is removed when trimming, optimizing the app size:

				
					<ItemGroup> 
  <RuntimeHostConfigurationOption Include="Feature.IsSupported" Value="false" Trim="true" /> 
</ItemGroup>
				
			

2. UnsafeAccessorAttribute supports generic parameters

The UnsafeAccessorAttribute in .NET allows unsafe access to private members (fields or methods) of a class. Initially introduced in .NET 8, this feature lacked support for generic parameters. .NET 9 adds support for generic parameters in both CoreCLR and Native AOT scenarios, enabling more flexibility in accessing generic type members.

Example:

In the following code, the UnsafeAccessorAttribute is used to access private fields and methods of a generic class:

				
					public class Class<T> 
{ 
    private T? _field; 
    private void M<U>(T t, U u) { } 
} 
 
class Accessors<V> 
{ 
    [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_field")] 
    public extern static ref V GetSetPrivateField(Class<V> c); 
 
    [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "M")] 
    public extern static void CallM<W>(Class<V> c, V v, W w); 
} 
 
internal class UnsafeAccessorExample 
{ 
    public void AccessGenericType(Class<int> c) 
    { 
        ref int f = ref Accessors<int>.GetSetPrivateField(c); 
        Accessors<int>.CallM<string>(c, 1, string.Empty); 
    } 
}
				
			

In this example, GetSetPrivateField and CallM are used to access and modify private members in a Class<int> object, with support for generic type parameters.

3. Garbage Collection

Dynamic Adaptation to Application Sizes (DATAS) is a feature in .NET that automatically adjusts the application’s heap size based on its memory requirements, ensuring that the heap size is roughly proportional to the long-lived data size. This improves memory efficiency by preventing excessive heap growth while maintaining performance. DATAS was introduced as an opt-in feature in .NET 8 and has been enhanced in .NET 9, enabling automatic heap size management by default.

Example:

In .NET 9, DATAS is enabled by default, meaning applications will dynamically adjust memory usage based on the amount of long-lived data they manage. This results in more efficient memory usage without requiring manual tuning by developers.

For more details, you can refer to the Dynamic Adaptation to Application Sizes (DATAS) documentation.

4. Performance Improvement

What’s New in .NET 9 Libraries

1. Base64Url

Base64 is an encoding scheme that converts binary data into a text format, using a set of 64 characters. This encoding is commonly used for transferring data, as it’s supported by many methods like Convert.ToBase64String. However, Base64 includes characters like ‘+’ and ‘/’, which can conflict with URL encoding because they have special meanings in URLs. 

To address this issue, the Base64Url scheme was created, which uses an alternate set of characters that are URL-safe. In .NET 9, a new Base64Url class was introduced, making it easier to work with Base64Url encoding and decoding. 

Example Code:

				
					ReadOnlySpan<byte> bytes = ...; 
string encoded = Base64Url.EncodeToString(bytes); 
				
			

This example shows how to use the Base64Url.EncodeToString method to encode a byte array into a Base64Url string, suitable for use in URLs.

2. BinaryFormatter

In .NET 9, the BinaryFormatter has been removed from the runtime. Although the API still exists, its implementation now always throws an exception, making it unusable. This change encourages developers to migrate to safer and more efficient serialization methods. A migration guide is available to help users transition from BinaryFormatter to alternative approaches.

Example:

If you try using BinaryFormatter in .NET 9, it will throw an exception:

				
					var formatter = new BinaryFormatter(); 
formatter.Serialize(stream, obj);  // This will throw an exception in .NET 9
				
			

Developers are advised to use alternatives like System.Text.Json or Newtonsoft.Json for serialization tasks.

3. Collections

The collection types in .NET gain the following updates for .NET 9: 

Collection Lookups with Spans 

In high-performance scenarios, spans are used to avoid unnecessary string allocations. In .NET 9, with the new allows ref struct feature in C# 13, it’s now possible to perform lookups on collection types like Dictionary<TKey, TValue> using spans. This helps optimize memory usage and performance in lookup-intensive code.

Example:

				
					private static Dictionary<string, int> CountWords(ReadOnlySpan<char> input) 
{ 
    Dictionary<string, int> wordCounts = new(StringComparer.OrdinalIgnoreCase); 
    Dictionary<string, int>.AlternateLookup<ReadOnlySpan<char>> spanLookup = 
        wordCounts.GetAlternateLookup<ReadOnlySpan<char>>(); 
 
    foreach (Range wordRange in Regex.EnumerateSplits(input, @"\b\w+\b")) 
    { 
        ReadOnlySpan<char> word = input[wordRange]; 
        spanLookup[word] = spanLookup.TryGetValue(word, out int count) ? count + 1 : 1; 
    } 
 
    return wordCounts; 
}
				
			

OrderedDictionary<TKey, TValue>

The OrderedDictionary<TKey, TValue> allows both ordered storage of key-value pairs and efficient key-based lookups. In .NET 9, a generic version of OrderedDictionary is introduced, improving type safety and efficiency.

Example:

				
					OrderedDictionary<string, int> d = new() 
{ 
    ["a"] = 1, 
    ["b"] = 2, 
    ["c"] = 3, 
}; 
 
d.Add("d", 4); 
d.RemoveAt(0); 
d.RemoveAt(2); 
d.Insert(0, "e", 5); 
 
foreach (KeyValuePair<string, int> entry in d) 
{ 
    Console.WriteLine(entry); 
} 
// Output: 
// [e, 5] 
// [b, 2] 
// [c, 3]
				
			

PriorityQueue.Remove() Method 

.NET 6 introduced PriorityQueue<TElement, TPriority>, but it lacked efficient priority updates. The new PriorityQueue<TElement, TPriority>.Remove() method in .NET 9 allows emulating priority updates, making it suitable for algorithms like Dijkstra’s algorithm in certain contexts.

Example:

				
					public static void UpdatePriority<TElement, TPriority>( 
    this PriorityQueue<TElement, TPriority> queue, 
    TElement element, 
    TPriority priority 
) 
{ 
    queue.Remove(element, out _, out _);  // Remove the element 
    queue.Enqueue(element, priority);     // Re-enqueue with new priority 
}
				
			

ReadOnlySet<T> 

.NET 9 introduces ReadOnlySet<T>, a read-only wrapper for mutable sets (ISet<T>), complementing ReadOnlyCollection<T> and ReadOnlyDictionary<TKey, TValue> for other collections.

Example:

				
					private readonly HashSet<int> _set = new(); 
private ReadOnlySet<int>? _setWrapper; 
 
public ReadOnlySet<int> Set => _setWrapper ??= new ReadOnlySet<int>(_set);
				
			

4.  Cryptography

The advancement of cryptography in .NET 9 strengthens data security by enhancing encryption algorithms and improving key management processes.

CryptographicOperations.HashData() Method 

The CryptographicOperations.HashData() method in .NET 9 allows for one-shot hashing or HMAC operations using a specified HashAlgorithmName. This simplifies hashing operations, as it eliminates the need for conditional logic based on the algorithm, improving performance and reducing allocations.

Example:

				
					static void HashAndProcessData(HashAlgorithmName hashAlgorithmName, byte[] data) 
{ 
    byte[] hash = CryptographicOperations.HashData(hashAlgorithmName, data); 
    ProcessHash(hash); 
}
				
			

KMAC Algorithm 

.NET 9 introduces the KMAC (KECCAK Message Authentication Code) algorithm, as specified by NIST SP-800-185. KMAC is a pseudorandom function based on KECCAK, supporting both one-shot and accumulated MAC generation. It’s available on Linux (OpenSSL 3.0 or later) and Windows 11 (Build 26016 or later).

Example:

				
					if (Kmac128.IsSupported) 
{ 
    byte[] key = GetKmacKey(); 
    byte[] input = GetInputToMac(); 
    byte[] mac = Kmac128.HashData(key, input, outputLength: 32); 
} 
else 
{ 
    // Handle scenario where KMAC isn't available. 
}
				
			
  • X.509 Certificate Loading 

.NET 9 introduces the X509CertificateLoader class to replace older, less secure certificate loading methods. This class provides a more secure, one-method-one-purpose design for certificate loading, supporting two widely compatible formats.

  • OpenSSL Providers Support 

.NET 9 enhances support for OpenSSL providers, including the ability to use SafeEvpPKeyHandle.OpenKeyFromProvider for interacting with providers such as TPM2 or PKCS11. This allows for secure key management with hardware security modules (HSM).

Example:

				
					byte[] data = [ /* example data */ ]; 
 
// Using a TPM provider 
using (SafeEvpPKeyHandle priKeyHandle = SafeEvpPKeyHandle.OpenKeyFromProvider("tpm2", "handle:0x81000007")) 
using (ECDsa ecdsaPri = new ECDsaOpenSsl(priKeyHandle)) 
{ 
    byte[] signature = ecdsaPri.SignData(data, HashAlgorithmName.SHA256); 
    // Use signature created by TPM. 
}
				
			

This method improves performance during the TLS handshake and interaction with RSA private keys using ENGINE components.

5. TimeSpan Class Enhancements in .NET9

The TimeSpan class in .NET 9 introduces new overloads for creating TimeSpan objects from integers. This is particularly useful to avoid precision issues that can arise when using double values, as the floating-point format can lead to slight inaccuracies. For example, TimeSpan.FromSeconds(101.832) might result in a time span that isn’t exactly 101 seconds and 832 milliseconds but instead a slightly imprecise value. 

The new overloads allow you to directly specify days, hours, minutes, seconds, milliseconds, and microseconds using integer values, providing a more accurate and efficient way to create TimeSpan objects.

Example:

				
					TimeSpan timeSpan1 = TimeSpan.FromSeconds(value: 101.832); 
Console.WriteLine($"timeSpan1 = {timeSpan1}"); 
// Output: timeSpan1 = 00:01:41.8319999 
 
TimeSpan timeSpan2 = TimeSpan.FromSeconds(seconds: 101, milliseconds: 832); 
Console.WriteLine($"timeSpan2 = {timeSpan2}"); 
// Output: timeSpan2 = 00:01:41.8320000
				
			

In this example, timeSpan1 shows the floating-point imprecision, whereas timeSpan2 demonstrates the new integer-based overload, providing a precise value.

6. LINQ

LINQ (Language Integrated Query) has received enhancements in .NET 9, making querying collections even easier and more powerful, particularly with the introduction of new query operators and better async LINQ support.

.NET 9 introduces new methods CountBy, AggregateBy, and Index to streamline collection operations and avoid unnecessary intermediate groupings, offering more efficient ways to process data.

1. CountBy: This method allows you to quickly calculate the frequency of each key in a collection, similar to GroupBy, but without allocating intermediate groupings.

Example: Find the most frequent word in a text:

				
					string sourceText = """ 
    Lorem ipsum dolor sit amet, consectetur adipiscing elit. 
    Sed non risus. Suspendisse lectus tortor, dignissim sit amet,  
    adipiscing nec, ultricies sed, dolor. Cras elementum ultrices amet diam. 
"""; 
 
KeyValuePair<string, int> mostFrequentWord = sourceText 
    .Split(new char[] { ' ', '.', ',' }, StringSplitOptions.RemoveEmptyEntries) 
    .Select(word => word.ToLowerInvariant()) 
    .CountBy(word => word) 
    .MaxBy(pair => pair.Value); 
 
Console.WriteLine(mostFrequentWord.Key); // Output: amet
				
			

AggregateBy: This method lets you perform custom aggregation by key, replacing the need for GroupBy when you need to aggregate values in a more general-purpose manner.

Example: Aggregate scores by ID:

				
					(string id, int score)[] data = 
    [ 
        ("0", 42), 
        ("1", 5), 
        ("2", 4), 
        ("1", 10), 
        ("0", 25), 
    ]; 
var aggregatedData = 
    data.AggregateBy( 
        keySelector: entry => entry.id, 
        seed: 0, 
        (totalScore, curr) => totalScore + curr.score 
    ); 
foreach (var item in aggregatedData) 
{ 
    Console.WriteLine(item); 
} 
// Output: 
// (0, 67) 
// (1, 15) 
// (2, 4)
				
			

Index: This method provides a quick way to obtain the index of each item in a collection. It simplifies the process of iterating through elements with their indices.

Example: Iterate through lines in a file with their line numbers:

				
					IEnumerable<string> lines2 = File.ReadAllLines("output.txt"); 
foreach ((int index, string line) in lines2.Index()) 
{ 
    Console.WriteLine($"Line number: {index + 1}, Line: {line}"); 
}
				
			

These methods enhance data manipulation efficiency and readability in common workflows like counting occurrences, aggregation, and indexing.

Leave a Reply

Your email address will not be published. Required fields are marked *