1. Getting to Know DTO
DTO (Data Transfer Object) is a term straight outta pro programming, especially from the world of distributed apps and services (like when one service calls another over the network). Basically, it's just an object—most of the time, a plain old C# class—whose only job is to hold a set of data and safely travel between layers of your program or between apps.
Let's imagine a chat between a client and a server. Every time a user sends a message, the client creates an object with the text, timestamp, and username, sends it to the server, and the server decides what to do with it. It'd be awesome if this object didn't have any extra junk: just the stuff needed to transfer data. That's exactly what a DTO class is.
public class UserDto
{
public int Id { get; set; }
public string Name { get; set; }
}
That's it! No business logic, no fancy methods—just properties.
Classes Just for Data Transfer
Picture this: you're in the pizza delivery biz. Your courier (DTO) doesn't need to know how to make pizza, take orders, or fix a bike. All they gotta do is carry the box from point A to point B. Same with a DTO class: its job is to store simple data. It shouldn't validate itself, it shouldn't know how to show itself in the UI—just store stuff.
- DTOs are like "people without a profession" or "objects without logic," just carrying data around.
- They're used so you don't clutter up your business logic with data transfer details: the smaller the suitcase, the easier it is to carry.
2. Real-World Problems with Regular DTO Classes
Sounds like a dream: declare a class with auto-properties and live happily ever after! But it's not that simple.
Data Mutability: What Could Go Wrong
public class ProductDto
{
public int Id { get; set; }
public string Name { get; set; }
}
Now imagine somewhere in your code you accidentally change a property:
ProductDto dto = new ProductDto { Id = 1, Name = "Milk" };
SomeApiProcess(dto);
dto.Name = "Kefir"; // Oops!
Because all the properties have public set, it's way too easy to mess up the object or, even worse, change it in one place and have it "leak" all over your system. This gets really nasty when objects are used by a bunch of modules and threads.
Copying and Comparing—Endless Boring Work
Let's say you wanna make a copy of ProductDto, but just change the name.
var otherDto = new ProductDto { Id = dto.Id, Name = "Cottage Cheese" };
If you've got a ton of properties, this turns into a super boring and bug-prone process of copying every field by hand.
And what if you need to compare objects? You'll have to override Equals and GetHashCode, which people often do half-heartedly or just forget about.
Immutability—Not for "Simple" Classes
Most of the time, DTOs should be immutable: you get the data once—and never change it. But a class with auto-properties just doesn't cut it: anyone can change any field.
3. What This Looks Like in a Big Project
Let's check out a real-life example.
You're a dev at a huge online store. You need to send order info over the network. So you make a class like this:
public class OrderDto
{
public int Id { get; set; }
public string Customer { get; set; }
public double Total { get; set; }
}
Everything works fine until you start getting a couple hundred thousand orders a week. Tons of microservices start tossing DTOs around, someone somewhere forgets that Total should be calculated, not set by hand, someone accidentally changes Customer after sending... And now you've got bugs that are a nightmare to track down.
How Can We Make It Better?
- Use only get-properties (no set).
- Come up with a special constructor so values are set only at creation.
- Override Equals and GetHashCode…
See where this is going? What was supposed to be a simple "suitcase" suddenly turns into a weird monster.
4. Quick Breakdown of Typical DTO Class Problems
In small projects, bugs from changing DTOs can be "caught by hand." In big ones—it's a disaster. DTOs are used for auth, money transfers, order processing—any illegal data change can turn into lost cash or (damn!) a criminal case.
Here's a "cheat sheet" of problems almost everyone runs into when writing DTOs as classes:
| Problem | Description |
|---|---|
| Mutability | Data is super easy to change anywhere—risk of bugs (especially with multithreading) |
| Cloning | Copying objects is a pain, usually done by hand |
| Comparison | By default, compared by reference, not by value |
| with-copy support | Would be nice to copy with some changed data without manual copying |
| Non-obvious nesting | Nested DTOs have to be copied fully, which is a drag |
5. Example: Passing Data Between Layers in Our App
Let's say we've got a daily planner app where we can store tasks. Before, we'd have a class like this:
public class TaskDto
{
public int Id { get; set; }
public string Description { get; set; }
public DateTime DueDate { get; set; }
}
We'd read TaskDto from a file, then add it to a collection, then show it on the screen. Everything was chill... until someone from another module accidentally changed DueDate right before serialization—and the user got all confused.
7. Can We Do Better?
Heck yeah! And not just can—we should! To fix these problems, C# introduced a special thing—record. It solves most of the headaches with DTO classes, almost like magic.
What does record do:
- Value-oriented by default: objects are compared by content, not by reference.
- Immutability: you can set only get-properties, values are set via constructor.
- Easy cloning: you get the with syntax for copying with changes.
- Auto-generated methods for comparison and display.
public record TaskDto(int Id, string Description, DateTime DueDate);
Cool? Heck yeah! But more on that—in the next lecture.
GO TO FULL VERSION