Introduction
In modern software development—especially in API-driven architectures and microservices—Data Transfer Objects (DTOs) play a vital role in transferring data between application layers or across service boundaries.
Traditionally, classes have been the default choice for implementing DTOs in C#. However, with the introduction of records in C# 9, developers now have a more expressive, concise, and intention-revealing alternative.
This article explains why DTOs are often a better fit than classes, and when classes should still be preferred.
Understanding DTOs and Their Role
A Data Transfer Object (DTO) is a simple, lightweight object whose sole purpose is to carry data.
DTOs:
Contains no business logic
Represent data contracts between layers or services.
Improve decoupling and maintainability.
By clearly separating data representation from business logic, DTOs make systems easier to evolve and scale.
Limitations of Using Classes for DTOs
While classes work well, they introduce some common challenges when used as DTOs:
Extra boilerplate code (constructors, equality, ToString)
Reference-based equality by default
Mutability that can lead to accidental data changes
Additional effort to enforce immutability
These issues become more noticeable in large codebases or distributed systems
What Are Records in C#?
Records, introduced in C# 9, are reference types designed specifically for data-centric models.
They provide:
Because DTOs are fundamentally data carriers, records align naturally with their purpose.
Why Records Are Ideal for DTOs
1. Immutability Ensures Data Integrity
Records are immutable by default, meaning their state cannot be modified after creation.
using System;
using System.Collections.Generic;
using System.Text;
namespace RadicalSchoolAdmin.Core.DTO
{
public record AdminSysCodeRequest(
string? SysType,
int SysCode,
string? SysName,
string? SysShortName,
string? SysDescription
);
}
This guarantees:
Immutability is especially important in multi-threaded and distributed systems.
Value-Based Equality Simplifies Comparisons
Records compare objects based on their values, not references.
var objItem = new ItemDto(1, "Box", 5, 8);
var objItemSecond = new ItemDto(1, "Box", 5, 8);
bool status = objItem == objItemSecond; // true
With classes, this requires overriding Equals() and GetHashCode().
Records handle it automatically, reducing bugs and boilerplate.
3. Concise and Readable Syntax
Records drastically reduce the amount of code required to define DTOs.
public record ItemDto(int Id, string Name, int lenth ,int width);
Behind the scenes, this single line provides:
Constructor
Equality members
Deconstruct() method
Meaningful ToString()
This leads to:
Cleaner code
Faster development
Better readability
4. Better Fit for Modern and Functional Styles
Modern .NET development increasingly embraces:
Immutability
Declarative code
Functional patterns
Records align naturally with these practices and integrate seamlessly with:
LINQ
Pattern matching
CQRS-style architectures
When Classes Are Still the Right Choice
Despite their advantages, records are not a universal replacement for classes.
Use classes when:
DTOs must be mutable
You require complex behavior or lifecycle management
The object represents an entity, not just data
EF Core tracking and partial updates are needed
Conclusion
Records provide a clean, expressive, and intention-revealing way to model Data Transfer Objects in modern C# applications.