C# required, record and init-only

Adam BieganskiAdam Bieganski
3 min read

Class

Before required you had to create a constructor to guarantee that all properties you wanted were actually set:

public class EmailPdfToUserCommand
{
    public EmailPdfToUserCommand(string invoiceId, string userFirstName, string userLanguage, string emailAddress)
    {
        InvoiceId = invoiceId;
        UserFirstName = userFirstName;
        UserLanguage = userLanguage;
        EmailAddress = emailAddress;
    }

    public string InvoiceId { get; private set; }
    public string UserFirstName { get; private set; }
    public string UserLanguage { get; private set; }
    public string EmailAddress { get; private set; }
}

public class SendEmails
{
    public void SendPdf(string invoiceId, Customer customer)
    {
        var command = new EmailPdfToUserCommand(
            invoiceId, 
            customer.FirstName, 
            customer.Language, 
            customer.Email
        );

        // execute the command
    }
}

The constructor syntax though is not very readable, since you don’t immediately know which properties you’re setting:

var command = new EmailPdfToUserCommand(
    invoiceId, 
    customer.FirstName, 
    customer.Language, 
    customer.Email
);

You can specify the argument names of course, but that’s still not exactly “it”:

var command = new EmailPdfToUserCommand(
    invoiceId: invoiceId, 
    userFirstName: customer.FirstName, 
    userLanguage: customer.Language, 
    emailAddress: customer.Email
);

Required

required to the rescue! If you mark the properties as required, you don’t need the constructor:

public class EmailPdfToUserCommandWithRequired
{
    public required string InvoiceId { get; set; }
    public required string UserFirstName { get; set; }
    public required string UserLanguage { get; set; }
    public required string EmailAddress { get; set; }
}

Now, when you’re instantiating this command, you can use the object initializer syntax:

var command = new EmailPdfToUserCommandWithRequired
{
    InvoiceId = invoiceId,
    UserFirstName = customer.FirstName,
    UserLanguage = customer.Language,
    EmailAddress = customer.Email
};

It’s also guaranteed that all the properties will be set.

This, for example, will not compile:

var command = new EmailPdfToUserCommandWithRequired
{
    InvoiceId = invoiceId,
    UserFirstName = customer.FirstName,
    UserLanguage = customer.Language,
        // compilation error
};

because you didn’t specify the value for EmailAddress:

Record

In case of DTOs or commands and any other plain (no logic) objects, it’s actually better to use a record instead of a class.

Who's brilliant idea was this?  | image tagged in funny,demotivationals,voyager,evilmandoevil,memes,carl sagan | made w/ Imgflip demotivational maker

Since in our example the values are not going to change (once we’ve created a command we’re not going to reuse it with different data in it), we can also make the properties init-only:

public record EmailPdfToUserCommandWithRequired
{
    public required string InvoiceId { get; init; }
    public required string UserFirstName { get; init; }
    public required string UserLanguage { get; init; }
    public required string EmailAddress { get; init; }
}

Now, this won’t compile:

public void SendPdfWithRequired(string invoiceId, Customer customer)
{
    var command = new EmailPdfToUserCommandWithRequired
    {
        InvoiceId = invoiceId,
        UserFirstName = customer.FirstName,
        UserLanguage = customer.Language,
        EmailAddress = customer.Email
    };

    command.UserLanguage = "en";  // compilation error
}

because we’re trying to modify an init-only property.

Positional record

We can make the definition of the record more concise by using the positional record syntax:

public record EmailPdfToUserCommandPositional(
    string InvoiceId,
    string UserFirstName,
    string UserLanguage,
    string EmailAddress);

but then the initialization syntax becomes this again:

public void SendPdfWithRequired(string invoiceId, Customer customer)
{
    var command = new EmailPdfToUserCommandPositional(
        invoiceId, 
        customer.FirstName, 
        customer.Language, 
        customer.Email
    );
}

so I’m not a big fan of that :)

not a big fan! - Imgflip

Required, record and init-only are great

Since the introduction of required in C# 11, I've really appreciated its utility. The record and init-only features, which were introduced earlier in C# 9, are also excellent. However, I'm not particularly fond of the positional record syntax.

1
Subscribe to my newsletter

Read articles from Adam Bieganski directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Adam Bieganski
Adam Bieganski