Check out new C# 12 preview features!

Kathleen Dollard

We’re excited to preview three new features for C# 12:

  • Primary constructors for non-record classes and structs
  • Using aliases for any type
  • Default values for lambda expression parameters

In addition to this overview, you can also find detailed documentation in the What’s new in C# article on Microsoft Learn.

To test out these features yourself, you can download the latest Visual Studio 17.6 preview or the latest .NET 8 preview. Find out what else is coming for developers in Announcing .NET 8 Preview 3 and other posts on the .NET blog.

Primary constructors for non-record classes and structs

Primary constructors let you add parameters to the class declaration itself and use these values in the class body. For example, you could use the parameters to initialize properties or in the code of methods and property accessors. Primary constructors were introduced for records in C# 9 as part of the positional syntax for records. C# 12 extends them to all classes and structs.

The basic syntax and usage for a primary constructor is:

public class Student(int id, string name, IEnumerable<decimal> grades)
{
    public Student(int id, string name) : this(id, name, Enumerable.Empty<decimal>()) { }
    public int Id => id;
    public string Name { get; set; } = name.Trim();
    public decimal GPA => grades.Any() ? grades.Average() : 4.0m;
} 

The primary constructor parameters in the Student class above are available throughout the body of the class. One way you can use them is to initialize properties. Unlike records, properties are not automatically created for primary constructor parameters in non-record classes and structs. This reflects that non-record classes and structs often have more complexity than records, combining data and behavior. As a result, they often need constructor parameters that should not be exposed. Explicitly creating properties makes it obvious which data is exposed, consistent with the common use of classes. Primary constructors help avoid the boilerplate of declaring private fields and having trivial constructor bodies assign parameter values to those fields.

When primary constructor parameters are used in methods or property accessors (the grades parameter in the Student class), they need to be captured in order for them to stay around after the constructor is done executing. This is similar to how parameters and local variables are captured in lambda expressions. For primary constructor parameters, the capture is implemented by generating a private backing field on the class or struct itself. The field has an “unspeakable” name, which means it will not collide with other naming and is not obvious via reflection. Consider how you assign and use primary constructor parameters to avoid double storage. For example, name is used to initialize the auto-property Name, which has its own backing field. If another member referenced the parameter name directly, it would also be stored in its own backing field, leading to an unfortunate duplication.

A class with a primary constructor can have additional constructors. Additional constructors must use a this(…) initializer to call another constructor on the same class or struct. This ensures that the primary constructor is always called and all the all the data necessary to create the class is present. A struct type always has a parameterless constructor. The implicit parameterless constructor doesn’t use a this() initializer to call the primary constructor. In the case of a struct, you must write an explicit parameterless constructor to do if you want the primary constructor called.

Find out more in the What’s new in C# 12 article with links to all new content for primary constructors.

You can leave feedback on primary constructors in the CSharpLang GitHub repository at Preview Feedback: C# 12 Primary constructors.

Using directives for additional types

C# 12 extends using directive support to any type. Here are a few examples:

using Measurement = (string, int);
using PathOfPoints = int[];
using DatabaseInt = int?;

You can now alias almost any type. You can alias nullable value types, although you cannot alias nullable reference types. Tuples are particularly exciting because you can include element names and types:

using Measurement = (string Units, int Distance);

You can use aliases anywhere you would use a type. For example:

public void F(Measurement x)
{ }

Aliasing types lets you abstract the actual types you are using and lets you give friendly names to confusing or long generic names. This can make it easier to read your code.

Find out more in the What’s new in C# 12 article.

You can leave feedback on aliases for any type in the CSharpLang GitHub repository at Preview Feedback: C# 12 Alias any type.

Default values for lambda expressions

C# 12 takes the next step in empowering lambda expressions by letting you specify default values for parameters. The syntax is the same as for other default parameters:

var addWithDefault = (int addTo = 2) => addTo + 1;
addWithDefault(); // 3
addWithDefault(5); // 6

Similar to other default values, the default value will be emitted in metadata and is available via reflection as the DefaultValue of the ParameterInfo of the lambda’s Method property. For example:

var addWithDefault = (int addTo = 2) => addTo + 1;
addWithDefault.Method.GetParameters()[0].DefaultValue; // 2

Prior to C# 12 you needed to use a local function or the unwieldy DefaultParameterValue from the System.Runtime.InteropServices namespace to provide a default value for lambda expression parameters. These approaches still work but are harder to read and are inconsistent with default values on methods. With the new default values on lambdas you’ll have a consistent look for default parameter values on methods, constructors and lambda expressions.

