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.

  • Michael Taylor 18

    I see no benefit in any of these features for regular code personally. But then again I’ve felt that way for C# features for a while now. The language has become a lot like C++ where minor syntax differences can result in major behavior changes. For example as a teacher I see students incorrectly put constructor parameters on the class declaration instead of a constructor all the time. Fortunately, up until now, it was a compiler error. Now it is going to work there but fail on other constructors and I’m going to have to explain to them a concept that is beyond what they are struggling with now, which is constructors.

    The language team is clearly catering to a small set of users who feel that feature X, Y and Z would save them a handful of keystrokes but is just complicating the language for now reason. It really needs to stop as the language is almost as complex as C++.

    The only feature mentioned that is remotely useful is type aliasing but that isn’t really even true type aliasing. Honestly wouldn’t it be easier to just add a language feature to define a typedef that maps one identifier to another? It could be local (like here) or global (perhaps prefixed by global). It is trivial for the compiler to swap identifier names during name resolution and would eliminate this hack for “complex type names”.

    • PAthum Bandara Premaratne 1

      I really hope that Kathleen read 👆 this…. ✊ talk to real n hardcore developers….. get their views stop catering to celebrity developers…. they won’t get you anywhere.

      • Kathleen DollardMicrosoft employee 3

        “stop catering to celebrity developers…. “

        I think about this a lot, although not in those terms. Instead, I think about how difficult it is to hear the people that do not talk to us. It is true that many of the features we do require some effort to understand while we are designing them. We think a lot about niche cases and long term implications to the design space. But we work to keep that process open to everyone by doing it at https://github.com/dotnet/csharplang.

        Do you have thoughts on how we can engage more “real hardcore developers”?

        We also balance the “you are moving to fast” with “you are not moving fast enough”

        • Jim Foye 3

          If you go back and read the comments on these blog posts over the years, you’ll learn a lot. For example, there were literally thousands of comments about how much everyone hate the new “Metro” look of Visual Studio when it came out. The thing is, MS folks choose to ignore this kind of feedback (which shouldn’t even be necessary – shouldn’t they know before releasing something like that how much folks are going to hate it?). I can also point to many issues on the website (which I cannot name, because I never go there anymore) where people suggest new features for Visual Studio and these suggestions received lots of votes and then were closed. Hell, I can’t get you guys to fix a simple WPF designer issue that has prevented me from upgrading to VS 2022. Fact is, you guys are not listening to us. That’s why you don’t “hear us”.

    • Kathleen DollardMicrosoft employee 0

      One of the things we considered with primary constructors is that it does feel more natural to put parameters on the class – and that leads to your students doing it. This is not just a matter of keystrokes.

      Considering ways to each construction and initialization, in the end is it not simpler to go with the students intuition, and does this allow delaying teaching construction/initialization which is non trivial with init scope, limitations on what can be used in initialization, object initializers and required, etc?

      This is an honest question. We absolutely think about the impact of language changes on folks new to C# and new to programming.

      • Michael Taylor 4

        Actually constructors are an advanced topic in C# after you’ve learned how to define classes. Why? Because you generally don’t need constructors in C# (like you do in C++) because you can use object initializer syntax. I generally tell my students that if they are defining constructors on a class then either they have very specific initialization needs (ordering or something) or the properties are readonly. init sort of solves that but honestly we don’t even cover it because out of a 16 week course where class design is 3 weeks we cannot cover all the variants of property syntax (of which we currently have 3). The focus is on generally used features and init properties aren’t common in my experience.

        When we do introduce constructors we say “like a method except no return type and name matches type”. Then introduce constructor chaining and base constructors. Since students are already comfortable with methods at this point it is an easy leap in my experience. But with primary constructors we now have to teach “put the most specific constructor parameters on the class but all the specializations in their own constructors. Also don’t forget to have all constructors call the primary constructor”. The discussions I see coming out of this include “why do I put one set of constructor parameters here and all others in ctors”, “what if I need to make a slight change to the argument (or do validation)”, and “what do you mean by a class/type constructor (static), isn’t that what I did on the class declaration”.

        Yet another issue with primary constructors is how much code has to be adjusted when I can no longer use them. For example initially I may just have a primary constructor that inits the underlying fields. But string properties shouldn’t be null (not all of us agree with getting rid of null BTW) and therefore need to convert to empty string and other arguments may need to be validated since you shouldn’t be able to create an invalid object. As soon as I want to do any of this then I have to remove the primary ctor and create a normal one. And I should also probably consider removing the constructor chaining that I had to add on the other ctors to get it to compile to begin with. Hopefully you’ll provide a refactoring to switch from primary ctors to regular ctors. Also, in my experience, many classes expose a protected default ctor in addition to public parameterized ctors for unit testing (at least). So a primary ctor wouldn’t fit with this at all I believe.

        Again, I personally don’t see any benefit in primary ctors (or defaults on lambdas) and yes I can just ignore the feature. But it is still code that I have to understand when looking at other people’s code and answer questions related to if students bring it up.

        • Gunnar Dalsnes 7

          “But it is still code that I have to understand when looking at other people’s code and answer questions related to if students bring it up.”
          this!

        • Kathleen DollardMicrosoft employee 2

          We see both of these features as important to avoid questions of why records have primary constructors or why default values in lambdas have to be done with an ugly attribute.

          I hear you, and I realize that you are unlikely to be teaching positional records in your introductory classes. But for devs overall, these features are both intended to create a more consistent surface for C#.

          It is helpful to get this feedback that increasing the surface area is problematic. We also think about how to focus new and existing users to a logical subset of C#. editor.config and analyzers help a bit here.

        • saint4eva 0

          Defaults on Lambdas is used by ASP.NET Core Minimal APIs

  • PAthum Bandara Premaratne 2

    in this rate I’d be able to see c# 100 in my lifetime…. 😂 not sure how you guys doing the versioning…

    also KISS…. just don’t let that slip your mind…. whoever responsible for the c#s future….

    • Kathleen DollardMicrosoft employee 3

      That would be 87 years from now on our current schedule. I am slightly skeptical that we will still be evolving C# then.

      C# has a different start date than .NET. C# 12 corresponds with .NET 8 because there were 4 .NET Framework versions before .NET Core began. We version both every year. So, that answers how we do C# versioning.

      There is a language design team (LDM) that is led by Mads Torgersen. Many people whose names you would recognize are involved as either part of the LDM or our review circle.

  • Ken Smith 8

    Every C# developer in the universe: “What we’d really like in C# 12 is discriminated unions.”
    Microsoft: “Here’s some default values in lambda expressions.”
    Every C# developer: “…”
    Microsoft: “And you’ll LIKE IT.”

    • Gabriel Halvonik 2

      I find that feature can be useful. Primary constructors seems to be more controversial though.

    • Kathleen DollardMicrosoft employee 4

      First C# developer: “We want discriminated unions just like Typescript”

      Second C# developer: “We want discriminated unions just like F#, they are the same, right”

      C# designer: “No, actually they are quite different, and both are very interesting. Also, there are challenges with the existing type system that F# can paper over more easily because they were designed into the language. We are committed to them feeling right for C#.”

      First and second C# developer: “We don’t care. we want them anyway”

      C# designer: “So do we. We are working on it. We have been working on it. It’s hard (particularly interchanging types that you’d think could be exchanged)”

      Narrator: “Yes, C# designers can use parentheses when they speak. They use parentheses a lot.”
      …time elapses

      C# designer: “Lets rethink this from a perspective of what people want to do…”

      I think our most recent efforts in this space are super interesting. You can

      There are more discussions at https://github.com/dotnet/csharplang, but these may be the best links to start with to tease apart our most recent thinking from the long history of our efforts on this feature.

      We want to land a DU feature that C# developers love. I believe we will do that. Unfortunately, not in C# 12.

      • saint4eva 0

        Hahahha

  • Ken Kohler 9

    This looks like another unnecessary change for the sake of change. I see no value in this and it needlessly makes c# more complicated than it needs to be. I would not use this going forward.

  • Anthony Hernandez 11

    I’m with most everyone else on this, mostly around primary constructors. Who asked for those? The usefulness is marginal and it’ll just make the code more complicated to navigate and debug. Now I have to worry about my teams mixing and matching how they’re initializing properties? Please, no! Aliases for types is also marginally useful. In 99% of cases I’m already defining a proper type. If a file needs in-line aliases then it’s more than likely doing too much. Default parameters for lambdas is another “feature” in search of a problem. In 99% of cases I’m only using lambdas for simple in-line use cases. If I’m seeing lambdas being used where they’re being overloaded with default parameters I’m going to ask them to create a method instead. Bottom line, nothing here actually adds value, helps us be more productive, or write cleaner code.

  • Tony 2

    It seems primary constructors will greatly help with DI services. No need to write all those private read only fields and their assignments.
    I wonder how default values in lambdas will affect query strings parsing in minimal api

  • Pavel Voronin 0

    Can primary constructor have body? For example for validation?

    • Kathleen DollardMicrosoft employee 1

      Primary constructors cannot have a body in this version. We may include that at some point in the future.

      • Pavel Voronin 0

        One more thing which concerns me with PC.
        If I substitute class for record then constructor arguments automatically become properties but named in camelCase style.
        What if later you decide for the same semantics of records, that is for having init/readonly property?

        I’d propose for having an opportunity to distinguish these two cases: properties vs private/protected(?) readonly (?) fields.

        Readonly/init properties.

        public class Student(int Id, string Name, IEnumerable<decimal> Grades) { ... }
        

        Declared as readonly private field.

        public class Student(int _id, string _name, IEnumerable<decimal> _grades)
        OR
        public class Student(
            private readonly int _id, 
            private readonly string _name, 
            private readonly IEnumerable<decimal> _grades)
        {
           ...
        }
        

        Declared as protected readonly field

        public class Student(int #id, string #name, IEnumerable<decimal> #grades) { ... }
        OR
        public class Student(
            protected readonly int _id, 
            protected readonly string _name, 
            protected readonly IEnumerable<decimal> _grades) { ... }
        

        The versions with modifiers are verbose, and default is readonly in 99%, so, I’d maybe allow contextual keyword:

        name field becomes mutable and protected.

        public class Student(int _id, mutable string #name, IEnumerable<decimal> _grades) { ... }
        

        Visibility modifiers should affect only how names are visible inside the class, for constructor callers # and _ should be dropped:

        public class Student(int _id, mutable string #name, IEnumerable<decimal> _grades) { ... }
        

        is equivalent to

        public class Student
        {
            private readonly int _id;
            protected string _name;
            private readonly IEnumerable<decimal> _grades;
        
            public Student (int id, string name, IEnumerable<decimal> grades) { ... }
        }
        

        P.S.
        Maybe not even _ for private members, but some other character which would make identifier illegal in any other context, so whenever I attempt to switch to record from class, compiler will show me the error. Maybe `?

        public class Student(int `id, string `name, IEnumerable<decimal> `grades)
        
  • Heinrich Moser 4

    First, I’d like to thank you for continuing to improve C#. This continuous improvement is the main reason we switched from VB.NET to C# some time ago. (Nullable reference types and record types are awesome, and I’m already looking forward to all language features that help reduce INotifyPropertyChanged boilerplate.)

    That having been said, I share the skepticism around primary constructors for non-record types. I’ll try to explain why:

    1. They add a lot of complexity to the language, since they look like the primary constructors of record types, but behave differently in a non-obvious way. That would be OK, if they were a long-awaited feature solving an important problem.

    2. But are they? I have a hard time coming up with a use case for them. If I write (a) a simple data container class, I’ll just use a record type. On the other hand, if I write (b) a complex class, my “main constructor” doing all the initialization is usually private, invoked by other (simpler) constructors or (well-named) factory methods. I seldom have complex, non-record classes where the “main constructor” is public.

    3. Which leads to another (minor) point: The primary constructor is public, which is a deviation from the long-standing principle in C# that “no accessibility modifier” means “the least accessibility that makes sense”. Yes, I am aware that making it private by default would be awkward as well, and I don’t have an elegant solution either. It’s just that C# is such an elegant language that adding stuff which does not feel elegant just seems… wrong. (It’s OK and “feels right” for record types, since they are special types for a specific purpose.)

    There’s one caveat to my criticism: I don’t (or rather: very seldom) use “constructor dependency injection”. Some have speculated here in the comments that this is the intended use case for this feature. If this is the case and it solves a real problem: That’s great, please ignore my comment, proceed, and don’t let me stop you. If it isn’t: What use case did you have in mind for this feature? Which common problem is it supposed to solve that justifies the cost?

  • Gunnar Dalsnes 4

    I still miss static extension methods thou:-)

    • Richard Wickens 1

      Yes please! Do this – I have been waiting for this since extension methods came out.

Feedback usabilla icon