Model Validation: Validating Incoming Data with Data Annotations and FluentValidation
We cover validating incoming data: defining quick rules with Data Annotations, the automatic validation of [ApiController], and FluentValidation for complex rules. What to choose and when?
In the previous article we defined input DTOs; we said which fields we expect from the client. But expecting something doesn't guarantee that thing arrives correctly. The client might send us an empty name, an invalid email, or a five-character password. This is exactly where model validation comes into play. Today we talk about how to meet the incoming data at the door and check whether it's valid.
Why Is Validation a Must?
There's a simple rule: never trust any data coming from the outside. No matter how many checks you put on the client side, in the end a raw request can reach your API; a malicious person or a faulty application can send data that doesn't follow your rules. Validation on the server side is the application's last and most reliable line of defense.
Think of validation like a building's entrance security. Everyone who wants to get in passes through the checkpoint first; those missing identification or violating a rule aren't let in. This way, your business logic inside the building always works with clean and reliable data. If you do the check at the door, you don't have to sprinkle "is this data valid?" checks all over your code inside.
Data Annotations: The Quick and Practical Way
The fastest way to validate in .NET is to place attributes above the DTO's properties. We call these Data Annotations. Let's add a few rules to the CreateUserDto from the previous article:
public class CreateUserDto
{
[Required]
[StringLength(50, MinimumLength = 2)]
public string Name { get; set; }
[Required]
[EmailAddress]
public string Email { get; set; }
[Required]
[MinLength(8)]
public string Password { get; set; }
}
Each attribute states a rule. [Required] says the field can't be left empty, [StringLength] the length limits, [EmailAddress] that the value must be in a valid email format, and [MinLength] at least how many characters it must be. The code itself reads almost like a list of rules: "name is required, between 2 and 50 characters; email is required and in valid format; password is required, at least 8 characters."
[ApiController] Already Does Half the Work
Now the best part. Do you remember the [ApiController] attribute we put at the top of our controller in the fourth article? Well, it kicks in automatically when the incoming data doesn't follow these rules. Even if you don't write a single check inside your action method, when a request that doesn't follow the rules arrives, .NET automatically returns a 400 Bad Request and produces a proper error response explaining which field is invalid and why.
So your method can stay this plain:
[HttpPost]
public IActionResult Create(CreateUserDto dto)
{
// If we reached here, it means the data is already valid.
// We don't need to write the validation check by hand.
return Ok($"Created user: {dto.Name}");
}
Notice the inside of the method: there's not a single validation line. Because if the code reached this point, you can be sure that the incoming data passed all the rules. If it hadn't, .NET would have turned the request away without running the method at all. This is a very valuable convenience that keeps your code both clean and secure.
Where Do Data Annotations Fall Short?
Data Annotations are great for simple rules. But when things get complicated, you hit their limits. Let's say you want to write a rule that looks at multiple fields or requires business logic, like "the password must not contain the user's name" or "the start date must be before the end date." Expressing these with attributes becomes either very hard or impossible.
Another issue is that the rules are embedded inside the DTO class. As the number of rules grows, your DTO overflows with attributes and becomes hard to read. This is exactly where a more powerful approach comes in: FluentValidation.
FluentValidation: The Powerful Way for Complex Rules
FluentValidation is a library that doesn't come with .NET itself but is much loved in the community. It lets you define validation rules not inside the DTO, but in a separate class. This way your DTO stays plain, and the rules live in their own home. We write the same rules with FluentValidation like this:
public class CreateUserDtoValidator : AbstractValidator
{
public CreateUserDtoValidator()
{
RuleFor(x => x.Name)
.NotEmpty()
.Length(2, 50);
RuleFor(x => x.Email)
.NotEmpty()
.EmailAddress();
RuleFor(x => x.Password)
.NotEmpty()
.MinimumLength(8);
}
}
Notice how it reads: almost like an English sentence. "For Name: don't be empty, be between 2 and 50 characters." This fluent writing is what gives the library its name. Its real power appears in complex scenarios; here you can comfortably express rules that look at multiple fields, run conditionally, or need to consult the database. As the rules grow, this separate class structure keeps your code organized.
So Which One Should I Choose?
Both are valid tools, and the choice mostly depends on the project's complexity. If your rules are simple (like required fields, length, format checks), Data Annotations are completely enough; you write them quickly without needing an extra library. When your rules start getting complex, involving logic across fields, or drowning the DTO in attributes, switching to FluentValidation keeps your code much cleaner.
A practical piece of advice: if you're on a small project or in the learning phase, start with Data Annotations, because they come ready inside .NET and you'll grasp the concept fastest this way. When the project grows and your rules get serious, you bring in FluentValidation. There's also nothing wrong with using both together in the same project.
A Small Experiment
Add the Data Annotations rules above to your CreateUserDto. Then, with an API testing tool, first send a valid request and see that the method runs. Next, deliberately violate the rules: leave the name empty, send an invalid email, send a short password. Look carefully at the 400 response [ApiController] automatically produces for you and at the messages explaining which fields were rejected and why. This experiment shows most clearly how validation works at the door.
In the next article we'll get into Entity Framework Core: what a DbContext is, how we talk to the database, and how we create the first migration. Until now we've always worked with fixed lists in memory; now the time is coming to store our data in a real database.