DTOs and Model Validation in .NET Core Web API
When building a Web API, accepting data from clients is only one part of the development process. We also need to ensure that the incoming data is valid, expose only the information that clients actually need, and protect our internal application models.
For example, imagine we are developing an Employee Management API.
A client wants to register a new employee by sending their name, email address, department, and age.
Should we directly accept our database Employee entity from the client?
Should we return the complete database entity in every API response?
Not necessarily.
This is where DTOs (Data Transfer Objects) and Model Validation become important.
In this tutorial, we will understand both concepts and implement them step by step using a practical Employee Management API.
1. What is a DTO?
DTO stands for Data Transfer Object.
A DTO is a simple object used to transfer data between different parts of an application or between an API and its client.
Instead of exposing our database entity directly, we create separate classes that define exactly which properties the client can send or receive.
For example, our Employee entity might contain:
EmployeeId
FullName
Email
Department
Salary
CreatedDate
IsActive
But when a client registers an employee, we may only need:
FullName
Email
Department
Salary
We don't need the client to provide EmployeeId or CreatedDate because these values can be managed by the application.
A DTO allows us to control this data transfer.
2. Why do we need DTOs?
Consider an application where we directly expose our database entity through API endpoints.
This can create several problems:
1. Unnecessary data exposure
Our database entity may contain internal properties that should not be visible to API consumers.
2. Uncontrolled updates
A client might send values for properties such as EmployeeId, IsActive, or other fields that should only be controlled by the application.
3. Tight coupling
If we change our database entity, the API response structure may also change unexpectedly.
4. Different requirements for requests and responses
The properties required when creating an employee may be different from the properties returned when retrieving employee information.
DTOs help us address these problems by separating the API contract from the database entity.
3. Create the Employee Entity
Let's start with our Employee entity.
This represents the employee data maintained by our application.
Create a folder named Entities and add a class called Employee.cs.
namespace EmployeeManagementAPI.Entities
{
public class Employee
{
public int EmployeeId { get; set; }
public string FullName { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string Department { get; set; } = string.Empty;
public decimal Salary { get; set; }
public DateTime CreatedDate { get; set; }
public bool IsActive { get; set; }
}
}Notice that this entity contains additional properties such as EmployeeId, CreatedDate, and IsActive.
These properties are maintained by our application rather than supplied directly by the client during employee registration.
4. Create a Request DTO
Now let's create our first DTO.
A Request DTO defines the data that the API expects from the client.
Create a folder named DTOs, followed by a class named CreateEmployeeRequestDto.cs.
namespace EmployeeManagementAPI.DTOs
{
public class CreateEmployeeRequestDto
{
public string FullName { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string Department { get; set; } = string.Empty;
public decimal Salary { get; set; }
}
}This DTO contains only the properties required to register a new employee.
The client cannot directly supply EmployeeId, CreatedDate, or IsActive through this request model.
5. Create a Response DTO
A Response DTO defines the data that our API returns to the client.
Create another class named EmployeeResponseDto.cs inside the DTOs folder.
namespace EmployeeManagementAPI.DTOs
{
public class EmployeeResponseDto
{
public int EmployeeId { get; set; }
public string FullName { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string Department { get; set; } = string.Empty;
public decimal Salary { get; set; }
public bool IsActive { get; set; }
}
}We now have separate models for incoming and outgoing data.
This gives us better control over our API contract.
6. What is Model Validation?
Creating DTOs helps us control the structure of incoming data.
But what happens if a client sends an empty name, an invalid email address, or a negative salary?
We need to validate the incoming data before processing it.
Model Validation checks whether the submitted data satisfies the rules defined for our request model.
In .NET, we can use Data Annotation attributes to define common validation rules.
Some commonly used validation attributes are:
| Attribute | Purpose |
|---|---|
[Required] | Ensures a value is provided |
[StringLength] | Restricts string length |
[MinLength] | Defines minimum string length |
[MaxLength] | Defines maximum string length |
[Range] | Restricts a numeric value to a range |
[EmailAddress] | Validates email address format |
[RegularExpression] | Validates a value against a pattern |
Let's apply these attributes to our Employee request DTO.
7. Implement Validation in the Request DTO
Update CreateEmployeeRequestDto.cs:
using System.ComponentModel.DataAnnotations;
namespace EmployeeManagementAPI.DTOs
{
public class CreateEmployeeRequestDto
{
[Required(ErrorMessage = "Employee name is required.")]
[StringLength(100, MinimumLength = 3,
ErrorMessage = "Employee name must be between 3 and 100 characters.")]
public string FullName { get; set; } = string.Empty;
[Required(ErrorMessage = "Email address is required.")]
[EmailAddress(ErrorMessage = "Please enter a valid email address.")]
public string Email { get; set; } = string.Empty;
[Required(ErrorMessage = "Department is required.")]
[StringLength(50, ErrorMessage = "Department cannot exceed 50 characters.")]
public string Department { get; set; } = string.Empty;
[Range(1, 1000000,
ErrorMessage = "Salary must be greater than zero.")]
public decimal Salary { get; set; }
}
}Let's understand these validation rules.
FullName cannot be empty and must contain between 3 and 100 characters.
Email must contain a valid email address format.
Department is required and cannot exceed 50 characters.
Salary must be between 1 and 1,000,000.
Now our request DTO contains both the data structure and its validation rules.
8. How Does Model Validation Work in a Controller?
In a controller-based Web API, we can use the [ApiController] attribute.
It provides automatic model validation for invalid request models.
When validation fails, the framework automatically returns an HTTP 400 Bad Request response with validation details.
We don't need to write ModelState.IsValid checks manually for the standard automatic validation behavior provided by [ApiController].
Let's implement our Employee API.
First, create an interface named IEmployeeService.cs inside a Services folder.
using EmployeeManagementAPI.Entities;
namespace EmployeeManagementAPI.Services
{
public interface IEmployeeService
{
Task<Employee> CreateAsync(Employee employee);
Task<Employee?> GetByIdAsync(int employeeId);
Task<IEnumerable<Employee>> GetAllAsync();
}
}We are using an interface so that the controller can depend on an abstraction rather than directly depending on a concrete service implementation.
This follows the Dependency Injection approach we discussed in Day 8.
Now create EmployeeController.cs inside the Controllers folder.
using Microsoft.AspNetCore.Mvc;
using EmployeeManagementAPI.DTOs;
using EmployeeManagementAPI.Entities;
using EmployeeManagementAPI.Services;
namespace EmployeeManagementAPI.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class EmployeeController : ControllerBase
{
private readonly IEmployeeService _employeeService;
public EmployeeController(IEmployeeService employeeService)
{
_employeeService = employeeService;
}
[HttpPost]
public async Task<IActionResult> CreateEmployee(
CreateEmployeeRequestDto request)
{
var employee = new Employee
{
FullName = request.FullName,
Email = request.Email,
Department = request.Department,
Salary = request.Salary,
CreatedDate = DateTime.UtcNow,
IsActive = true
};
var createdEmployee =
await _employeeService.CreateAsync(employee);
var response = new EmployeeResponseDto
{
EmployeeId = createdEmployee.EmployeeId,
FullName = createdEmployee.FullName,
Email = createdEmployee.Email,
Department = createdEmployee.Department,
Salary = createdEmployee.Salary,
IsActive = createdEmployee.IsActive
};
return CreatedAtAction(
nameof(GetEmployeeById),
new { id = response.EmployeeId },
response);
}
[HttpGet("{id:int}")]
public async Task<IActionResult> GetEmployeeById(int id)
{
var employee = await _employeeService.GetByIdAsync(id);
if (employee == null)
{
return NotFound();
}
var response = new EmployeeResponseDto
{
EmployeeId = employee.EmployeeId,
FullName = employee.FullName,
Email = employee.Email,
Department = employee.Department,
Salary = employee.Salary,
IsActive = employee.IsActive
};
return Ok(response);
}
[HttpGet]
public async Task<IActionResult> GetAllEmployees()
{
var employees = await _employeeService.GetAllAsync();
var response = employees.Select(employee =>
new EmployeeResponseDto
{
EmployeeId = employee.EmployeeId,
FullName = employee.FullName,
Email = employee.Email,
Department = employee.Department,
Salary = employee.Salary,
IsActive = employee.IsActive
});
return Ok(response);
}
}
}The controller uses the injected IEmployeeService, just as we learned in Day 8.
For simplicity, the example maps between the entity and DTO manually. In a larger application, we can move this mapping into a dedicated mapping layer or use a mapping library.
The service implementation and database configuration are application-specific; the interface here shows the contract required by the controller.
9. Understand Automatic Model Validation with an Example
Let's assume our client sends the following request to the API.
Endpoint:
POST /api/employeeRequest JSON:
{
"fullName": "",
"email": "invalid-email",
"department": "",
"salary": -500
}This request contains multiple validation errors:
FullName is empty.
Email has an invalid format.
Department is empty.
Salary is negative.
Because our controller has the [ApiController] attribute, the request is automatically rejected when model validation fails.
The controller action does not execute.
Expected HTTP status:
400 Bad RequestThe response contains validation information, generally in a validation ProblemDetails response, with details associated with the invalid properties.
This helps API consumers understand what needs to be corrected.
10. Test a Valid Employee Request
Now let's send valid employee information.
Endpoint:
POST /api/employeeRequest JSON:
{
"fullName": "Rahul Sharma",
"email": "rahul@example.com",
"department": "IT",
"salary": 55000
}If the request passes validation and the service successfully creates the employee, the API returns:
HTTP status:
201 CreatedSample response JSON:
{
"employeeId": 101,
"fullName": "Rahul Sharma",
"email": "rahul@example.com",
"department": "IT",
"salary": 55000,
"isActive": true
}Notice that the client does not need to provide EmployeeId, CreatedDate, or IsActive.
The application manages those properties.
Also, our response DTO controls exactly which properties are returned to the client.
11. DTO vs Entity: What Is the Difference?
Many developers initially confuse DTOs with database entities.
Although both are classes, they serve different purposes.
| Entity | DTO |
|---|---|
| Represents application or database data | Represents transferred data |
| Used by application and persistence logic | Used at API boundaries |
| May contain internal properties | Contains selected properties |
| Can be mapped to a database table | Does not need to map to a table |
| Represents the application's data model | Represents the API contract |
For example, an Employee entity may contain internal audit fields, while the EmployeeResponseDto exposes only the information required by the client.
12. Where Should We Use DTOs?
DTOs are particularly useful in applications where multiple clients consume the same API.
Consider an Employee Management application:
An admin dashboard may need employee salary and employment status.
An employee portal may only need name, department, and email.
A mobile application may need a smaller response containing only a few properties.
Instead of exposing the same entity everywhere, we can create different response DTOs for different requirements.
This gives us flexibility without changing our underlying entity.
13. Important Things to Remember
Before implementing DTOs and validation in a real application, keep these points in mind:
Avoid exposing database entities directly through API endpoints.
Create separate request and response DTOs when their requirements differ.
Use Data Annotations for common validation rules.
Use
[ApiController]for automatic model validation in controller-based APIs.Do not trust client-side validation alone. Always validate incoming data on the server.
Remember that DTO validation does not replace business validation. For example, checking whether an email address already exists in the database requires additional application logic.
Keep your API contract separate from your database structure.
Conclusion
DTOs and Model Validation are important concepts when building maintainable and secure .NET Web APIs.
DTOs help us control the information entering and leaving our API, while Model Validation ensures that incoming data follows the rules defined by our application.
In our Employee Management example, we created separate request and response DTOs, applied validation attributes, and used automatic model validation through [ApiController].
We also reused the Dependency Injection concept from Day 8 to keep our controller loosely coupled with the service layer.
What's Next?
Day 10 – Entity Framework Core
In the next article, we will explore Entity Framework Core and understand how to work with databases in .NET Web API applications.
Learn • Build • Grow Together
No comments:
Post a Comment