C# 12: Primary Constructors
In November 2023, Microsoft released .NET 8.0, and since then you can use C# 12.
So, it might be a good time for you to learn about the new features of the programming language of the year 2023. To learn the features, I start with this post a small series of blog posts.
In addition to this series about C# 12, you might also see some new posts about C# 11 features that I haven’t blogged about yet, and of course about other topics, like .NET, GitHub, TypeScript etc. But now, let’s focus on C# 12.
If you have followed this blog for a while, you might have noticed that I didn’t blog much in the past months. I’m still here, but I was very busy with my open source project MvvmGen, with my day-to-day job, with conference talks, with the creation of Pluralsight courses and of course with 3 kids (Kudos to my wife for all the help).
Thomas Claudius Huber in 2024
The plan for the future is to write on this blog again on a regular basis. If there’s something specific you’d love to read about, let me know in the comments or via my contact form.
C# 12 – What’s New?
There are many new features in C# 12, like primary constructors for classes/structs, collection expressions, the possibility to alias any type with a using, default parameters in lambda expressions and ref readonly parameters. You will learn about all of these in this blog post series. Now let’s start and let’s take a look at primary constructors.
C# 12 or C# 12.0?
Good point. There where versions in the past that had a decimal place, like for example C# 7.3 or C# 8.0. But since Microsoft is releasing a new version of .NET every year, a new version of C# is released every year too. So today, there are less reasons than before to use a decimal place for C#, as we get a new version every year. And according to the table that you find on this official page about the C# language versioning, version 10 was the first version that did not use a decimal place in the official documentation. This means that also C# 11 and C# 12 don’t use a decimal place there.
At the same time, I wouldn’t say that calling it C# 12.0 is wrong. And actually, messages in the code editor still call it 12.0. So, I guess, 12 is the marketing number, 12.0 is what it’s called from the tech perspective. But that’s just a detail. We all know, versioning is never easy. :-)
So this means of course that C# 12 == C# 12.0 is true.
(If you’re a JavaScript dev, here we go: C# 12 === C# 12.0)
Understand Primary Constructors
With C# 12, Microsoft introduces primary constructors for classes and structs. In the code snippet below you find a simple Person
class like you would create it before C# 12 – and of course it’s also still valid with C# 12. It has a constructor with a firstName
parameter. That firstName
parameter is used to initialize the FirstName
property.
public class Person
{
public Person(string firstName)
{
FirstName = firstName;
}
public string FirstName { get; set; }
}
Now, if you are on a .NET 8.0 project, it will support C# 12 by default. When you put the cursor on the Person
class’ constructor in Visual Studio, you can press CTRL+. to open the suggestions provided by the light bulb. As you can see in the screenshot below, the very first suggestion says Use primary constructor.
After the refactoring, you end up with a Person
class like you see it below that uses a primary constructor. The primary constructor is defined directly after the class name. From a syntactic point of view it is just a list of parameters inside of a pair of parentheses. It does not have a body. You can use the constructor parameters in the body of the class, for example to initialize members like properties and fields. Below you can see that the FirstName
property is initialized with the firstName
constructor parameter.
public class Person(string firstName)
{
public string FirstName { get; set; } = firstName;
}
So, you see, with primary constructors, your code gets much more compact. But what is C# actually doing behing the scenes? Let’s take a look at the generated Common Intermediate Language (CIL) code to which C# gets compiled. CIL is also known as just Intermediate Language (IL), and it was formerly called Microsoft Intermediate Language (MSIL).
Look at the Common Intermediate Language (CIL)
Let’s take a look at the CIL code of the Person
class below that uses a primary constructor to initialize the FirstName
property.
namespace CSharpTwelve;
public class Person(string firstName)
{
public string FirstName { get; set; } = firstName;
}
To look at the CIL code, I use the Intermediate Language Disassembler tool (ildasm.exe) that comes with Visual Studio 2022. You can open it via a Developer Command Prompt. There you type ildasm and press ENTER. In the screenshot below you can see the code for the generated constructor. That code first sets the backing field of the FirstName
property to the value of the firstName
constructor parameter. That backing field is automatically generated for an auto property behind the scenes to store the property’s value, and the FirstName
property is such an auto property. Next, the constructor calls the constructor of the object
class, which is in this case the base class of the Person
class, as you didn’t specify any other base class.
So, this means in other words that the generated constructor code first initializes all the members with the values of the constructor parameters, and after this, it calls the constructor of the base class.
Next, let’s take a look at calling a constructor of a base class.
Call a Constructor of a Base Class
Below you see a Developer
class that inherits from a Person
class. The Developer
class defines a primary constructor with the parameters firstName
and language
. It passes the firstName
parameter as an argument to the constructor of the Person
class. To call the constructor of the Person
class, a pair of parentheses is used after the Person
class, and it contains the arguments. In the code snippet below that’s the firstName
parameter.
public class Person(string firstName)
{
public string FirstName { get; set; } = firstName;
}
public class Developer(string firstName, string language) : Person(firstName)
{
public string WritesCodeWith => language;
}
Note that the base class constructor does not have to be a primary constructor. It can be any constructor. Below you see a code snippet in which the Person
class defines a regular constructor. The Developer
class has a primary constructor that calls the one of the Person
class. This means, the Developer
class below is exactly the same as the one in the code snippet above.
public class Person
{
public Person(string firstName)
{
FirstName = firstName;
}
public string FirstName { get; set; }
}
public class Developer(string firstName, string language) : Person(firstName)
{
public string WritesCodeWith => language;
}
So, you see, calling a base class constructor is very simple and very straight forward.
Next, let’s take a look at constructor overloads.
Create Constructor Overloads
Also with a primary constructor in place, you can define any number of constructor overloads in your class. But with primary constructors, there is one rule: Any other constructor must call the primary constructor with the this
keyword. This is necessary to ensure that the parameters of the primary constructor are assigned when you access them anywhere in the class, for example to initialize a property.
In the code snippet below you see the Developer
class with another constructor that has also a lastName
parameter. Note how that constructor calls the primary constructor with this(firstName, language)
.
public class Developer(string firstName, string language) : Person(firstName)
{
public Developer(
string firstName,
string language,
string lastName) : this(firstName, language)
{
LastName = lastName;
}
public string WritesCodeWith => language;
public string? LastName { get; set; }
}
Access Parameters in The Class
So far, you’ve seen in the code snippets of this blog post that you can access the parameters of the primary constructor to initialize properties. But the truth is that you can access them anywhere in the body of your class. This makes them also quite convinient when you build classes into which you want to inject dependencies. Let’s take a look at a simple example.
Let’s create an interface for a repository that can load developers:
public interface IDeveloperRepository
{
Developer[] LoadDevelopers();
}
Next, let’s add an implementation. Of course, you could load the developers from a Web API, from a database or from anywhere else. As it doesn’t matter, I create them just in memory:
public class DeveloperRepository : IDeveloperRepository
{
public Developer[] LoadDevelopers()
{
return [
new Developer("Thomas","C#"),
new Developer("Julia","TypeScript"),
new Developer("Anna","Python")
];
}
}
Now let’s say that you want to build a DeveloperViewModel
class that needs to load developers. So, it depends on the IDeveloperRepository
interface. Of course, you can define it with a primary constructor like you see it in the code snippet below. In the Load
method, the constructor parameter with the name repository
is used to load the developers and to print them to the console.
public class DeveloperViewModel(IDeveloperRepository repository)
{
public void Load()
{
var developers = repository.LoadDevelopers();
foreach (var dev in developers)
{
Console.WriteLine($"{dev.FirstName}: {dev.WritesCodeWith}");
}
}
}
This means, now the DeveloperViewModel
can be used as you see it below. When you call its Load
method, it loads the developers via the passed in DeveloperRepository
instance and it writes the developers to the console.
var viewModel = new DeveloperViewModel(new DeveloperRepository());
viewModel.Load();
Again, What Is Going on Behind the Scenes?
In the previous section you created the DeveloperViewModel
class that you see below. It contains a primary constructor with a repository
parameter. The repository
constructor parameter is used in the Load
method to load some developers and to write them to the console.
public class DeveloperViewModel(IDeveloperRepository repository)
{
public void Load()
{
var developers = repository.LoadDevelopers();
foreach (var dev in developers)
{
Console.WriteLine($"{dev.FirstName}: {dev.WritesCodeWith}");
}
}
}
Now the question is: How is it possible that the DeveloperViewModel
can access the repository
constructor parameter in its Load
method? Doesn’t this require that the parameter value is stored in a field of the class? Indeed. When you take a look at the class with ildasm.exe, you can see that a private field is generated behind the scenes. That field is used to store the value of the repository
constructor parameter, so that it can be used in the Load
method.
Note that the private field is not generated for every constructor parameter. The C# compiler decides if it is necessary or not. In this example, it is necessary, as you access the repository
constructor parameter in the Load
method. If you use a constructor parameter just to initialize a property, then no field will be created for that parameter.
But these are just details of what is going on behind the scenes with the C# compiler. For you as a developer, it means that you can use primary constructor parameters anywhere in the body of your class. If it is necessary to store the value in a field, the C# compiler will just do this.
Is the Generated Field Readonly?
If the C# compiler generates a field behind the scenes, that field is not readonly. This means that you can change the value of the field in the body of your class. In the following code snippet you can see a Person
class with a firstName
constructor parameter. As the parameter is used in the getter and setter of a property, a firstName
field is generated. The generated firstName
field is changed in the setter of the FirstName
property. This works, as the field is not readonly.
public class Person(string firstName)
{
public string FirstName
{
get => firstName;
set => firstName = value;
}
}
Instead of using a setter like in the code snippet above, you can change the field of course in any method. The code snippet below changes it in a ChangeFirstName
method.
public class Person(string firstName)
{
public void ChangeFirstName(string newName)
{
firstName = newName;
}
}
Now, does this mean that primary constructors are not for you if you want to use readonly fields?
No, you can still use primary constructors, even if you need readonly fields. You can store the parameter value in a readonly field like in the code snippet below. As you can see, the readonly field can have exaclty the same name as the constructor parameter. If you do this, the C# compiler won’t generate an additional field for the constructor parameter if you want to use the field in the body of your class, like for example in a method.
public class Person(string firstName)
{
private readonly string firstName = firstName;
}
When you declare a readonly field like in the code snippet above, you will get an error like in the screenshot below if you try to change the firstName
field. Because now with the name firstName
you’re referencing the readonly field that you defined, the C# compiler does not generate a field anymore.
The C# compiler also detects edge cases. What for example if you write code like in the following code snippet. As you can see, the firstName
constructor parameter is stored in a _firstName
readonly field (note that the name of this field starts with an underscore). But in the ChangeFirstName
method, you accidentially access the firstName
constructor parameter instead of the readonly _firstName
field.
public class Person(string firstName)
{
private readonly string _firstName = firstName;
public void ChangeFirstName(string newName)
{
firstName = newName;
}
}
When you do something like in the code snippet above, it means that the C# compiler generates also a firstName
field behind the scenes that is then used in the ChangeFirstName
method. But as you initialized the readonly field _firstName
with the value of the firstName
constructor parameter, your intention was probably definitely to use that readonly field _firstName
in your class, and not the firstName
constructor parameter. And exactly because of this, the C# compiler generates a warning for the code snippet above. You can see it in the screenshot below. As you can see, it says that the firstName
constructor parameter is captured into the state of the type. This means that the C# compiler generates a firstName
field to store that state respectively that value of the constructor parameter, as you access it in the ChangeFirstName
method. In addition, the value of the firstName
constructor parameter is also used to initialize a field, property, or event. Yes, you initialize the _firstName
readonly field. So, it’s that field that you should use in the ChangeFirstName
method, and when you do that, the warning goes away.
So, you see, working with primary constructors and readonly fields works as well.
Didn’t Primary Constructors Exist Already For Record Types?
Yes, you’re absolutely right. Primary constructors existed already since C# 9 for record types. But there’s a difference between primary constructors for records and for classes. For record types, the C# compiler creates a public property for every constructor parameter, for classes the C# compiler does not do this.
Take a look at the Person
record in the code snippet below. It has a primary constructor with a FirstName
parameter. For this record, the C# compiler generates a public FirstName
property that gets initialized with the value of the FirstName
constructor parameter.
public record Person(string FirstName);
Primary constructors for classes don’t generate public properties behind the scenes.
Summary
You learned in this blog post about primary constructors for classes and structs that were introduced with C# 12. They let you define your classes in a more compact way than ever before. I also believe that this new syntax is more readable, especially when your class has only a single constructor to initialize some members.
I hope you enjoyed reading this blog post about C# primary constructors.
In the next blog post, you will learn about another C# 12 feature: Collection Expressions.
Comments (3)
One important difference that is often overlooked is that the field that is created is not, and cannot, be marked as readonly. Therefore is no compiler support to prevent the field from being changed, as is typical with “normal” constructors assigning to private readonly fields.
As such, I would not recommend using Primary constructors.
Hi Rob,
great input! I added an additional section to this blog post for you and others: “Is the Generated Field Readonly?”. It describes a few things about primary constructors and readonly fields. Hope this helps.
Thank you,
Thomas
[…] Primary Constructors […]