Find out more in the article on What’s new in C# 12.

You can leave feedback on default values for lambda parameters in the CSharpLang GitHub repository at Preview Feedback: C# 12 Default values in lambda expressions.

Next steps

We hope you will download the preview and check out these features. We are experimenting in C# 12 with a dedicated issue for each feature. We4 hope this will focus feedback and make it easier for you to upvote what other people are saying. You can find these at Preview Feedback: C# 12 Primary constructors, Preview Feedback: C# 12 Alias any type, and Preview Feedback: C# 12 Default values in lambda expressions.

You can follow our implementation progress for C# 12 at Roslyn Feature Status. You can also follow the design process at the CSharpLang GitHub repository where you will find proposals, discussions and meeting notes.

We look forward to hearing from you!

71 comments

Discussion is closed. Login to edit/delete existing comments.

  • gc y 2

    Will “safe fixed buffer” become part of C# 12?

    • Kathleen DollardMicrosoft employee 0

      We are discussing it, which you can watch in the LDM notes. We would like it to be part of C# 12, but have not yet settled the details. Some of the details are challenging.

      • Marcin Rybak 0

        Would limiting it to unmanaged structs make it simpler? This would add a lot of value, for people who deal with low level network datagrams, IoT and embedded. Like struct MyDatagram { public Point points[10]; }. Fingers crossed!

  • Assis Zang 0

    Nice features! Thanks for sharing! 😀

  • Max Mustermueller 12

    For me C# is getting more and more complex on every version. It took a long time until I figured out when to use classes and when structs. And I’m not alone, look at stackoverflow, there are top votes questions about the differences. Then records got added. I never used them, I still have no idea what their purpose it. The new switch syntax is so complicated to write, I always write the normal one and let Visual Studio convert it. When should we use “in” on parameters? Never used it, never saw VS recommended, never had a problem not using it but for some reason it has been added to the language. “Out” I only use on winapi calls. I really don’t know what kind of problems your team is trying to solve every year but it appears to me that your just trying to add something just so you added something. Or your team is listen to a minority who always ask for something as they feel it might be cool to have without thinking about it.

    I feel like there are now hundreds of ways to do the same thing. This is not helpful. This makes it just more complicated and only raises more questions about “When I should I use” or “Whats the difference between”. Please focus on solving real world issues and stop reinvent the wheel.

    • 심승용 [seungyongshim] 0

      Make wheel, again.. again.. again.. lol

  • Andrey Goran 7

    You MS guys are killing the language. And you are doing it fast. What a fantastic, clear and simple language it used to be, and what it is now. When reading modern C# code in GitHub I often simply do not understand what’s going on. I would need to spend 1 month each year just to keep up with new features. It took me couple weeks to learn C# 2 when I moved from Java, and with C# 12 it would take a year. You do not understand simple fact – software developers have millions of other things and really difficult concepts to keep in mind every day, our primary task is to build business logic and we fight its complexity and try to keep things simple every day. Now thanks to C# team we are adding even more complexity instead of fighting it. I have no hope about the future of C#, the damage already done cannot be fixed.

  • Stevie White 1

    Hi Kathleen,

    While I am having trouble finding the totality of usefulness for all these new features, I can foresee instances where I will like having them. My nitpicking concern here is more well-spoken in the other comments – I am not sure these new features will be intuitive enough to be appreciated by newer C# developers.

    That aside, thank you for the update and I appreciate the work you and the rest of the Dot Net team are accomplishing. I’ll look forward to trying these features out once .NET 8 reaches GA. 🙂

  • David Taylor 0

    I wanted to respond with a thank you Kathleen.

    I have been hoping this exact primary constructor feature would be added for a number of years. Anyone who heavily uses dependency injection, (which is used throughout ASP.NET) will love this feature. This will greatly improve the readability of my projects and given I have noted some negative reactions in the comments, I wanted to take the time to say this will be a great feature for our team.

  • Daniel Smith 1

    Rather than multiple language features that just add different syntax for doing the same things, I’d much rather the team focused on a core set of feature changes that add significant value (e.g. like when generics were added).

    I would gladly ditch all of these recent alternative syntax features for Extension Everything (e.g. extension properties etc). This has massive community backing, and it’s something that’ll add huge value to C#. We got a little bit of it with extension operators recently, and that opened the gates for generic math which was a huge win. Imagine what could be done if extension everything was fully embraced!

  • Raymond Lei 0

    Does type aliasing support anonymous type?

Feedback usabilla icon