#csharp #dotnet #backend

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!