Day 21: Data Validation & DTOs
Welcome to Day 21. Weâve been passing full Book entities directly from the client to the database. This is a massive security risk.
If your User model has an IsAdmin boolean property, a clever hacker can send {"username": "hacker", "password": "123", "isAdmin": true} in the JSON body. Your API will happily deserialize that into the entity and save it to the database, making them an admin! This is called an Over-Posting Attack.
Data Transfer Objects (DTOs)
A DTO is a dummy class created purely to define the exact shape of the data you expect to receive or return. It does not exist in the database.
Letâs create a DTO for creating a new user:
public class UserCreateRequestDto
{
public string Username { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
}
Now, the hacker canât set IsAdmin, because IsAdmin simply doesnât exist on the DTO.
Mapping DTOs to Entities
In your controller, you accept the DTO, and then map it over to the real Database Entity manually.
app.MapPost("/users", async (UserCreateRequestDto request, AppDbContext db) =>
{
// The request only contains what we safely expose
// Map DTO to Entity manually
var newUser = new User
{
Username = request.Username,
Password = HashPassword(request.Password),
IsAdmin = false // Hardcoded rule!
};
db.Users.Add(newUser);
await db.SaveChangesAsync();
});
Note: For large applications, developers often use the library AutoMapper to automate bridging fields with matching names.
Data Validation
Once we have a DTO, we need to enforce rules cleanly. We shouldnât allow empty usernames or 2-letter passwords.
ASP.NET Core has built-in Data Annotations.
using System.ComponentModel.DataAnnotations;
public class UserCreateRequestDto
{
[Required]
[StringLength(50, MinimumLength = 3, ErrorMessage = "Username must be 3-50 chars.")]
public string Username { get; set; } = string.Empty;
[Required]
[EmailAddress]
public string Email { get; set; } = string.Empty;
[Required]
[MinLength(8)]
public string Password { get; set; } = string.Empty;
}
If you use MVC Controllers ([ApiController]), ASP.NET Core automatically parses these attributes! If someone submits an empty username, the framework instantly returns a 400 Bad Request with an array of errors before your endpoint logic ever runs.
(For Minimal APIs, you typically install a library like FluentValidation to achieve the same result elegantly).
Challenge for Day 21
Create a minimal DTO for your /books POST endpoint that only accepts a Title and Price. Use data annotations to ensure the Title isnât empty, and the price is always greater than 0.0.
Tomorrow: Global Error Handling!