Want to Make Your C# DTOs More Manageable?  Turn Them into Records!

Records are neat.  They look and behave like classes but with a couple key differences (it should be noted that .NET treats them as classes with extra steps).  For starters, records measure equality different than our normal classes would.  For an instance of a class to be equal to another instance of that same class, both objects instantiated from it must point to the same reference in memory. 

With a record, you can have two separate instances and they will both be considered equal if the properties and values match.  Another key difference is how we can define them.  You could code them as you would a class (using the “record” keyword in place of “class”), our you can create a positional record type.

In my opinion, positional record types are easier to define as they look just like method signatures, which results in less code overall.  They are also immutable, as their properties can only be set on instantiation (NOTE: With a normal record type or class, you would need to make use of the “init” keyword to accomplish this). 

When I think of a class where I would want the properties to be immutable and declarations to be simplified, the first thing that comes to mind are DTOs.  But don’t take my word for it; Ronland Guijt states that records are perfect for DTOs in his Pluralsignt course, which I highly recommend you watch:

https://app.pluralsight.com/library/courses/working-c-sharp-records/table-of-contents

However, I’d like to go a step further and say that positional record types are best for this.  Let’s examine why.  Say I have a Customer DTO with the following properties:

FirstName
LastName
StreetAddress
City
Zip
State
PhoneNumber 
Email

Here is the class definition.  I set each property to make use of init, instead of set, so that I cannot change their values after instantiation.

public class CustomerDto
{
    public string FirstName { get; init; }
    public string LastName { get; init; }
    public string StreetAddress { get; init; }
    public string City { get; init; }
    public string ZIP { get; init; }
    public string State { get; init; }
    public string PhoneNumber { get; init; }
    public string Email { get; init; }
}

I could make it a record by simply changing the “class” keyword to “record”, but this offers no real advantage unless I am looking to compare two instances of this record against each other or if I wish to clone the record with slightly different data.  There are also other features regrading inheritance we could take advantage of, but I will avoid speaking to those for the sake of simplicity.

public record CustomerDto
{
    public string FirstName { get; init; }
    public string LastName { get; init; }
    public string StreetAddress { get; init; }
    public string City { get; init; }
    public string ZIP { get; init; }
    public string State { get; init; }
    public string PhoneNumber { get; init; }
    public string Email { get; init; }
}

But there is a better way.  If I define my DTO as a positional type of record, I get immutability and a shorthand definition right out of the box!

public record CustomerDto(string FirstName, string LastName,
    string StreetAddress, string City, string ZIP, string State,
    string PhoneNumber, string Email);

However, there is a downside.  In the code below I map the contents of the DTO to a new instance of the Customer class so I can add this data to my database.  Say I don’t want to type out customerDto.PropertyNameHere every time I want to assign the contents of my DTO to my new entity.  I could choose to make use of the deconstruct feature on the record, which allows me to define local variables with shorter names.

public bool AddCustomer(CustomerDto customerDto)
{
    // deconstruct used here to make vars with shorter names
    var (fName, lName, streetAddr, city, zip, state, phone, email) = customerDto;

    Customer customer = new Customer()
    {
        FirstName = fName,
        LastName = lName,
        StreetAddress = streetAddr,
        City = city,
        ZIP = zip,
        State = state,
        PhoneNumber = phone,
        Email = email
    };

    var result = uow.AddCustomer(customer);

    return result;
}

For this to work, the record takes the position of the variable defined on the left and matches it with the value of the property with the same position on the right.  It’s plausible that in the future somebody may want to modify this record by adding or removing a property. They would need to make sure the positions of the variables we defined match the position of the properties defined in the record. 

What happens if that is not the case? Maybe a compile time error will catch it if the wrong data types are mapped, but it is just as likely that the wrong variable maps with a property of the same data type. At this point we are entirely at the mercy of any Data Annotations or other validation being used for the customer class to catch that the data is incorrect.   If it doesn’t, we submit faulty data to the database.    With that said, if you are making use of unit tests properly you should be able to catch this well before you are ready to merge your code to your production branch. 

And that’s that!  By making use of  positional type records for our DTOs, we have successfully defined them with less code and ensured their values won’t be changed with the power immutability.