6 reglas para escribir DTOs
Qué es un DTO, qué se puede modelar como uno, por qué record le gana a class acá, y las reglas que uso para no terminar reutilizando el mismo DTO para todo.
Un DTO (Data Transfer Object) es un objeto cuyo único propósito es transportar datos entre capas o procesos. Nada más.
El problema no es la definición — es lo fácil que es romperla. Un DTO que empieza simple termina con lógica de negocio adentro, o reutilizado en tres lugares que en realidad necesitaban formas distintas. Estas son las reglas que uso para que eso no pase.
Inspirado en 5 Rules for DTOs, de Steve Smith (Ardalis) — acá sumé una regla más y lo reescribí a mi manera.
#1. Sin lógica ni comportamiento
No deben contener métodos que implementen reglas de negocio. Si un DTO tiene un método que decide algo, dejó de ser un DTO — se convirtió en otra cosa con un nombre engañoso.
#2. No deben forzar encapsulamiento
No obligues a usar setters/getters complejos; propiedades simples son suficientes. Un DTO no protege invariantes de dominio — solo transporta datos. Esa responsabilidad es de otra capa.
#3. Deben usar propiedades
Siempre propiedades públicas, preferiblemente auto-implementadas.
public record CreateItemRequest(string Title, string Description);
#4. Sin sufijo "DTO" o "Dto"
El nombre debe ser claro por el contexto, no por el sufijo. Evitá terminar un DTO simplemente en *Dto — sabemos que es un DTO, lo que no dice es para qué. Preferí *Request, *Response, *Event, *Message, según el rol que cumple. Ese único cambio evita que termines reutilizando el mismo DTO genérico para entrada, salida y evento a la vez.
(Escribí sobre esto con más detalle en Coding Conventions.)
#5. Qué se puede modelar como DTO
- API Request o Response
- Resultados de base de datos
- Mensajes — comandos, consultas, eventos
- View Models de MVC
#6. Inmutables
Una vez creado, sus valores no deben cambiar.
public record CreateItemResponse(Guid Id, string Title, string Description);
Con record, la inmutabilidad viene gratis — no hace falta escribirla a mano.
#Bonus: record vs class
Para DTOs, record es más compacto y simple de leer que la clase equivalente.
// Requests
public record CreateItemRequest(string Title, string Description);
// Response
public record CreateItemResponse(Guid Id, string Title, string Description);
// Command
public record CreateItemCommand(string Title, string Description);
// Query
public record GetItemByTitleQuery(string Title);
// Event
public record ItemCreatedEvent<T>(Guid Id, DateOnly OccurredOn, T data);
El equivalente de CreateItemRequest escrito como clase:
public class CreateItemRequest
{
public string Title { get; init; }
public string Description { get; init; }
public CreateItemRequest(string title, string description)
{
Title = title;
Description = description;
}
}
Mismo resultado, mucho más código para llegar ahí.
Hay otra diferencia importante que no se ve en el código de arriba: la igualdad. Una class compara por referencia — dos instancias son iguales solo si son el mismo objeto en memoria, salvo que sobrescribas Equals/GetHashCode. Un record compara por valor, automáticamente: dos instancias son iguales si sus propiedades tienen los mismos valores.
var a = new CreateItemRequest("Título", "Desc");
var b = new CreateItemRequest("Título", "Desc");
a == b; // true en record, false en class (sin overrides)
Esto no es solo una curiosidad — es exactamente lo que hace que record sea una base natural para Value Objects: objetos que se definen por su valor, no por su identidad.
#Un séptimo punto: versionado
Después de publicar esto, un arquitecto me hizo un comentario que me pareció válido y quiero sumar: los DTOs — sobre todo los de Request/Response de una API pública — también necesitan poder versionarse.
Un DTO no vive aislado de la evolución de tu API. Si CreateUserRequest cambia de forma incompatible (un campo que pasa a ser obligatorio, uno que se elimina), y ya tenés clientes integrados contra la versión anterior, necesitás una estrategia: sufijar por versión (CreateUserRequestV2), versionar por ruta y mantener DTOs separados por versión, o diseñar pensando en evolución aditiva (campos nuevos opcionales, nunca romper los existentes).
No contradice las reglas anteriores — las complementa. Un DTO sigue siendo inmutable y sin lógica; el versionado es sobre cómo lo hacés evolucionar en el tiempo sin romper a quien ya lo consume.
#dto #csharp #api-design