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.

  • Muhammad Miftah 8

    using type aliases seem to only work within the file, unless you define it as a global using, but then the global scope is polluted.
    I think you should introduce a namespace-wide equivalent, maybe introduce a type keyword for that? Also it would be extra cool to also have the option to reify the type alias into something that exists and is reflection-queryable at runtime. Also usable in generics and generic type constraints. TypeScript already has this!

    • Kathleen DollardMicrosoft employee 1

      Interesting ideas. If we did any of those things it would be a separate feature. The great thing about our current release cadence is that we can thoughtfully, stepwise improve core features.

      It would be awesome if you entered these ideas in a new discussion at https://github.com/dotnet/csharplang.

  • Anthony Steiner 5

    Mixed feeling on the primary constructors, need further testing. Giving any type an alias is something I always wanted so thanks on that. Default values on lambdas is great but not something I’d be using a lot. Now I’m really really waiting for Semi-auto-properties, Collection Literals and ofc Collection Literals, because I do really think that C# type inference should be at least somewhat close to the ones we have in C++ or dart.

    • Kathleen DollardMicrosoft employee 1

      I hope you’ll experiment with primary constructors and see how they feel in use.

      I’m glad you like the other features in this preview. Look for more of the features you’re looking for in the next preview!

  • Steve Dunn 4

    Type aliases are fantastic! You can take them a step further with a NuGet package named ‘Vogen’; it allows you to define them to be a Value Object meaning you can’t new one up, or default one, or confuse one with its primitive. Very similar performance to using primitives too (e.g. no need to derive from a base class)

    https://github.com/SteveDunn/Vogen

    Probably the most useful aspect is the additional compilation errors generated by the analyzer to ensure that you don’t accidentally create an instance with an invalid value. The only way to create them is via the `From` method meaning that can’t bypass the (optional) validation, e.g. `CustomerId id = CustomerId.From(42);`

    // catches object creation expressions
    var c = new CustomerId(); // error VOG010: Type 'CustomerId' cannot be constructed with 'new' as it is prohibited
    CustomerId c = default; // error VOG009: Type 'CustomerId' cannot be constructed with default as it is prohibited.
    
    var c = default(CustomerId); // error VOG009: Type 'CustomerId' cannot be constructed with default as it is prohibited.
    var c = GetCustomerId(); // error VOG010: Type 'CustomerId' cannot be constructed with 'new' as it is prohibited
    
    var c = Activator.CreateInstance(); // error VOG025: Type 'CustomerId' cannot be constructed via Reflection as it is prohibited.
    var c = Activator.CreateInstance(typeof(CustomerId)); // error VOG025: Type 'MyVo' cannot be constructed via Reflection as it is prohibited
    
    // catches lambda expressions
    Func f = () => default; // error VOG009: Type 'CustomerId' cannot be constructed with default as it is prohibited.
    
    // catches method / local function return expressions
    CustomerId GetCustomerId() => default; // error VOG009: Type 'CustomerId' cannot be constructed with default as it is prohibited.
    CustomerId GetCustomerId() => new CustomerId(); // error VOG010: Type 'CustomerId' cannot be constructed with 'new' as it is prohibited
    CustomerId GetCustomerId() => new(); // error VOG010: Type 'CustomerId' cannot be constructed with 'new' as it is prohibited
    
    // catches argument / parameter expressions
    Task t = Task.FromResult(new()); // error VOG010: Type 'CustomerId' cannot be constructed with 'new' as it is prohibited
    
    void Process(CustomerId customerId = default) { } // error VOG009: Type 'CustomerId' cannot be constructed with default as it is prohibited.
    • Toke N. 3

      Hi Steve – first of, thanks for creating Vogen. It’s a great library to solve primitive obsession and I have been using it for some time. (I created something similar doing source generation from types define in JSON prior to switching to Vogen due to the awesome analyzers and ease of use.)

      But you wrote “Type aliases are fantastic!” and with you background in Vogen, I’m actually a bit unsure why it’s fantastic? Could you elaborate on what’s fantastic here? It doesn’t seem to bring any of the value Vogen brings as far as I can tell from the discussion on Github. (Ie. it isn’t type safe, doesn’t allow validation etc.) So I’m a bit curious as to that statement by you. 🙂 Not that I’m against the feature – it probably has a few valid usecases, but it doesn’t seem to solve any of the issues Vogen solves.

  • JinShil 5

    Please put some attention towards completing existing language features that were cut from previous releases. One in particular is this:

    https://github.com/dotnet/csharplang/blob/6b5a397cc4faef6d481d9bace13d7e3c19de63bb/meetings/2019/LDM-2019-04-29.md

    Default interface implementations and base() calls

    Conclusion
    Cut base() syntax for C# 8. We intend to bring this back in the next major release.

    That “next major release” with this feature never happened.

    Another incomplete feature is the ability to use the file access modifier on more than types.

    • Kathleen DollardMicrosoft employee 1

      Pinging on the issue or creating a new one at https://github.com/dotnet/csharplang is a great way to bring up an issue. This is a feature that was considered and we did not pursue at the time. I cannot find a proposal. We discuss things in LDM that we do not follow up on, and the specific language here is unfortunate – the intent was probably “bring it back up”.

      Also, our design process has changed due to shorter scheduled release cycles. This allows us to think of evolving features over many releases, rather than trying to bundle a huge issue together. There is no aspect of C# that I would consider “finished” and I think this leads to a gentler evolution. It also means a feature like the file access modifier can be released for the known case (types) and then we can get feedback on any additional evolution.

      • MgSam 1

        Far worse than not implementing parts of DIM is the fact that the docs on MSDN about DIMs are completely inaccurate and reflect a version of the spec that is not what was actually implemented. It is totally crazy to me that a language used by millions of developers has no accurate documentation anywhere about a major type system change introduced 4 years ago!

        • Kathleen DollardMicrosoft employee 1

          Can you clarify “docs on MSDN”? Our docs are at Microsoft Learn and if there is something inaccurate, please file an issue which you can do from a button on the screen.

          • Paulo Pinto 2

            Everyone knows that MSDN is the old name for Microsoft Learn, nice way to avoid the question.

            Many new C# features get documented on Microsoft Learn as a plain dump from the original Github proposal.

          • Kathleen DollardMicrosoft employee 1

            (@Paulo, I cannot answer you directly, I assume because the thread has become too deep)

            Asking what was intended was an honest question, because DIM is documented here: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/interface with a tutorial here: https://learn.microsoft.com/en-us/dotnet/csharp/advanced-topics/interface-implementation/default-interface-methods-versions. If there is something incorrect on those pages, please file an issue.

            @MgSam I believe you are referring to the spec linked to from the pages above. DIM is a posted features spec, not a proposed or promulgated spec and as such does need to be considered in combination with other notes like the Language Design Meeting notes, which are linked at the bottom of the feature spec. I have just filed an issue on that page that we should put a note on all feature spec pages.

            Over the last few years, we have evolved our documentation to avoid duplication, which was often out of date. Thus, the references page relies on the spec. For some previous versions , this is problematic because we had a hole in our spec process we are working to fill, and C# 8 bears the worst of this. The spec process, appropriately, includes people from outside Microsoft and can only move at the speed in which it can move. Work has been done both on current C# versions and on patching the hole, but it is a challenging process.

            @Paulo During C# 11, we overhauled the way we create “What’s new” content, specifically moving if from devs to docs. There were two problems: Devs understand the feature from the perspective of the spec so it tended to repeat the spec, and documentation was duplicated as it also landed in the correct spot in docs. There have been growing page as we hit our stride on this new approach, which is also imperfect. Now it is imperfect in that we will sometimes have “preview” docs in that they may not be completed before preview release. Let me know if you think the recent What’s new still feels like a dump from the proposal.

          • MgSam 0

            Hi @KathleenDollard,

            I’m referring to https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-8.0/default-interface-methods, which is the top search result for Default Interface Methods and is the only official documentation on it as far as I’m aware. It talks about using base. calls, which as we know was not implemented. It also does not mention a few extremely common pitfalls that the feature has.

            DIMs have been a major source of confusion in the community with multiple StackOverflow questions about people not understanding them. I filed a Github issue in the C# lang repo on this months ago; it was ignored by the team until Rich Lander came and unceremoniously closed it without any resolution because apparently it’s not polite to report bugs/problems any longer? It really is frustrating to have the MS team treat community members who are trying to help in this way.

          • Paulo Pinto 1

            What a strange reply system.

            Anyway, although the language reference has been improved since I last looked at it, we still have a draft reference for C# 7, C# 11 “documentation” that still refers to the feature as if it was a proposal,

            https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/introduction

            https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-11.0/static-abstracts-in-interfaces

            I think these two examples suffice to make my point, no need to point out every single one.

            Now compare this documentation with Java 20, or Python 3.11, just came out, all JEP and PEP proposals properly integrated into the regular language reference,

            https://docs.oracle.com/javase/specs/jls/se20/html/index.html

            https://docs.python.org/3/reference/index.html

            Certainly Microsoft has the resources to improve the docs team staff.

      • JinShil 0

        The issue about incomplete Default Interface Methods can be found at https://github.com/dotnet/csharplang/issues/2337

        Every time a new blog post about upcoming language features is published, there’s too much emphasis on superficial features instead of taking things across the finish line. Finishing things and removing arbitrary limitations should be the priority.

        • Kathleen DollardMicrosoft employee 0

          Could you ping that issue. There has been only one comment since 2020. It really helps us track what affecting people. It is easier to track if you put a new comment – it can be as simple as “I want you to do this”. Upvoting is also good.

          • JinShil 0

            I already pinged that issue on February 24, 2022 after you suggested it last time I brought it up when the C#11 blog post was published with its superficial features. I upvoted it and so did others. What are you suggesting now? That I ping it again? Why don’t you just tell me to go pound sand?

            Here’s an idea: Try keeping a list of unfinished features that need to be revisited periodically, add “finish DIM”, and follow through before adding any more superficial syntax.

        • saint4eva 1

          It seems you just learnt a new word – superficial. Lol

  • André Köhler 9

    I’m not sure “Primary constructors for non-record classes and structs” will be very useful for me. I fear this feature will further complicate the language. I don’t want C# to end up being as complicated as C++ when it comes to finding out where a member variable is initialized (if ever).

    “Using aliases for any type” sounds very useful. Often I did not use value tuples because because I had to repeat the long tuple name because I do not like to use the ‘var’ keyword because I prefer explicit types so my compiler gives me errors whenever I change the code somewhere and forget to change it elsewhere. Now I can just define an alias for my tuple types, that’s great.

    Not sure I ever felt a need for “Default values for lambda expression parameters”, but it can’t hurt either.

  • Ian Marteens 8

    The most common use for primary constructors is missed with this implementation. You won’t create properties, but it’s fine for you creating hidden fields and diluting all the initialization along the whole class. The most commonly stated reason for primary constructors is “oh, they avoid you to initialize this.j = k”. But with this contraption, you still can write:

    public int J { get; } = k;

    C# last versions are part of a saga on diminishing returns. I would prefer no primary constructors to this ill-conceived feature, honestly.

    • Kathleen DollardMicrosoft employee 0

      Just for clarification: are you saying that you would prefer us not do this feature unless it creates and initializes public properties for every parameter?

      • Ian Marteens 2

        Yes, that’s right. Ok, I know the mantra: “if won’t hurt you if you don’t use it”. But the fact is that it would be a missed opportunity: it will be THE way C# implements the feature.

        It’s not just the public properties: it’s the spreading of initialization along the full class body. What you had in a single point, easy to check, you’ll now have it dispersed.

        • Kathleen DollardMicrosoft employee 0

          Thank you.

    • anonymous 0

      this comment has been deleted.

  • David Raška 26

    Please stop for god’s sake. This “we must invent something new every year no mather what“ policy is really getting out of hand.
    What a nice, simple and powerfull language it was few years ago, then you added some nice and useful syntactic sugar. And then somebody showed up and invented dozens ways of object initialization. Stop polluting C#. Stop this madness. Keep it simple, stupid.

    • PAthum Bandara Premaratne 1

      my point exactly…. initially they build it now breaking. donno what’s wrong with these people.

    • Paulo Pinto 1

      Just imagine how C# 20 will look like.

      • Jim Foye 1

        I believe that’s when they will add default lambda automatic property generic interface missing non-nullable parameter constructors.

    • Alexander Selishchev 1

      Years go by and in 2023 we are unable to see what caused null reference exception in chained calls 😄but primary costructors, thats what the team will do

  • Stilgar Naib 8

    I don’t like the primary constructor thing specifically that it does not generate properties like in records. The records primary constructor feature would be super useful in normal classes because we still can’t replace them with records in many places (Blazor bindings, EF, etc.) It looks like this feature targets DI but I was never bothered that I have to inject 3-4 things and assign them to properties. The real problem is when I have a class with 20 properties and I need to type them multiple times

    The using alias is cool but it would be cooler if we could get an actual type alias where assignment between different aliases is compiler error so we could easily do things like strongly typed Ids

  • Eugene TolmachevMicrosoft employee 6

    Aliasing types lets you abstract the actual types

    Types let you abstract things, aliases don’t do any abstracting. F# had this feature forever and tbh it’s a plague.
    Here’re some of the problems with aliases
    – the error message – when you pass (string, int) instead of (string,string) the error is unhelpful in pointing out the real difference.
    – the alias is not a type, so anything “shaped” like what you expect can still be passed in – it offers no safety whatsoever. So what if your alias says “Account”, you can still pass Address into it!

    Primary constructors lead towards “cheap” real types, so those are welcome! But even better would be zero-cost types (erased types), something like UoMs in F#.

    Default parameters in lambdas – I feel like having an easy way to supply default value for a nullable anywhere is a better feature and it doesn’t require language changes. Default param values have known issues (wrt versioning) and extending that into lambdas will just make the problem surface bigger.

    To add regarding default param values and lambdas – there’s currently a problem converting lambda to expression tree if method you are calling inside a lambda has a param with default, you have to supply the value regardless otherwise you get a compiler error. Wish that was addressed instead, otherwise we have to look forward to more of the same problems.

    /2c

  • Gabriel Halvonik 7

    Honestly, single one feature I was looking forward was semi-auto properties (with anonymous interface implementations we will probably never see), but it seems to be suspended…again. That is really unfortunate, that something that feels so natural to use and has high demand is left behind features that most users don’t appreciate. But overall I like it that language is regularly developed and get it that everyone can’t be satisfied.

    • Kathleen DollardMicrosoft employee 6

      Semi-auto properties are not in this preview, but are still planned for .NET 8.

Feedback usabilla icon