In this article, we’ll take a look at a new feature of C# 12 and .NET 8 for classes and structs called primary constructors that is still in preview at the time of writing the article.

To download the source code for this article, you can visit our GitHub repository.

Let’s dive in!

Primary Constructors for Records

Records are immutable reference types that offer value-based equality. Primary constructors for record types were introduced in C# 9:

public record Point(double X, double Y);

With this line, we define a record type called Point that has two properties – X and Y. When we initialize a new Point, the compiler will automatically assign the two properties:

Don't like the ads? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!
[Fact]
public void WhenConstructorIsInvoked_ThenAllPropertiesAreAssigned()
{
    // Arrange
    var x = 5.0;
    var y = 6.5;

    // Act
    var point = new Point(x, y);

    // Assert
    point.X.Should().Be(x);
    point.Y.Should().Be(y);
}

Here we verify that when the constructor is called with x and y variables, the corresponding values are assigned to the created point object’s properties X and Y. We also use the FluentAssertions library to perform the assertions, ensuring that point.X and point.Y equal the expected values of our variables x and y, respectively.

With this simple but powerful feature, we can save a lot of time and needles lines of code. Next, we’ll see how the feature looks for classes and structs.

Installing the Prerequisites

Before the official release of the primary constructors for non-record classes and structs features, we must install certain components in order to utilize them. First, we need to start by installing the latest .NET 8 preview version. Then we also need to install the latest preview version of Visual Studio

After we are ready, we can start experimenting with the new features.

Primary Constructors for Classes and Structs

Let’s first look at a simple class which we call Doctor, but everything applies to struct types as well:

public class Doctor
{
    private List<string> _patients;

    public string Name { get; set; }
    public bool IsOverworked => _patients.Count >= 5;

    public Doctor(string name)
    {
        Name = name;
        _patients = new List<string>();
    }

    public void AddPatient(string patient) => _patients.Add(patient);
}

We define a Doctor class with properties Name and IsOverworked. The Name property is a string representing the doctor’s name, and IsOverworked is a computed property returning true if the doctor has at least five patients. The constructor takes a name parameter and initializes the Name property while also creating an empty _patients list to store patient names. The class has an AddPatient() method that allows adding patient names to the private list _patients.

Now, let’s utilize a primary constructor for the same class:

public class Doctor(string name, List<string> patients)
{
    public string Name { get; set; } = name;
    public bool IsOverworked => patients.Count >= 5;

    public Doctor(string name)
        : this(name, new List<string>())
    {
    }

    public void AddPatient(string patient) => patients.Add(patient);
}

We update our Doctor class by adding a primary constructor that accepts name and patients parameters. The main constructor still  takes the name parameter but it calls the primary constructor, passing the name and an empty List<string>. The name and patients parameters will be available to us throughout the body of the class, much like private fields. We can use them to initialize properties, which we do for the Name property. Another thing we can do is to use them inside methods or while computing values for properties as we do with the IsOverworked property.

Key Points From Primary Constructors for Classes and Structs

One very important thing we should remember is that properties will not be automatically created for primary constructor parameters. This helps us only expose the properties we really need to. Classes and structs are complex data structures with underlying logic and not all constructor parameters should be accessible to the outside world.

Another crucial thing is the fact that all constructors in a class should call the primary one using the this keyword. Calling the primary constructor is a requirement to ensure that all essential data is available for the creation of a class or struct. Failure to do so will result in errors and inconsistencies that can jeopardize the integrity of our code. It is crucial that you prioritize this step to ensure the optimal functionality and efficiency of your code.

One final thing to remember is that classes and structs with primary constructors still have reference-based equality and don’t get the == (equality) and != (inequality) operators by default. If you want to use them, you will have to explicitly override them.

Conclusion

In conclusion, primary constructors in C# 12 and .NET 8 offer a powerful feature for classes and structs, allowing concise and efficient initialization of properties. While they were initially introduced for records in C# 9, the extension to classes and structs provides similar benefits. With primary constructors, we can streamline object creation, eliminating the need for manual property assignments and reducing unnecessary code. By ensuring that all constructors call the primary one, we guarantee the availability of essential data, leading to more robust and maintainable code.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!