Index
While I was coding for the API post, I realized I have never explained a minimal but crucial difference when developing APIs.
We need to understand the difference between DTO and Entity.
1- What is a DTO
As its name suggests, a DTO
is a "Data Transfer Object" and it's the object we return from our APIs to other services. It should only contain data, no business logic at all.
This object must be serializable.
Note: ViewModel is NOT the same as DTO, even though in many scenarios they can overlap. A ViewModel can contain business logic, and we use them in web applications, whereas DTOs are used for APIs. A common practice is that a ViewModel is composed of multiple DTOs, among other properties.
2 - What is an Entity
An entity is made up of two things:
- An object that represents data from the database; for example, an object that represents each row in the database.
- An entity that encapsulates critical business rules of our application that relate to this object and that can contain some business logic.
Keep in mind that it should NEVER contain more properties than what exists in the database.
3 - Why do we need to differentiate DTO and Entity?
The easiest way to see why we need this separation is through an example.
Let's base the example on a database table that we will use in this project; as we recall from the previous post, it's to create a website, our CV.
For this example I'll use the education part, where we have an object containing: id, start date, end date, degree name, the educational institution or university, and finally the grade. This matches each row in the database:
public class Educacion{ public int Id { get; set; } public DateTime StartDate { get; set; } public DateTime? EndDate { get; set; } public string CourseName { get; set; } public string UniversityName { get; set; } public decimal Mark { get; set; } }
With this in mind, imagine we're returning this information in our API. We have other applications or our own front end (which we'll build in Blazor) that consumes this information and displays it.
So far so good, but, what happens if we no longer want to store the grade in the database, because with experience we see it's completely irrelevant; the grade is only relevant for a first job.
As we said, the entity should only contain the fields in the database, so we should remove the grade from the entity.
At this point we have two options:
- Instead of obtaining the grade from the database, we get it from another source. Let's say we get the grade directly from the government, in this case we're also removing the grade from the entity. But, we still send the grade. In this scenario we only need to update where the grade is fetched from; the rest of the applications or services will not be affected.
- In our specific case, we should remove the grade property from the DTO, but clearly, the end result is the same, our DTO won't contain the property. In this case, the best option is to version the API or the object itself. Because other services using our API may break since a field is missing.
4 - Versioning API in C#
First of all, and before starting, this is very important: if you are using an API and the only clients or consumers are yourselves, DO NOT create versions, instead try to update the applications that reference it. In the end it's less work to update two applications that use a service than to maintain different API versions. Always avoid unnecessary versions if you can.
Another option is to maintain two separate repositories; version 1 returns one result, version 2 returns another. When switching from version 1 to 2, version 1 is only modified to fix bugs; no new features are added. This scenario is very common in old codebases.
But the option I find most recommendable is versioning the API in the URL.
Because for the client, it's very easy to understand—if they call `v1
` that's version 1, if they call `v2
` that's version 2.
The downside is that clients must update the URL to call the correct version.
To use versions in .NET, you need to use the library Microsoft.AspNetCore.Mvc.Versioning
And with this, we can specify which API versions we accept using the [ApiVersion]
attribute on the class. But before that, we need to add versioning to our middleware services in the startup.cs file.
public void ConfigureServices(IServiceCollection services){ services.AddControllers(); services.AddApiVersioning();}
Once we have this, we have to change the route in our controller to add version checking.
[Route("api/v{version:apiVersion}/[controller]")][ApiVersion("1")][ApiController]public class PerfilPersonalController : ControllerBase{ [HttpGet("{id}")] public string Get(int id) { ///Codigo } //Resto de métodos}
Right now, our API will only accept calls to the URL https://www.dominio.com/api/v1/perfilpersonal/{id}
But we want to have multiple DTOs at the same time, since version 2 removes the grade from our object. For this, our DTO library should contain both versions, but in different namespaces.
To do this, we create a folder in the DTO project for version 1 and one for version 2.
And, of course, we need different DTOs when responding:
namespace WebPersonal.Shared.Dto.V1{ public class EducationDto { public int Id { get; set; } public DateTime StartDate { get; set; } public DateTime? EndDate { get; set; } public string CourseName { get; set; } public string UniversityName { get; set; } public decimal Mark { get; set; } }}namespace WebPersonal.Shared.Dto.V2{ public class EducationDto { public int Id { get; set; } public DateTime StartDate { get; set; } public DateTime? EndDate { get; set; } public string CourseName { get; set; } public string UniversityName { get; set; } }}
Now, at this point, we have two Dto
with the same name, but different namespaces, but what we want is to call https://www.dominio.com/api/v2/perfilpersonal/{id}
and get the new Dto
.
To do this, we need to follow the same logic with the controller as with the Dto, which means using multiple namespaces.
As we can see, by having two controllers, each one can return a different version of the DTO. To do this, we need to specify it in the using. And of course, in the controller, we should specify [ApiVersion("2")]
.
using System;using Microsoft.AspNetCore.Mvc;using WebPersonal.BackEnd.Model.Entity;using WebPersonal.BackEnd.Model.Mappers.v2;using WebPersonal.Shared.Dto.V2;namespace WebPersonal.BackEnd.API.Controllers.v2{ [ApiController] [Route("api/v{version:apiVersion}/[controller]")] [ApiVersion("2")] public class EducationController : ControllerBase { [HttpGet("{id}")] public EducationDto Get(int id) { var entity = EducationEntity.Create(id, DateTime.UtcNow, null, "ejemplo", "UniversityEjemplo"); return entity.Map(); } }}
Note: In my case, I created a mapper to map from entity to Dto, so I also need to version this Dto.
And with this, we can already make calls to multiple versions of the API:
If we call version 1:
Url: https://localhost:44363/api/v1/Education/1Response:{ "id": 1, "startDate": "2020-06-14T14:11:50.3187551Z", "endDate": null, "courseName": "ejemplo", "universityName": "UniversityEjemplo", "mark": 5}
If we call version 2:
Url: https://localhost:44363/api/v2/Education/1Response: { "id": 1, "startDate": "2020-06-14T14:12:15.0414859Z", "endDate": null, "courseName": "ejemplo", "universityName": "UniversityEjemplo"}
As we see, for the client the only change needed to use the new API is to switch the version, and the result will come back without the grade field.
Conclusion
- Using DTOs and entities can be considered a best practice, so we may not always see the benefits immediately when developing or working with them. But we will likely see their advantages if we need to change the application in the future or integrate with other services.
- Separating both concepts gives us code independence, and certain changes do NOT affect each other. This is one of the main reasons why old applications are so hard to maintain—because all the concepts are mixed up. You never know where changes might have a quick effect, and this directly impacts progress. Separating concerns leads to much easier maintenance.
- Both classes should NOT share information. For example, if both the DTO and the entity use the "course" object, we should not use it as a shared property. Instead, we have a DTO for one case and an entity for the other. Just like when we have two classes with a repeated property, we don't create a class with that common property for both.
If there is any problem you can add a comment bellow or contact me in the website's contact form