In this article, we are going to learn the differences between a DTO (Data Transfer Object) and a POCO (Plain Old CLR/C# Object).
We use these types of objects in everyday coding. However, we are often not sure what the real differences are. We are going to dive deeper and clarify the differences between a DTO and a POCO.
Let’s start.
What is a POCO?
Plain Old C#/CLR Object or POCO is based on the object style. This is different from the DTO which is about usage. POCO classes should always be independent of any frameworks and function on their own.
POCOs are simple and we should be able to instantiate them anywhere without difficulties. POCOs can have methods to implement logic and behaviors like validation. A POCO has no naming conventions for properties or methods.
What is a DTO?
A DTO stands for Data Transfer Object. As the name suggests, its main purpose is to transfer data from one place to another. With that, we can use DTOs to establish communication between multiple parts of our system. One of the most common use cases of DTOs is communication between a server and a client. To pass our DTOs across different systems, DTOs should always be serializable.
We often store some sensitive data in the database, so we don’t want to send it to the client. That’s where DTOs shine, we can map our domain objects to present only the data we need for specific use cases.
Since the only purpose of DTOs is to transfer data, we shouldn’t have any logic or behavior inside our DTOs. That said, DTO should be easy to read and only have properties. If a class has methods with logic or behavior, it is not a DTO.
Immutability with DTOs
DTOs are built for transferring data. In most cases, the client or the receiver of a DTO should not be able to modify it. That’s where immutability comes in handy because we can enforce those rules on our DTOs.
In older versions of C# language, this was more complex to achieve since we had to create our properties as getters. To set data upon object creation, we could use parameterized constructors but then end up with long construction declarations.
Luckily, C# version 9 introduced out-of-the-box immutability with Records that we can use to enforce immutability on our DTOs.
Do DTOs Need Encapsulation?
Since DTOs only transfer the data and don’t have any behavior, we don’t need to enforce encapsulation. That said, we can use fields instead of properties, or even readonly fields if we want to enforce immutability in that way.
However, our DTOs should always be serializable and some serializers only work with properties, so we need to be careful.
Naming Conventions for DTOs
In many cases, we use different suffixes when naming DTOs. One of the most common suffixes is the “DTO” itself. In that case, if we have a Person
domain object, we would call our DTO PersonDTO
. Sometimes we want to split our DTOs based on the different operations we perform. In such cases, there is a possibility that we will have GetPersonDTO
, CreatePersonDTO
, etc.
Another common naming suffix for DTOs is the word “Model”. Some may argue that the “DTO” prefix is too generic since it doesn’t reveal our class place in the architecture. When that is the case, we can use something like PersonViewModel
which clearly states that it is used on the view layer of our architecture.
DTO vs POCO
Now, we are going to create some use cases in .NET Web API project to learn how we can use DTOs and POCOs.
Usage of POCOs
POCOs are frequently used as DDD (Domain Driven Design) entities to model state and behavior. With that, let’s create a domain folder inside our project with a new class:
public class Person { public Person(int id) { Id = id; } public int Id { get; private init; } public string? FirstName { get; set; } public string? LastName { get; set; } public string? Address { get; set; } public DateTime DateOfBirth { get; set; } public string? JobTitle { get; set; } public string DoWork() { return $"{JobTitle} doing his work!"; } public bool IsAdult() { var currentYear = DateTime.Now.Year; var age = currentYear - DateOfBirth.Year; return age >= 21; } }
We can see that we have both properties and methods inside our class to describe behavior. The important thing to note is that no external libraries or dependencies are used within the class.
Secondly, let’s create a fake data store to retrieve the Person
data:
public static class FakeDataStore { public static ICollection<Person> GetPersonDomainObjects() { return new List<Person> { new Person(1) { FirstName = "Bruce", LastName = "Wayne", Address = "DC Street 22", JobTitle = "Batman", DateOfBirth = new DateTime(1972, 7, 8) }, new Person(2) { FirstName = "Tony", LastName = "Stark", Address = "Marvel Street 9", JobTitle = "Iron-Man", DateOfBirth = new DateTime(1968, 5, 5) }, new Person(3) { FirstName = "Peter", LastName = "Parker", Address = "Marvel Street 87", JobTitle = "Spider-Man", DateOfBirth = new DateTime(1992, 2, 4) }, }; } }
Here we return three POCO Person
objects to simulate the database data.
Usage of DTOs
Now, let’s create a DTO based on the Person
class that we can use to transfer data from server to client:
public record PersonDetails(string FirstName, string LastName, string Address, DateTime DateOfBirth);
Here we use most of the fields from our POCO class with the exception of the Id
property in order to hide it from the client. This is a good example of how we can modify DTOs to fit our purposes.
Combination of DTOs and POCOs
Finally, let’s see the combination of DTOs and POCOs in our service class. To do that, let’s create an interface for the service implementation first:
public interface IPersonService { Task<ICollection<PersonDetails>> GetPersonDetailsData(); }
In the IPersonService
interface we have one method for retrieving our PersonDetails
DTOs.
Now, let’s create the implementation for the GetPersonDetailsData()
method:
public async Task<ICollection<PersonDetails>> GetPersonDetailsData() { var people = FakeDataStore.GetPersonDomainObjects(); var personDetailsDtos = people.Adapt<ICollection<PersonDetails>>(); return await Task.FromResult(personDetailsDtos); }
Here we set our POCOs to the people
variable and then use the Adapt()
method from Mapster to map the collection of Person
POCOs to the collection of PersonDetails
DTOs.
Then, we use the Task.FromResult()
method to simulate the asynchronous behavior and return the data of DTOs.
Lastly, let’s create a Minimal API endpoint in our Program
class to retrieve the data:
app.MapGet("/people-details", async (IPersonService personService) => await personService.GetPersonDetailsData());
And then get the results through Postman:
Perfect, we got our PersonDetails
DTO results.
Conclusion
In this article, we’ve learned the main aspects and differences between a POCO (Plain Old CLR/C# Object) and a DTO (Data Transfer Object). We had a look at the usage example of both types and confirmed how we can use them in different parts of our Web API project.
The main conclusion is that DTOs should be used only for transferring data, without any logic or behavior. On the other hand, we should use POCOs to describe the behavior of our object without any third-party dependencies. By their definitions, all DTOs should be POCOs, because if a class is not a POCO, it cannot be a valid DTO. That said, we can see why developers confuse these two types.
However, the reverse is not true, because if that was the case, the two types would be equivalent. So, all DTOs are POCOs, but not all POCOs are DTOs.