Photo by Elza Kurbanova / Unsplash

Sculpting Seamless Communication: Elevating Web App Development with C# Records for DTO Excellence

Webdevelopment Aug 1, 2023

In modern webapplication, there will be used a DTO to separate Database Objects from communication objects to abstract the Datatransfer and avoid delviering unwanted, internal or technical data to the consumer. So using DTO's a common scenario but it sometimes comes up with several boilerplate codes. With the release of C# 9.0, there comes a new feature called records into the game. this article will show you, how this can help you to get better better code readability and maintainability and also avoid boilerplate code.

What is a DTO?

DTO is a shortcut for “Data Transfer Object” It represents an object that will be used for a transfer of data in a particular scenario like a web service.

DTOs are commonly used in client-server architectures, where data needs to be exchanged between the client-side and server-side components. They help in decoupling the data representation from the business

Key characteristics of a DTO

Let me describe some key characteristics of a DTO that will be met.

Data Carrier:

DTOs are simple data containers that hold data and may include properties to represent the data fields. Normally they don't have any business logic.

Serializable:

DTOs are often designed to be easily serialized and deserialized, allowing them to be sent over the network or stored in a persistent storage system.

Immutable:

Immutable DTOs ensure that the transported data remains unchanged during its journey through different layers, reducing the chances of unintended modifications.

How to create a DTO

Here is an example DTO that will be used in a web API communication

public class MemberDTO
{
   public string Name { get; set; }
   public string City { get; set; }
}

This is a really simple DTO implementation but it is ready to be used as in this deserialization example below:

// Receiving the result of an HTTP call
var httpResponse = await httpClient.GetAsync("https://api.example.com/members/1");
var content = await httpResponse.Content.ReadAsStringAsync();

var memberDTO = JsonConvert.DeserializeObject<MemberDTO>(content);

But where is the boilerplate code then?

The previous DTO is a very simple piece of code since it just has two properties defined, but in a real application we are used to creating additional features inside DTO, as constructors and other customization as we can see below:

public class MemberDTO
{
    public string Name { get; set; }
    public string City { get; set; }

    // Default constructor
    public MemberDTO()
    {
        // Empty constructor
    }

    // Parameterized constructor
    public MemberDTO(string name, string city)
    {
        Name = name;
        City = city;
    }

    // Equals method for comparison
    public override bool Equals(object obj)
    {
        if (obj == null || GetType() != obj.GetType())
            return false;

        MemberDTO other = (MemberDTO)obj;
        return Name == other.Name && City == other.city;
    }

    // GetHashCode method to calculate hash code for comparison
    public override int GetHashCode()
    {
        unchecked
        {
            int hash = 17;
            hash = hash * 23 + (Name != null ? Name.GetHashCode() : 0);
            hash = hash * 23 + City.GetHashCode();
            return hash;
        }
    }

    // String representation for UserDTO
    public override string ToString()
    {
        return $"Name: {Name}, Age: {City}";
    }
}

This defines a class that contains beside the properties Name and City the method for comparison Equals, the ToString method represents the object as sring and also the GetHashCodemethod.



What are records?

A record is a structure that combines immutable properties with some predefined behaviors, such as comparison and string representation.

Instead of creating traditional classes with properties, constructors, Equals, GetHashCode, and ToString methods, you can use the records’ syntax to define simple and efficient data types.

We can rewrite the last example using the following statement:

public record MemberRecord(string Name, string City);

When utilizing records in C# to represent DTOs, you can benefit from several advantages:

Conciseness and Readability:

Records’ syntax is more compact, reducing the amount of code required to define a data transfer object. This makes the code easier to read and understand, especially when compared to traditional classes.

Immutability by Default:

Records are immutable by default, meaning their properties cannot be changed after the object is created. This immutability ensures that the transported data remains unmodified, avoiding unintended side effects.

Default Method Implementations:

Records automatically generate Equals, GetHashCode, and ToString method implementations. This ensures proper comparison and allows records to be used in collections like dictionaries and sets without issues.

With Expression for Updates:

Records enable the creation of updated copies of objects using the straightforward with expression feature. This is particularly useful when you need to modify some properties without altering the original object.

