C# 12 New Features

As every year around this time, let's take a look at the list of new features introduced in the latest version of C#, in this case, the new features of C# 12. There are still a couple of weeks until the version is officially "released," but all the new features have already been announced, so let's take a look.

 

 

1 - Primary constructors for class and Struct

Let's start with primary constructors. You may ask, what is a primary constructor? Well, if you work with records in C#, you know that we can specify the “parameters” directly in the record definition, instead of creating the properties/fields separately.

 

Basically, this is a primary constructor. Until now, this was only possible with records, and now it can also be used in classes and structs:

//Normal:public class User{    public string Name { get; set; }    public string Email { get; set; }}//Primary constructors:public class User(string Name, string Email);

This change may seem trivial but I always use it when working with records, so I hope to do the same with classes and structs. Not to mention the amount of boilerplate code we're going to avoid writing thanks to this.

 

By the way, this means your class will no longer have a default empty constructor. And having primary constructors doesn't block us from creating more constructors or adding additional logic in the class itself.

 

 

2 - Default values in lambda expressions

From now on, we'll be able to set default values when defining a lambda. Imagine we have a delegate that receives a value and concatenates it into a string:

var buildString = (string s) => $"El valor es {s}";string result = buildString("test");

Now, we can set a default value in the lambda:

var buildString = (string s = "test") => $"El valor es {s}";string result = buildString();

 

It's a small change that at first glance may not seem too important, but for me it adds consistency to the language— if you can specify default values in a regular method, why not in a lambda?

 

 

3 - Type aliases

For a long time we've been able to use an alias for a class when there's another class with the same name in a different namespace. You've probably come across code like this:

using MyUser= namespace.dentro.de.mi.poryecto.User;

This is known as an alias. From C# 12, we'll be able to define types in the same way. Let me clarify: we declare a name as we do now, and simply specify a name and define which properties it will have:

using CustomUser = (string Nombre, string Apellido);using UserIds = int[];

As you can see, it acts as a tuple, and personally that's the only use case I find valuable.

 

Sometimes you need to create a whole class just to pass 3 or 4 arguments to a method, but the method signature gets too long so you end up copying a tuple several times. With this, that problem is solved, since you'll only need to define it once.

Personally, with primary constructors, I find that a private inner class is more useful than this, but anyway, I hope this feature doesn't end up being overused everywhere— since in my opinion, it could make code a bit unmaintainable.

 

 

4 - Expressions in collections

In the past few versions of C#, we've seen improvements in how collections and expressions are declared. In C# 11, we saw list patterns, which mainly use the spread operator, the two dots ...

 

Among other changes, you can now easily concatenate multiple arrays using that operator:

int[] row0 = [1, 2, 3];int[] row1 = [4, 5, 6];int[] row2 = [7, 8, 9];int[] single = [..row0, ..row1, ..row2];foreach (var element in single){    Console.Write($"{element}, ");}// Output:// 1, 2, 3, 4, 5, 6, 7, 8, 9, 

Since there are many collection-related features, here's the original Microsoft blog.

 

5 - InlineArray attribute in C# 12

Let's stop for a moment with Inline arrays. I wasn't sure whether to include this feature because, honestly, I'm not sure who will actually use it.

 

Inline arrays are a C# attribute that allows us to define a sequence of primitives. As with any array, you need to know the size, but not when you instantiate the array—it's at compile time that its size is set:

[InlineArray(10)]public struct TestArray{    private int _element;}//UsageTestArray array = new TestArray();//Iterates the same as a regular arrayfor (int i = 0; i < 10; i++){    array[i] = i;}foreach (var i in array){    Console.WriteLine(i);}

Because its contents must be primitive types, it allows creating the InlineArray as a struct, and thus it will be stored on the stack, not on the heap, making it more efficient.

Now that it's explained, as I said before, who will use it? Maybe beyond videogame developers?

 

 

6 - Interceptors in C# 12

C# 12 introduces interceptors, which feels like déjà vu since, well, interceptors already exist. We've seen them on this blog in Entity Framework, and if I recall correctly, they also exist in .NET Framework, though honestly, I never used them.

But these in C# 12 are different and only share the name. Once again, in my opinion, Microsoft could have come up with a better name.

 

So, what are they actually? An interceptor is simply a method that replaces another method so it can call itself. This means the code we write replaces the existing one.

And this substitution occurs at compile time, not during runtime—which is good, because if it was runtime... well, hacks everywhere.

 

Let's look at a simple example. First, we must enable the feature in our project:

<InterceptorsPreviewNamespaces>$(InterceptorsPreviewNamespaces);Interceptors</InterceptorsPreviewNamespaces>

This means it's in preview mode, so it's not officially released and could change. Now, we need a class that is an attribute where we'll create our InterceptsLocationAttribute:

namespace System.Runtime.CompilerServices{    [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]    sealed class InterceptsLocationAttribute : Attribute    {        public InterceptsLocationAttribute(string filePath, int line, int character)        {        }    }}
  • Note: as you can see, we've specified a particular namespace, because that attribute will exist in the future, it just doesn't yet, so we include it ourselves for now in our own project.

 

Now, we should implement the actual interception, meaning we need to specify the code that will be replaced. To do this, we add the attribute, which includes the file (full path), the line, and the column of the first letter of the method to be replaced:

