C# required, record and init-only


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
.
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 :)
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.
Subscribe to my newsletter
Read articles from Adam Bieganski directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
