NDepend Blog

Improve your .NET code quality with NDepend

C#12 class and struct Primary Constructors

April 13, 2023 3 minutes read

C# 12 class and struct Primary Constructor

Since C#9 we have the convenient primary constructor syntax for class record (or just record) and struct record:

C#12 introduces primary constructor for non-record class and struct but beware, it is very different! This is because the underlying motivation is different:

  • record primary constructor represents a concise way to generate public read-only properties. This is because a record is a simple immutable object designed to hold some states.
  • class and struct primary constructor represents a concise way to generate private fields. This is because class and struct are implementations with internal logic that uses internal states often initialized at construction time.

For example here is a situation where quite a few lines of code can be saved:

CSharp12 Primary Constructor Motivation

A valid criticism to consider is that the absence of a primary constructor body could result in the creation of classes that lack proper parameter validation, thereby encouraging the development of lazy code. This potential issue is already present in records’ primary constructors.

No Property Generated

Let’s make clear that unlike record primary constructor, properties are not generated:

CSharp12 Primary Constructor Property Are Not Generated

Since no properties are generated, another difference with record  primary constructor is that the with keyword cannot be used:

CSharp12 Primary Constructor and with syntax

Usage of private fields generated by Primary Constructor

Let’s illustrate how generated private fields can be used:

Actually in this code sample above, the C# compiler generates 2 private fields, one for the primary constructor parameter yearOfBirth and one for the property YearOfBirth1 backing fields:

CSharp12 Primary Constructor Field Decompiled

Naming Conflict

Primary constructor parameter naming conflict is authorized by the compiler with both field and property. This can lead to odd situations like here:

CSharp12 Primary Constructor Field Naming Conflict

The property or field then hides the primary constructor parameter everywhere except in property initialization:

C# 12 Primary Constructor Property hides Parameter

However parameters should be Camel case (yearOfBirth), property should be Pascal case (YearOfBirth) and by convention private fields should be underscore Camel case (_yearOfBirth) so naming conflict should be naturally avoided.

Primary Constructor and Others Constructors

class and struct primary constructors can save quite some code but the downside is that it forces (eventual) other constructors to call the primary constructor through the keyword this :

Also a derived class cannot have a primary constructor as long as its base class doesn’t have a constructor with no parameters (which is named a default constructor):

CSharp12 Primary Constructor and Derived Class

Finally let’s notice that a struct with a primary constructor still has a default constructor.

This is because in C# a struct has value-type semantic. This implies that a default(TStruct) instance can be created through new TStruct() with all states initialized with their default values, 0 for value types and null for references.

Conclusion

C#12 class and struct primary constructor is a great new syntax to write more concise code. However it can be misleading since it is quite different than the existing record primary constructor syntax. This is why in this post we insisted on the different underlying motivations.


I wish a breakpoint could be set on class and struct primary constructors. For now I can only break on primary constructor call when a parameter is used in a property initialization. (I checked that record primary constructors don’t have that also). This could be useful the same way one can break upon a get; or set; expression. I submitted a request for that.

C# 12 class and struct Primary Constructor and breakpoint