public static class Interceptors{     [InterceptsLocation(@"C:\Repos\csharp12\Interceptors\Program.cs", line: 3, character: 25)]    public static int Interceptor1(this Operation obj, int a, int b)    {        Console.WriteLine("esto es el primer interceptor");        return a * b;    }     [InterceptsLocation(@"C:\Repos\csharp12\Interceptors\Program.cs", line: 4, character: 25)]    public static int Interceptor2(this Operation obj, int a, int b)    {        Console.WriteLine("esto es el segundo interceptor");        return a - b;    }}

What we can see is that it's a static class, and the static method is the one that replaces the code we use, and it also receives a "this" parameter of the type we are replacing:

 

For this example, let's replace how this method is used:

public class Operacion{    public int Sumar(int a, int b)    {        return a + b;    }}

And finally, here is how it's used:

Console.WriteLine("Hello, World!");Operacion operacion = new Operacion();var result1 = operacion.Sumar(3, 5);var result2 = operacion.Sumar(10, 5);Console.WriteLine($"resultado1 {result1}");Console.WriteLine($"resultado2 {result2}");

Once we've compiled, we can see in the IL code that we have the calls to the methods we created:

interceptors c# 12

There are a few things here that bother me:

 

First is the path: for now, it must be absolute, which means you need a fixed place for the project, and if someone else clones your code, the path must be the same, which doesn't make much sense.

And then the usual risks of including malicious code or overwriting functionality, but anyway, not too worrying since this works at compile time.

By the way, interceptors can only overwrite code in your own project, not from NuGet packages. Which, in my opinion, would be ideal. To be honest, I don’t see the usefulness of this feature yet.

 

 

7 - Dependency injection with keys

With this new version of C#, we now have access to what's called keyed services in dependency injection.

 

You may be wondering, what is a keyed service? It's simple: when we inject an interface multiple times in dependency injection, what happens internally is that we get a list. For example, with the following code, it inserts a list of IEmailProvider:

builder.Services.AddScoped<IEmailProvider, GmailProvider>();builder.Services.AddScoped<IEmailProvider, HotmailProvider>();

And then, if we just inject it, we get the last one injected. But if we retrieve the services manually, we see that we can get either the first with GetService or the list.

But now we have the ability to use AddKeyedService, which means we can specify a key, an identifier for our service:

builder.Services.AddKeyedScoped<IEmailProvider, GmailProvider>("gmail");builder.Services.AddKeyedScoped<IEmailProvider, HotmailProvider>("hotmail");

And then, to get the service, we just specify the identifier:

IEmailProvider emailProvider = app.Services.GetKeyedService<IEmailProvider>("gmail");

 

Of course, if we're in a service, we can inject the one we want to use:

public class SendEmail{    private readonly IEmailProvider _emailProvider;    public SendEmail([FromKeyedServices("gmail")]IEmailProvider emailProvider)    {        _emailProvider = emailProvider;    }}

 

This is very, very useful. Previously, you'd have to inject the whole list and then look for the one you wanted, as shown in this code from my library distribt.

But now you may wonder, what does this solve in the real world? A basic example is this: imagine you have the following code:

public class SendEmail{    private readonly IEmailProvider _emailProvider;    public SendEmail(IEmailProvider emailProvider)    {        _emailProvider = emailProvider;    }    public async Task<bool> Execute(string subject, string body, string to)    {        //validations, etc        await _emailProvider.SendEmail(subject, body, to);        return true;    }}

A class "SendEmail" that sends emails using your trusted provider, Gmail. But, for reason X, you want to migrate to Hotmail.

 

Previously, there were several ways to approach this—one is to create an IHotmail interface that inherits from IEmailProvider and inject that, another is to create a new interface IHotmailProvider that doesn't inherit from the other and inject that, and so on. With keyed services, we can inject both services with the correct interface by simply indicating which one, so migrations like this are much easier and don't "pollute" the rest of the code.

public class SendEmail{    private readonly IEmailProvider _originalEmailProvider;    private readonly IEmailProvider _newEmailProvider;    public SendEmail([FromKeyedServices("gmail")]IEmailProvider originalEmailProvider,         [FromKeyedServices("hotmail")]IEmailProvider newEmailProvider)    {        _originalEmailProvider = originalEmailProvider;        _newEmailProvider = newEmailProvider;    }    public async Task<bool> Execute(string subject, string body, string to)    {        try        {            //validations, etc            await _newEmailProvider.SendEmail(subject, body, to);            return true;        }        catch (Exception e)        {            //validations, etc            Console.WriteLine($"el nuevo provider no funciona {e.Message}");            await _originalEmailProvider.SendEmail(subject, body, to);            return true;        }    }}



And that's it, those are all the new features in the new version of C# 12!

If you liked the post, don't forget to share!


Best regards

This post was translated from Spanish. You can see the original one here.
If there is any problem you can add a comment bellow or contact me in the website's contact form

Uso del bloqueador de anuncios adblock

Hola!

Primero de todo bienvenido a la web de NetMentor donde podrás aprender programación en C# y .NET desde un nivel de principiante hasta más avanzado.


Yo entiendo que utilices un bloqueador de anuncios como AdBlock, Ublock o el propio navegador Brave. Pero te tengo que pedir por favor que desactives el bloqueador para esta web.


Intento personalmente no poner mucha publicidad, la justa para pagar el servidor y por supuesto que no sea intrusiva; Si pese a ello piensas que es intrusiva siempre me puedes escribir por privado o por Twitter a @NetMentorTW.


Si ya lo has desactivado, por favor recarga la página.


Un saludo y muchas gracias por tu colaboración

© copyright 2025 NetMentor | Todos los derechos reservados | RSS Feed

Buy me a coffee Invitame a un café