What is new in C# 12?

C# 12 is the new C# version available in .NET 8. Since .NET and C# are backward compatible with old versions, we can combine the new syntax with other features included in C# 11, C# 10, and older.

Let’s review each feature in C# 12 with an example:

InLine Arrays

This is a new way to create arrays in C# using the attribute [System.Runtime.CompilerServices.InlineArray], where we need to specify the size (how many positions we need in the array) and add a property with the primitive type (int, double, string, etc…) where the value will be saved. But by default, each position will have the default value defined for the primitive type; for example, for int type, I will be 0.

// InLine Arrays

var array = new MyArray();

array[0] = 2;

foreach (var item in array)
{
    Console.WriteLine(item);
}


[System.Runtime.CompilerServices.InlineArray(5)]
public struct MyArray
{
    private int _element;
}

Default parameters in lambda functions

Now we can define an optional parameter in lambda functions, setting a default value for the parameter. In the following example, we set one as a default value for the parameter increment. The function will increase by one when we don’t set a value for this parameter.

//default parameters in lambda functions
var IncrementBy = (int source, int increment = 1) => source + increment;

Console.WriteLine(IncrementBy(5)); // 6
Console.WriteLine(IncrementBy(5, 2)); // 7

Params parameter in lambda functions

Using params, we can dynamically receive parameters with the same type in our functions or methods; this is now supported for lambda functions also:

// Params parameter in lamba functions
var sum = (params int[] values) =>
{
    int sum = 0;
    foreach (var value in values)
        sum += value;

    return sum;
};

Console.WriteLine(sum(1, 2, 1, 1, 1, 1)); // 7
Console.WriteLine(sum(1, 1)); // 2

Primary constructor

In simple classes, it is now possible to declare the constructor with simplicity. This is a feature included for records, and it’s available for classes as well.

Before C# 12

public class Person
{
    public string Name { get; set; }
    public string LastName { get; set; }
    
    public void Person(string name, string lastname)
    {
      Name = name;
      LastName = lastname;
    }
}

After C# 12

// primary constructor

Person newPerson = new Person("Miguel", "Teheran");

public class Person(string name, string lastname)
{
    public string Name { get; set; } = name;
    public string LastName { get; set; } = lastname;
}

Alias any type

Using the “alias any type” feature, we can create an alias for a primitive value to improve the readability or for a class with a long name to make the code shorter.

In the following example, we can use the alias City to clarify what we are saving in the collection, which is a List of strings.

// Alias any type
using City = string;

List<string> CityList = new List<City>();

CityList.Add("New York");
CityList.Add("Bogotá");
CityList.Add("Lima");

foreach (City city in CityList)
{
    Console.WriteLine(city);
}

Interceptors

With this new feature, we can intercept any line of code in runtime and change the logic to customize the expected behaviors.  This is useful in case we need to support a specific scenario for an operative system and avoid exceptions. But also, this could be confusing and can bring challenges to identifying issues or unexpected behaviors in production environments.

First, we need to enable this option by adding this line to our project file:

<Features>InterceptorsPreview</Features>

Then we need to create a new attribute InterceptsLocationAttribute.cs,

namespace System.Runtime.CompilerServices
{
    [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
    public sealed class InterceptsLocationAttribute(string filePath, int line, int character) : Attribute
    {

    }
}

This is the following example of the code; we have a simple HelloWorld class with a simple HelloWorld method that prints in the terminal HelloWorld.

// interceptors

static class Program
{
    public static void Main()
    {
        HelloWord helloWord = new HelloWord();
        helloWord.HelloWorld();
        Console.ReadLine();
    }
}

class HelloWord
{
    public void HelloWorld() => Console.WriteLine("Hello World");
}

The idea now is to create an interceptor to change line 8, which we call the HelloWorld method in the program class. To achieve this, we must create an interceptor in the following way:

using System.Runtime.CompilerServices;

namespace ConsoleApp1
{
    static class Interceptor
    {
        [InterceptsLocation("C:\\Personal\\demonet8\\ConsoleApp1\\Program.cs", line: 15, character: 17)]
        public static void InterceptorMethod(this HelloWord c)
        {
            c.HelloWorld();
            Console.WriteLine("Hi!");
        }
    }
}

Let's enhance the display functionality by including the file path, line number, and character position. Instead of the ordinary "Hello, World", we will now proudly showcase "Hi!" because of the interceptor.