In this post I introduce the primary constructors feature that debuted in C#12 along with .NET 8. I describe the various ways to use them and show how they work behind the scenes.
The first appearance of primary constructors: record
Primary constructors were first introduced in C#10 as part of the record
feature. Records provide a terse syntax for creating "data" types designed for encapsulation of logic. For example, take the definition of a Person
record
type:
public record Person(string FirstName, string LastName);
The C# compiler takes this definition and generates a type that looks a little like the following (I've simplified this significantly for my purposes, but you can view the "real" lowered code on sharlab.io)
public class Person : IEquatable<Person>
{
// The primary constructor lowers to a "normal" constructor
public Person(string FirstName, string LastName)
{
this.FirstName = FirstName;
this.LastName = LastName;
}
// Public properties are created from the primary constructor parameters
public string FirstName { get; init; }
public string LastName { get; init; }
// The generated Equals method compares the types structurally,
// comparing each individual method
public virtual bool Equals(Person other)
{
if ((object)this != other)
{
if ((object)other != null
&& EqualityComparer<string>.Default.Equals(FirstName, other.FirstName))
{
return EqualityComparer<string>.Default.Equals(LastName, other.LastName);
}
return false;
}
return true;
}
}
There's a lot more generated code than I've shown above (and I've simplified the above code too!), but it shows some of the important points about records:
- The primary constructor lowers to a perfectly ordinary constructor
- Public properties are created for each of the record's primary constructor parameters, and they're initialized from the primary constructor's parameters.
- Equality comparison members are generated, which compares the
record
instances structurally.
There's a lot more to records, but I'm not going to go into records any further here as I'm mostly interested in their use of primary constructors.
Note that you don't have to use primary constructors with
record
types, they're just commonly used as they provide a very terse syntax. You can also use a standard "class-like" syntax and provide your own constructor.
Records have quickly become a popular way of encapsulating data thanks to their immutability by default, simple syntax, and structural equality. But in C#12 some of that terseness was made available to regular class
and struct
types when they gained primary constructors.
An introduction to primary constructors in C#12
Records provide immutability and structural equality, which is great for encapsulating data, but sometimes you don't want that. Lets say you want a "regular" class
version of the Person
type. Prior to C#12, you would have to create a class something like this:
public class Person
{
public Person(string firstName, string lastName)
{
FirstName = firstName;
LastName = lastName;
}
public string FirstName { get; }
public string LastName { get; }
}
There's a lot of duplication here. You have the name of the properties (FirstName
, LastName
), the name of the constructor parameters (firstName
, lastName
), and you have to assign one to the other. Primary constructors allow you to simplify the definition and reduce some of the duplication.
There's broadly two different ways to use primary constructor parameters
- To initialize members (or to pass to a base constructor).
- Directly referencing the parameters in members.
We'll look at each of these approaches in turn.
Initializing members with parameters
The first usage, initializing members, is directly analogous to the class
definition shown above, but it reduces some of the duplication and is generally more compact:
public class Person(string firstName, string lastName)
{
public string FirstName { get; } = firstName;
public string LastName { get; } = lastName;
}
If you compare this definition to the previous one, you'll see that it's significantly more compact. Previously, the type definition took 6 lines of code and 4 lines of braces for 10 lines of vertical height. We've literally halved all those numbers in the definition above, and I'd argue it's still pretty easy and obvious what's going on.
Note that I'm only using
class
types in this post for simplicity, but you can use primary constructors withstruct
types too!
Lets look at a variation of this. We'll start with the following type:
public class Person
{
private readonly string _firstName;
private readonly string _lastName;
public Person(string firstName, string lastName)
{
_firstName = firstName;
_lastName_ = lastName;
}
public string FullName => $"{_firstName} {_lastName}";
}
In this example we're initializing two fields using the constructor parameters, and then using those fields in a different method. Again, the constructor is just doing simple assignments, so we could rewrite this using primary constructors:
public class Person(string firstName, string lastName)
{
private readonly string _firstName = firstName;
private readonly string _lastName = lastName;
public string FullName => $"{_firstName} {_lastName}";
}
That's much cleaner, but primary constructors let you go one step further, and remove those field declarations completely!
Referencing constructor parameters in members
In the previous example we used primary constructor parameters to initialize field values. But those values aren't used anywhere other than in the FullName
property, so we can actually use primary constructors to "simplify" the class even further:
public class Person(string firstName, string lastName)
{
// Directly referencing the primary constructor parameters
// 👇 👇
public string FullName => $"{firstName} {lastName}";
}
In this example, we're not obviously saving the constructor parameters anywhere; instead we're directly referencing the values in a member. But we can take this even further, and even mutate those values from a member:
public class Person(string firstName, string lastName)
{ // Referencing the parameters 👇
public string FullName => $"{firstName} {lastName}";
public void SetName(string first, string last)
{
firstName = first; // 👈 Mutating the primary constructor parameters
lastName = last; // 👈
}
}
If you run something like the following, you can see that the constructor parameters are mutable:
var p = new Person("Andrew", "Lock");
p.SetName("Joe", "Bloggs");
Console.WriteLine(p.FullName); // "Joe Bloggs"
I've heard some people describe primary constructor parameters as a new "kind" of parameter, or "class level" parameters. But I think looking at how primary constructors are implemented behind-the-scenes makes things much clearer.
How do primary constructors work behind the scenes?
A great way to understand many new C# features, especially ones that are essentially syntactic sugar, is to use Sharplab.io to see how the compiler "lowers" the features into simpler C#.
For example, let's start with one of our first primary constructor examples, where we're using the parameters to set some fields, and then using those fields in the FullName
property:
public class Person(string firstName, string lastName)
{
private readonly string _firstName = firstName;
private readonly string _lastName = lastName;
public string FullName => $"{_firstName} {_lastName}";
}
If we paste that into sharplab, and change the result type to C#, it shows that the primary constructor simply generates a "normal" constructor. The following is roughly what's printed (somewhat simplified):
public class Person
{
private readonly string _firstName;
private readonly string _lastName;
public string FullName => string.Concat(_firstName, " ", _lastName);
// The primary constructor is converted into a "normal" constructor
public Person(string firstName, string lastName)
{
_firstName = firstName;
_lastName = lastName;
}
}
The only extra code generated by the primary constructor is the "real" constructor at the end of the definition that initializes the field values. If you only use the primary constructor to initialize fields and properties, then this is all the primary constructor generates: a normal constructor.
Now let's look at the other approach, where the primary constructor parameters are used directly in members. Taking this example again:
public class Person(string firstName, string lastName)
{
public string FullName => $"{firstName} {lastName}";
}
Running this in sharplab, you can see that the implementation is remarkably similar to the previous example:
public class Person
{
// These fields are generated by the compiler, and have "unspeakable"
// names that are invalid in C#, but are valid in IL
private string <firstName>P;
private string <lastName>P;
public string FullName => string.Concat(<firstName>P, " ", <lastName>P);
// The generated constructor sets the values of the generated fields
public Person(string firstName, string lastName)
{
<firstName>P = firstName;
<lastName>P = lastName;
}
}
As shown above, if you reference the primary constructor parameters anywhere other than in a field or property initializer, the compiler automatically generates fields to store the parameter values, so they can be used in other methods and properties. This is one of the big features of primary constructors, but it's one I personally have mixed feelings about, as I'll discuss in the next post.
Adding validation to initialization
One of the general concerns about features like primary constructors is the "grow up" story. There's a risk that a feature works great for the simple cases, but as soon as you need to move to something more complex, you have to ditch the feature entirely. While that certainly can be the case for primary constructors, there's still a lot you can do before you get to that point.
For example, it's common to add validation code in constructors, to check for null
values or invalid arguments. If you're using primary constructors for initialization, then you may be able to keep using primary constructors even if you need to add validation.
The following example adds a check that firstName
is not null
to the initialization code. It also adds a more complex validation call for lastName
by calling IsValidName
and using a ternary expression.
using System;
public class Person(string firstName, string lastName)
{
// 👇 Throw if firstName is null
private readonly string _firstName = firstName ?? throw new ArgumentNullException(nameof(firstName));
// 👇 Throw if lastName is not valid
private readonly string _lastName = IsValidName(lastName) ? lastName : throw new ArgumentNullException(nameof(lastName));
public string FullName => $"{_firstName} {_lastName}";
private static bool IsValidName(string name)
=> name is not "";
}
Which compiles into something like this:
public class Person
{
private readonly string _firstName;
private readonly string _lastName;
public string FullName => string.Concat(_firstName, " ", _lastName);
public Person(string firstName, string lastName)
{
if (firstName == null)
{
throw new ArgumentNullException("firstName");
}
_firstName = firstName;
if (!IsValidName(lastName))
{
throw new ArgumentNullException("lastName");
}
_lastName = lastName;
}
private static bool IsValidName(string name) => !(name == "");
}
Of course, you can certainly go overboard with this initialization code. At a certain point, it probably makes sense to ditch the primary constructor and switch to a regular constructor instead. It's worth noting that you can only add validation to constructor parameters like this if you're using them for initialization, not if you're using them implicitly.
We've covered most of the important features of primary constructors at this point, but I've carefully avoided one question: should you use them? In the next post I discuss some of the places I like to use primary constructors, and some of the things I dislike about them.
Summary
In this post I talked about primary constructors, which were introduced in C#12 with .NET 8. I discussed their origin in record
s and how they can significantly reduce the duplication in your classes. I showed the two different approaches you can use: initialization of fields and properties, or implicit capture. I showed that if you use initialization only, the complier simply generates a normal constructor. If you use the implicit capture approach, the compiler also generates a mutable field for each parameter. In the next post I'll discuss some of the things I like and don't like about primary constructors.