Special Offer: My C#/.NET Bootcamp Course is out now. Get 10% OFF using the code FRIENDS10.

In a previous video, I explained the new required keyword introduced in C# 11 and how it might change how you create objects from classes.

Today, I want to explain how the new required keyword affects record types, and at the end, I will tell you how I will define most of my data objects in the future.

Introduction

Let’s explore this example code. We have a Book represented as a record type with an author and a title property.

var book1 = new Book("Agatha Christie", "The Moving Finger");

var book2 = new Book // compiler error because there is no constructor with 0 arguments.
{
    Author = "Agatha Christie",
    Title = "The Moving Finger",
};

public record Book(string Author, string Title);

We create two instances of the Book type. The first uses a constructor generated by the record type. The second uses the object initializer syntax.

As you can see, with the default record type definition, we can use the generated constructor with two arguments, but we cannot use the object initializer syntax.

Using the Object Initializer Syntax

But what if we want to use the object initializer syntax? The first step is to define the properties in the record type using the required keyword.

Unfortunately, we cannot use the required keyword with the default record type syntax. However, we can explicitly define the properties and apply the required keyword.

var book1 = new Book("Agatha Christie", "The Moving Finger"); // compiler error because there is no constructor with 2 arguments

var book2 = new Book
{
    Author = "Agatha Christie",
    Title = "The Moving Finger",
};

public record Book
{
    public required string Author { get; set; }
    public required string Title { get; set; }
}

We can now use the object initializer syntax but not the constructor.

What’s interesting is that we can switch from a record type to a class type, and we have the same compiler error when using the constructor but are able to use the object initializer syntax.

Using the Constructor and the Object Initializer

So, in the beginning, we had the compact default implementation of a record type which only allowed us to use the constructor to create instances.

We then used the explicit property definition and applied the required keyword to use the object initializer syntax. But what if we want to be able to use both?

Well, we need to implement a default constructor and a constructor with two arguments decorated with the SetsRequiredMembers attribute.

using System.Diagnostics.CodeAnalysis;

var book1 = new Book("Agatha Christie", "The Moving Finger");

var book2 = new Book
{
    Author = "Agatha Christie",
    Title = "The Moving Finger",
};

public record Book
{
    public Book()
    {        
    }

    [SetsRequiredMembers]
    public Book(string author, string title)
    {
        Author = author;
        Title = title;
    }

    public required string Author { get; set; }
    public required string Title { get; set; }
}

If you aren’t familiar with the SetsRequiredMembers attribute, I highly suggest watching the video where I explain the required keyword in great detail. 

The short summary is that when we implement a constructor ourselves, we need to tell the compiler that we provide values for all required properties. The compiler does not verify it during compilation. That’s why we must use the SetsRequiredMembers attribute in this case.

As a result, we can now use both the constructor and the object initializer syntax to create objects of the Book record type.

However, I’m sure you’ll agree that this implementation is rather long and almost looks like a class implementation. Well, again, we can simply replace the record keyword with the class keyword, and we get a class implementation.

Class versus Record Type

This brings up the question of what the differences and similarities between classes and record types are. Again, I have a video that explains record types and the differences between classes and record types in great detail.

The short version is that the record type provides an implementation of the ToString method and, most importantly, value equality.

For classes, we assume that we change state using method calls. For record types, we assume that the state does not change after the object has been created.

Classes usually contain logic in methods that act on data in properties or fields. Record types usually contain data that is accessed and used by other types.

For simple data objects, I use record types because I can compare them based on their values, and for types that include logic and manipulate state, I use classes.

The great learning here is that we can use the same implementation to achieve the same object creation with both the record type and classes.

How Will I Implement Data Objects?

With the new required keyword available, I will default to using record types and explicitly define the properties using the required keyword. 

var book = new Book
{
    Author = "Agatha Christie",
    Title = "The Moving Finger",
};

public record Book
{
    public required string Author { get; set; }
    public required string Title { get; set; }
}

It will allow me to use the object initializer syntax, which I believe to be the most readable and, therefore, the most maintainable syntax to create objects.

I will not implement constructors and not use constructors for creating data objects based on record types anymore. Also, I do not need the SetsRequiredMembers attribute, which in my opinion, isn’t the most developer-friendly solution anyway.

Let me know in the comments how you will create your data objects in the future. And if this was helpful, consider subscribing to learn more about .NET development in the future. 

 

 

Claudio Bernasconi

I'm an enthusiastic Software Engineer with a passion for teaching .NET development on YouTube, writing articles about my journey on my blog, and making people smile.