How to Achieve Immutable DTOs With C#

Written by igorlopushko | Published 2022/08/15
Tech Story Tags: c-sharp | dotnet-core | mutations | immutability | clean-architecture | immutable-dto | coding | learn-to-code

TLDRIn object-oriented and functional programming, an object cannot be modified after it is created. Immutable objects are also useful because they are inherently thread-safe. The question of whether or not DTOs should be immutable is a frequent question. Let’s dive in and see how immutability could be implemented with C#. We can move farther and use some syntactic sugar and use auto-properties to minimize the code. We create a simple object that represents a person and define a couple of properties.via the TL;DR App

In object-oriented and functional programming, an immutable object is an object whose state cannot be modified after it is created. This is in contrast to a mutable object, which can be modified after it is created. Immutable objects are also useful because they are inherently thread-safe.

There is a frequent question of whether or not Data Transfer Objects should be immutable - that is, should our design of the classes and types of a DTO enforce immutability? To answer the question we need to understand the purpose of DTOs. DTO is an object that carries data between processes. It does not have any behaviour except for storage, retrieval, serialization, and deserialization of its own data. It might be helpful to communicate between layers in the application (e.g. using N-tire or Clean architecture).

The Use Case

Consider any object. It can be a product, a cart, a person, or anything else. It's created, it lives for some period, and at some point, it ceases to exist. The object could be passed between many endpoints or application layers. What if, during some or all its life span, the object should never change? It would be great if the compiler could catch such behaviour. Otherwise, we would rely on a runtime exception, which is less efficient. Too often silent failures occur. In other words, when an invalid operation happens, it's not properly handled in a try/catch block. It is also hard to catch exceptions between many endpoints and provide roll-back mechanisms.

Old-fashioned approach

Let’s dive in and see how immutability could be implemented with C#. I’ll create a simple object that represents a person and define a couple of properties:

public class Person
{
  public string FirstName { get; set; }
  public string LastName { get; set; }
  public int Age { get; set; }
}

You see that for now, we are far from the immutability of this object. However, this type of DTO is a common practice in old-fashion projects. What we have to do to make it slightly better is to define private fields and use a constructor.

public class Person
{
  private string _firstName;
  private string _lastName;
  private int _age;

  public string FirstName
  {
    get { return _firstName; }
  }
  
  public string LastName
  {
    get { return _lastName; }
  }
        
  public int Age 
  {
    get { return _age; }
  }

  public Person(string firstName, string lastName, int age)
  {
    _firstName = firstName;
    _lastName = lastName;
    _age = age;
  }
}

Now we can guarantee that the object will be immutable since we initialize internal fields with the constructor and use only the getter for the properties. We can move farther and use some syntactic sugar and use auto-properties to minimize the code.

public class Person
{
  public string FirstName { get; }

  public string LastName { get; }

  public int Age { get; }

  public Person(string firstName, string lastName, int age)
  {
    FirstName = firstName;
    LastName = lastName;
    Age = age;
  }
}

Looks weird so far. When we introduce immutability via constructors, this introduces a breakable contract - a method with fixed arguments. What about deserialization? It passed a long time when deserializers could not access private properties and instantiate an object. The following code works just fine using Newtonsoft.Json:

string input = "{ firstName: \"John\", lastName: \"Doe\", age: 44 }";
Person deserializedPerson = JsonConvert.DeserializeObject<Person>(input);

Modern approach

In C# 9 record reference type appeared that provides built-in functionality for encapsulating data. How it could be done now? Use the record keyword instead of class. Define properties as usual. Use the init keyword for the setter. An init-only setter assigns a value to the property or the indexer element only during object construction. This enforces immutability so that once the object is initialized, it can't be changed again.

public record Person
{
  public string FirstName { get; init; }
  public string LastName { get; init; }  
  public int Age { get; init;}
}

Looks simple and minimalistic. No need to have a constructor in the definition. Record object could be created as follows:

Person person = new Person()
{
  FirstName = "John", 
  LastName = "Doe", 
  Age = 44
};

Summary

The primary benefit of immutability is that you can't change the object! That makes a lot of sense in communicating between many endpoints or different application layers.

If you search the Web for how to achieve immutability in C#, you'll find a common theme. Previously it was quite a tricky way to implement immutability for the DTOs in C# using constructor parameters for initialization, making backing variable read-only, and removing setters (leaving only getters) for public properties. When the record reference type appeared it makes more sense to use it in the proper way.


Written by igorlopushko | Programmer, Architect, Teacher
Published by HackerNoon on 2022/08/15