Back to: C# New Features Tutorials
C# 12 New Features with Examples:
In this article, I will give an overview of C# 12’s New Features with Examples. C# 12 was introduced alongside .NET 8 and brings many features to improve performance, developer productivity, code clarity, and API extensibility. These enhancements are particularly valuable for building modern applications for desktop, web, cloud, or mobile platforms.
The focus of C# 12 is to help developers write less code to do more, while ensuring better runtime performance and scalable, maintainable architectures. The following are the main features in C# 12:
- Primary Constructors for All Types
- Collection Expressions
- [InlineArray(n)] Attribute
- Default Parameters in Lambda Expressions
- Ref Readonly Parameters
- Alias Any Type
- Experimental Attribute
- Interceptors
System Requirements to Test C# 12 New Features:
To test the new C# 12 features, you must have the appropriate versions of the .NET SDK and Visual Studio. Here is what you will need:
.NET SDK
For C# 12 features, you will need the .NET 8 SDK, as C# 12 was introduced alongside .NET 8.
- Required Version: .NET 8 SDK or later (C# 12 is a part of this release)
- Download Link: https://dotnet.microsoft.com/en-us/download/dotnet/8.0
Ensure that you have the correct version installed by checking the SDK version with the following command in your terminal or command prompt: dotnet –version. This should output a version starting with 8.x.x.
Visual Studio for Windows
To work with C# 12, Visual Studio 2022 (version 17.8 or later) is required. This version fully supports .NET 8 and the new C# 13 features, including improved IntelliSense and tooling for working with new language features.
- Required Version: Visual Studio 2022 version 17.8 or later.
- Download Link: https://visualstudio.microsoft.com/downloads/
Primary Constructors for All Types
Primary constructors are now available for all class and struct types, enabling constructor parameters to be declared inline with the type declaration. These parameters remain in scope throughout the type and can be used for field or property initialization.
namespace CSharp12NewFeatures { public class Person(string name, int age) { public string Name { get; } = name; public int Age { get; } = age; } public class Program { static void Main(string[] args) { var person = new Person("John", 30); Console.WriteLine($"{person.Name}, {person.Age}"); } } }
Use Case: Allowing constructor parameters to be used directly within the type reduces repetitive or standard code and enhances readability. Please click here for more detailed information about Primary Constructors in C# with Real-time Examples.
Collection Expressions
A new unified syntax [ … ] simplifies the initialization of collections such as arrays, lists, and spans. The spread operator (..) can include elements from other collections.
namespace CSharp12NewFeatures { public class Program { static void Main(string[] args) { // Array int[] numbers = [1, 2, 3]; // List<T> List<string> colors = ["Red", "Green", "Blue"]; // Span<T> Span<double> measurements = [1.5, 2.3, 3.7]; // Spread operator to combine int[] more = [.. numbers, 4, 5]; Console.WriteLine(string.Join(", ", more)); // 1, 2, 3, 4, 5 } } }
Use Case: Streamlines the syntax for collection creation, making code more readable and concise by unifying collection initialization patterns. Please click here for more detailed information about Collection Expressions in C# with Real-time Examples.
[InlineArray(n)] Attribute
The [InlineArray(n)] attribute allows fixed-size buffers inside structs to be defined for high-performance, allocation-free memory usage.
using System.Runtime.CompilerServices; namespace CSharp12NewFeatures { [InlineArray(3)] public struct SmallBuffer { private int _element0; } public class Program { static void Main(string[] args) { var buffer = new SmallBuffer(); buffer[0] = 10; buffer[1] = 20; Console.WriteLine(buffer[1]); // Output: 20 } } }
Use Case: Enables efficient memory management and faster access in performance-critical applications that require fixed-size arrays. Please click here for more detailed information about the [InlineArray(n)] attribute in C# with Real-time Examples.
Default Parameters in Lambda Expressions
Lambda expressions can now have default parameter values, similar to methods, enhancing flexibility in their use.
namespace CSharp12NewFeatures { public class Program { static void Main() { // Lambda with default parameter for 'greeting' var greet = (string name, string greeting = "Hello") => { Console.WriteLine($"{greeting}, {name}!"); }; // Using default greeting greet("John"); // Defaults to "Hello" // Providing a custom greeting greet("Jane", "Good morning"); // Custom: "Good morning" } } }
Use Case: This allows for more concise and reusable lambda expressions, especially when defaults are commonly used in certain scenarios. Please click here for more detailed information about Default Parameters in Lambda Expressions in C# with Real-time Examples.
Ref Readonly Parameters
The ref readonly modifier allows passing variables by reference while ensuring they cannot be modified, providing read-only access to reference types.
namespace CSharp12NewFeatures { public class Program { static void Main() { int num = 10; PrintValue(ref num); } public static void PrintValue(ref readonly int value) { // value cannot be modified here Console.WriteLine(value); } } }
Use Case: This improves API clarity where in or ref would have been awkward, especially for performance-sensitive scenarios. Please click here for more detailed information about the ref read-only modifier in C# with Real-time Examples.
Alias Any Type
The using directive can now alias any type, including tuples, arrays, pointer types, and others, not just named types.
namespace CSharp12NewFeatures { using IntList = System.Collections.Generic.List<int>; public class Program { static void Main() { IntList numbers = new IntList { 1, 2, 3, 4 }; Console.WriteLine(string.Join(", ", numbers)); } } }
Use Case: Simplifies complex type signatures and enhances code readability by allowing aliasing of a wider variety of types. Please click here for more detailed information about Alias Any Type in C# with Real-time Examples.
Experimental Attribute
The [Experimental] attribute can be applied to methods or types to indicate that they are experimental and subject to change. If used without opting in, this attribute triggers a compiler warning.
using System.Diagnostics.CodeAnalysis; namespace CSharp12NewFeatures { public class Program { [Experimental("ThisMethodIsExperimental")] public static void ExperimentalMethod() { Console.WriteLine("This method is experimental."); } static void Main() { ExperimentalMethod(); // Error: method is marked experimental } } }
Use Case: Helps developers manage and track experimental or unstable APIs during development. Please click here for more detailed information about the [Experimental] attribute in C# with Real-time Examples.
Interceptors
Interceptors allow developers to hook into method calls and dynamically modify behavior at compile time using source generators. This feature is currently in preview.
using System.Reflection; namespace CSharp12NewFeatures { // Define a custom interceptor attribute [AttributeUsage(AttributeTargets.Method)] public class LogInterceptorAttribute : Attribute { // Define the action to be executed before the method call public void BeforeInvocation(string methodName) { Console.WriteLine($"Entering method: {methodName}"); } // Define the action to be executed after the method call public void AfterInvocation(string methodName) { Console.WriteLine($"Exiting method: {methodName}"); } } // Example class to demonstrate method interception public class OrderService { [LogInterceptor] public void CreateOrder(int orderId) { // Simulate some business logic Console.WriteLine($"Creating order with ID: {orderId}"); } } // A method to dynamically call a method and apply interception public class MethodInterceptor { public static void InvokeWithInterceptor(object target, string methodName, params object[] parameters) { // Get the method info for the target object MethodInfo method = target.GetType().GetMethod(methodName); // Check if the method exists and has the LogInterceptor attribute if (method != null) { // Retrieve the custom attribute safely var attribute = (LogInterceptorAttribute)method.GetCustomAttribute(typeof(LogInterceptorAttribute)); if (attribute != null) { // Invoke the 'BeforeInvocation' logic before calling the method attribute.BeforeInvocation(methodName); } // Invoke the actual method method.Invoke(target, parameters); if (attribute != null) { // Invoke the 'AfterInvocation' logic after calling the method attribute.AfterInvocation(methodName); } } else { Console.WriteLine($"Method '{methodName}' not found on target object."); } } } public class Program { static void Main(string[] args) { var orderService = new OrderService(); // Using MethodInterceptor to invoke the method with the interceptor applied MethodInterceptor.InvokeWithInterceptor(orderService, "CreateOrder", 101); } } }
Use Case: Provides advanced code generation and method interception capabilities, enabling dynamic behavior modification in certain methods. Please click here for more detailed information about Interceptors in C# with Real-time Examples.
In the next article, I will discuss Primary Constructors for All Types in C# with Examples. In this article, I explain the Overview of C# 12 New Features with Examples. I want your feedback. Please post your feedback, questions, or comments about this article.