Comparison records as Value Type:

Records are value types and have value semantics. This means that when you compare two records, you are comparing their values, not their memory references.

Unlike classes, which have reference semantics, records have a comparison implementation that considers the values of their properties.

How do general classes compare objects?

It is possible to compare reference types in C#. However, when comparing reference types, you are comparing the memory addresses (references) of the objects, not their actual content.

Two references will be considered equal only if they point to the same memory location, i.e., if they refer to the same object instance in memory.

Default class comparison example

class MemberDTO
{
   public string Name { get; set; }
   public string City{ get; set; }
}

MemberDTO user1 = new MemberDTO { Name = "John", City = "Bochum"};
MemberDTO user2 = new MemberDTO { Name = "John", City = "Herne" };

bool areEqualClasses = user1.Equals(user2); // Returns 'False'

It will return false since there are two different objects pointing to different memory references.

So in this case, you must override the `Equals` method to get a comparison method between two objects. Optionally you must override the GetHashCode to get a propert unique value to compare each other objects.

Custom class comparison example

public class MemberDTO
{
    public string Name { get; set; }
    public string City{ get; set; }

    public override bool Equals(object obj)
    {
        if (obj == null || GetType() != obj.GetType())
            return false;

        MemberDTO other = (MemberDTO)obj;
        return Name == other.Name && City == other.City;
    }
}

MemberDTO user1 = new MemberDTO  { Name = "John", City= "Bochum"};
MemberDTO user2 = new MemberDTO { Name = "John", City = "Herne" };

bool areEqualClasses = user1.Equals(user2); // Returns 'True'

Overring the Equals method is enough to get true in a class comparison, although comparison using the == equality operator still returns false

MemberDTO user1 = new MemberDTO { Name = "John", City= "Bochum" };
MemberDTO user2 = new MemberDTO { Name = "John", City = "Herne" };

bool areEqualClasses = person1 == person2; // Returns 'False'

This problem we can now solve, by overriding the equality operator:

public class MemberDTO
{
    public string Name { get; set; }
    public string City{ get; set; }

    public override bool Equals(object obj)
    {
        if (obj == null || GetType() != obj.GetType())
            return false;

        MemberDTO other = (MemberDTO)obj;
        return Name == other.Name && City == other.City;
    }

    // Override the equality operator ==
    public static bool operator ==(MemberDTO left, MemberDTO right)
    {
        if (left is null || right is null)
            return false;

        return left.Equals(right);
    }

    // Override the inequality operator !=
    public static bool operator !=(MemberDTO left, MemberDTO right)
    {
        return !(left == right);
    }
}

MemberDTO user1 = new MemberDTO { Name = "John", City = "Herne" };
MemberDTO user2 = new MemberDTO { Name = "John", City = "Herne"};

bool areEqualClasses = person1 == person2; // Returns 'True'

Overriding the == operator obligates us to also override the != operator, but now everything works like a charm. That was easy and very maintainable? I think not...

How to use a record?

Let's look at the same example by using a record-Type for DTO.

public record MemberRecord(string Name, string City);

That's all! All of the previus defined methods are already generated by the record itself. So let's do a test like above

MemberRecord user1 = new MemberRecord("John", "Herne");
MemberRecord user2 = new MemberRecord("John", "Herne");

bool areEqualRecords2 = user1.Equals(user2); //returns True
bool areEqualRecords1 = user1 == user2; //returns True

user1.ToString(); //returns MemberRecord { Name = John, City ="Herne"}
user1.GetHashCode(); //returns 725737345
user2.GetHashCode(); //returns 725737345

As we can see records also internally imimplement the ToString and GetHashCode methods. That's cool eh?

Conclusion

In conclusion, while Data Transfer Objects (DTOs) are vital for segregating data and facilitating efficient communication in web applications, they often lead to redundant boilerplate code. With the advent of C# 9.0, the introduction of records offers an elegant solution.

Records streamline DTO creation by combining immutability, predefined methods, and concise syntax. This simplifies code, enhances readability, and promotes better data transfer practices, ultimately improving code quality and maintainability in modern web development.

So what do you think about records? Did you used this in your projects?

Tags