Поиск  
Always will be ready notify the world about expectations as easy as possible: job change page
Sep 5, 2023

Start using C# records for DTOs instead of regular classes

Автор:
Edson Moisinho
Источник:
Просмотров:
1553

Simplifying Data Transport in C#.

Start using C# records for DTOs instead of regular classes

In modern C# development, data transport objects (DTOs) play a crucial role in exchanging information between different layers of an application, such as between a client and a server, and traditionally, developers have used classes to define DTOs, which involves writing boilerplate code for properties, constructors, comparison methods, and string representations.

With the introduction of C# 9.0, a new and more efficient alternative has emerged: records.

In this article, we will explore the benefits of using records over classes for DTOs, demonstrating how records can significantly improve code conciseness, readability, and reliability while enhancing the data transport process.

First things first. What is a DTO?

DTO stands for “Data Transfer Object” It is a design pattern used in software development to transfer data between different layers or components of an application.

The primary purpose of a DTO is to encapsulate data and transport it from one part of the application to another without exposing the underlying data structures or implementation details.

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 logic or database schema, providing a clear separation of concerns.

• • •

Key characteristics of a DTO

Data Carrier:
DTOs are simple data containers that hold data and may include properties to represent the data fields. They typically contain no behavior or 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.

To check other DTO definitions and similar concepts, check my specific article about it here.

• • •

How to create a DTO

The following example shows how to define and use a regular class as DTO to hold an API result data:

public class UserDTO
{
   public string Name { get; set; }
   public int Age { 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/user/1");
var content = await httpResponse.Content.ReadAsStringAsync();

var userDTO = JsonConvert.DeserializeObject<UserDTO>(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 UserDTO
{
    public string Name { get; set; }
    public int Age { get; set; }

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

    // Parameterized constructor
    public UserDTO(string name, int age)
    {
        Name = name;
        Age = age;
    }

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

        UserDTO other = (UserDTO)obj;
        return Name == other.Name && Age == other.Age;
    }

    // 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 + Age.GetHashCode();
            return hash;
        }
    }

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

The UserDTO class has properties Name and Age, constructors (default and parameterized), custom comparison methods Equals and GetHashCode, and a custom string representation method ToString.

• • •

What are records in C#

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.

The declaration of a record includes properties directly in the record’s header, making the syntax more concise and readable.

We can rewrite the last example using the following statement:

public record UserRecord(string Name, int Age);

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 UserDTO
{
   public string Name { get; set; }
   public int Age { get; set; }
}

UserDTO user1 = new UserDTO { Name = "John", Age = 30 };
UserDTO user2 = new UserDTO { Name = "John", Age = 30 };

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

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

If you want to compare the actual content of objects of a reference type (the property values, for example), you need to override the Equals method and, optionally, the GetHashCode method. By doing so, you can specify how the comparison should be performed based on the properties or fields of the object.

Custom class comparison example:

public class UserDTO
{
    public string Name { get; set; }
    public int Age { get; set; }

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

        UserDTO other = (UserDTO)obj;
        return Name == other.Name && Age == other.Age;
    }
}

UserDTO user1 = new UserDTO { Name = "John", Age = 30 };
UserDTO user2 = new UserDTO { Name = "John", Age = 30 };

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:

UserDTO user1 = new UserDTO { Name = "John", Age = 30 };
UserDTO user2 = new UserDTO { Name = "John", Age = 30 };

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

But we can solve it, by overriding the equality operator:

public class UserDTO
{
    public string Name { get; set; }
    public int Age { get; set; }

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

        UserDTO other = (UserDTO)obj;
        return Name == other.Name && Age == other.Age;
    }

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

        return left.Equals(right);
    }

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

UserDTO user1 = new UserDTO { Name = "John", Age = 30 };
UserDTO user2 = new UserDTO { Name = "John", Age = 30 };

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

Overriding the == operator obligates us to also override the != operator, but now everything works like a charm.

• • •

How to use a record?

The following example shows how to define and use a record as DTO to hold an API result data.

public record UserRecord(string Name, int Age);

And it will automatically generate everything we defined earlier:

UserRecord user1 = new UserRecord("John", 30);
UserRecord user2 = new UserRecord("John", 30);

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

user1.ToString();       //returns UserRecord { Name = John, Age = 30 }

user1.GetHashCode();    //returns 725737345
user2.GetHashCode();    //returns 725737345

As we can see records also internally implements the ToString and GetHashCode methods.

• • •

Final Considerations

Using records in C# to represent DTOs is a powerful and efficient approach. They offer concise syntax, immutability by default, and automatic implementations of important methods, simplifying data transportation and manipulation.

By utilizing records for DTOs, you can improve code readability, ensure the immutability of transported data, and take advantage of automatically generated method implementations.

Thank you for reading it until here, I hope it can help you and your team make your code cleaner, less error-prone, and easier to maintain over time, see you next time.

Похожее
Nov 12, 2020
Author: Joydip Kanjilal
Lazy initialization is a technique that defers the creation of an object until the first time it is needed. In other words, initialization of the object happens only on demand. Note that the terms lazy initialization and lazy instantiation mean...
Jul 25, 2023
Unleashing the Power of Meta-Programming: A Comprehensive Guide to C# ReflectionReflection, put simply, is a mechanism provided by the .NET framework that allows a running program to examine and manipulate itself. It’s like a coding mirror that gives your application...
Apr 18
Author: Michael Shpilt
One of the most used databases these days is PostgreSQL (aka Postgres). Its accomplishments include being the most popular DB [among professional developers] according to Stack Overflow survey of 2022, the database in all of the fastest TechEmpower benchmarks, and...
Oct 28, 2022
There are a lot of problems that spring from leaving a codebase unattended and uncared for longer periods of time. That’s why code refactoring is a big part of keeping digital products functional in the long term. How to know...
Написать сообщение
Почта
Имя
*Сообщение


© 1999–2024 WebDynamics
1980–... Sergey Drozdov
Area of interests: .NET Framework | .NET Core | C# | ASP.NET | Windows Forms | WPF | HTML5 | CSS3 | jQuery | AJAX | Angular | React | MS SQL Server | Transact-SQL | ADO.NET | Entity Framework | IIS | OOP | OOA | OOD | WCF | WPF | MSMQ | MVC | MVP | MVVM | Design Patterns | Enterprise Architecture | Scrum | Kanban