The Best Way to Validate Objects in .NET (2024 Guide)
A friendly guide to validating objects in .NET: Data Annotations, FluentValidation, IValidatableObject, and the new built-in validation in .NET 10.
Introduction
Think about going to the airport. Before you board your flight, you pass through a security check. A guard looks at your ticket and makes sure it is real. Another person checks that your bag is not too heavy. Someone confirms your name matches your passport. Only after all these checks pass do you get to walk onto the plane.
Your program needs the same kind of check at the gate. When data comes in from a user, a form, or another service, you must ask simple questions before you let it inside. Is the name filled in? Is the age a real number? Is the email shaped like an email? This careful checking is called validation.
Validation means making sure an object holds good, sensible data before your code trusts it. If you skip this step, bad data leaks deep into your app. Then you get strange bugs, broken pages, and even security holes. So we check at the gate, early and clearly.
In this guide we will look at the main ways to validate objects in .NET, from the oldest and simplest to the newest in .NET 10. You will learn what each one is good at, where each one struggles, and how to pick the right tool. Let us begin with the big picture.
Why validation matters
Imagine a website where people sign up. A user types their age into a box. They accidentally type "twelve" instead of "12". If your code never checks this, it might crash later when it tries to do math with the word "twelve". Or worse, it might save bad data and confuse every report after that.
Good validation gives three big wins:
- It protects your data so only sensible values get saved.
- It gives users clear, friendly error messages instead of a crash.
- It keeps your code clean because the rest of your app can trust the data.
There is one golden rule here. Never trust input from the outside. A user can type anything. Another service can send broken data. So you always check first.
The three main approaches
In .NET, there are three popular ways to validate an object. Here is a quick map of them before we go deeper into each one.
Three ways to validate in .NET
Steps
Data Annotations
Attributes on the model
FluentValidation
Rules in a separate class
Built-in .NET 10
AddValidation for Minimal APIs
Let us walk through each one with real code so you can see how they feel in practice.
Approach 1: Data Annotations
Data Annotations are small labels you place right above your properties. They are built into .NET, so you do not need to install anything. You just add an attribute like [Required] and the framework knows what to check.
Here is a simple user model with a few rules.
using System.ComponentModel.DataAnnotations;
public class RegisterUser
{
[Required(ErrorMessage = "Please enter your name.")]
[StringLength(50, MinimumLength = 2)]
public string Name { get; set; } = string.Empty;
[Required]
[EmailAddress(ErrorMessage = "That email does not look right.")]
public string Email { get; set; } = string.Empty;
[Range(18, 120, ErrorMessage = "Age must be between 18 and 120.")]
public int Age { get; set; }
}Read it like a sentence. The name is required and must be between 2 and 50 letters. The email is required and must look like an email. The age must sit between 18 and 120. The rules live right next to the data, so they are easy to see.
In an ASP.NET Core MVC or controller app, this validation runs automatically. The framework fills a thing called ModelState. If anything is wrong, ModelState.IsValid becomes false, and you can send the errors back to the user.
[HttpPost]
public IActionResult Register(RegisterUser user)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
// Data is clean here, safe to save.
return Ok("Welcome!");
}Where Data Annotations shine and where they struggle
Data Annotations are wonderful for small, plain rules. But they have limits. They cannot easily call a database to ask "is this email already taken?" because they do not support async work well. They also mix your rules into your model class, which can get messy when rules grow complex or depend on each other.
| Strength | Weakness |
|---|---|
| Built in, zero setup | No clean async support |
| Quick to read | Hard to do complex rules |
| Great for small apps | Rules live inside the model |
| Common attributes ready | Reusing rules is awkward |
Approach 2: FluentValidation
When your rules grow up, FluentValidation is the friend you want. It is a free, open-source library. The big idea is simple: you keep your validation rules in a separate class, away from the model. This keeps both the model and the rules clean and easy to test.
A quick and important note on licenses. Some popular .NET libraries, like MediatR and MassTransit, moved to a paid commercial license. FluentValidation did not. It is still free and open source. But the lesson stands: always check a library's license before you add it to a real project.
To use it, install the FluentValidation package, then write a validator class.
using FluentValidation;
public class RegisterUserValidator : AbstractValidator<RegisterUser>
{
public RegisterUserValidator()
{
RuleFor(u => u.Name)
.NotEmpty().WithMessage("Please enter your name.")
.Length(2, 50);
RuleFor(u => u.Email)
.NotEmpty()
.EmailAddress().WithMessage("That email does not look right.");
RuleFor(u => u.Age)
.InclusiveBetween(18, 120);
}
}The rules read almost like plain English. "Rule for the name: it must not be empty, and its length must be between 2 and 50." Notice how the RegisterUser model stays completely clean. No attributes at all. The model only holds data, and the validator only holds rules. That separation is the heart of why people love this library.
The superpower: async and cross-field rules
FluentValidation can do things Data Annotations cannot. It can run async checks, like asking a database if an email is already used. It can also write rules that look at more than one field at once.
RuleFor(u => u.Email)
.MustAsync(async (email, cancellation) =>
{
// Pretend this checks the database.
return await IsEmailFreeAsync(email);
})
.WithMessage("This email is already taken.");Here is how a request flows when FluentValidation is in charge.
Approach 3: IValidatableObject
Sometimes a rule needs to look at the whole object at once. For example, "the end date must come after the start date." Neither field alone is wrong, but together they can be. For this, .NET gives you an interface called IValidatableObject.
You add one method named Validate to your model. Inside it, you can compare fields and yield return an error when something is off.
using System.ComponentModel.DataAnnotations;
public class Booking : IValidatableObject
{
public DateTime StartDate { get; set; }
public DateTime EndDate { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext context)
{
if (EndDate <= StartDate)
{
yield return new ValidationResult(
"End date must be after the start date.",
new[] { nameof(EndDate) });
}
}
}This runs after the simple attribute checks pass. So the order is: first the [Required] style rules, then your custom Validate method. It is a handy middle ground when you do not want a whole separate library but still need a rule that spans two fields.
New in .NET 10: built-in Minimal API validation
For a long time, Minimal APIs had a gap. They did not validate request models on their own. You had to wire up FluentValidation yourself or write checks by hand. .NET 10 fixes this. It adds built-in validation for Minimal APIs.
Turning it on takes two small steps.
First, register the service in your Program.cs:
builder.Services.AddValidation();Second, add a line to your .csproj file so the source generator can do its work. This part is easy to forget, and forgetting it is the most common reason the feature seems "broken":
<PropertyGroup>
<InterceptorsNamespaces>$(InterceptorsNamespaces);Microsoft.AspNetCore.Http.Validation.Generated</InterceptorsNamespaces>
</PropertyGroup>After that, ASP.NET Core will validate your request models using the same familiar Data Annotation attributes you already know, like [Required], [Range], [StringLength], and [EmailAddress]. It even works for values in the route, the query string, and headers. And it understands IValidatableObject too, so your cross-field rules keep working.
When validation fails, you get a clean, standard error response called ProblemDetails with a 400 Bad Request status. You do not have to format the errors yourself.
Enabling built-in validation in .NET 10
Steps
AddValidation()
Register the service
Edit .csproj
Add InterceptorsNamespaces
Attributes work
Returns ProblemDetails on error
Here is the order in which the checks run, drawn out so it is clear.
How to choose
You might be wondering which one to use. There is no single "best" for every case, but there is a best for your case. Here is a simple table to guide you.
| Your situation | Good choice |
|---|---|
| Tiny app, simple rules | Data Annotations |
| Minimal API on .NET 10 | Built-in validation |
| Rule spans two fields | IValidatableObject |
| Complex or async rules | FluentValidation |
| Want rules out of the model | FluentValidation |
| Big team, many shared rules | FluentValidation |
A very common and healthy pattern is to mix them. Use Data Annotations or built-in validation for the easy checks, and reach for FluentValidation when a rule gets tricky. You do not have to pick only one for the whole project.
A few friendly tips
Here are some habits that keep validation pleasant as your project grows.
- Validate at the edge. Check data the moment it enters your app, before it touches your real logic.
- Write clear messages. "Age must be between 18 and 120" helps a user far more than "Invalid input".
- Keep models clean when you can. If rules get heavy, move them to a FluentValidation class.
- Test your rules. Validation is just code, and code deserves tests. FluentValidation is especially easy to test.
- Never rely only on the browser. Front-end checks are nice for users, but the server must still validate, because anyone can skip the front end.
References and further reading
- Model validation in ASP.NET Core — Microsoft Learn
- FluentValidation Documentation
- Minimal API Validation in .NET 10 — Nikola Tech
- Built-in Validation with IValidatableObject — dotnet/aspnetcore on GitHub
- The Best Way To Validate Objects in .NET in 2024 — antondevtips
Quick recap
- Validation is a security gate that checks data before your app trusts it.
- Data Annotations are built in and great for small, simple rules, but weak at async and complex logic.
- FluentValidation keeps rules in a separate class, supports async and cross-field rules, and is still free and open source.
- IValidatableObject lets you write one rule that looks at the whole object, like comparing two dates.
- .NET 10 adds built-in validation for Minimal APIs: call
AddValidation()and add theInterceptorsNamespacesline to your.csproj. - There is no single best tool. Pick by your needs, and feel free to mix Data Annotations with FluentValidation.
- Always validate on the server, never trust input, and write friendly error messages.
Related Posts
SOLID Principles in C# and .NET: A Beginner-Friendly Guide
Learn the 5 SOLID principles in C# and .NET with simple words, real-life examples, diagrams, and clean code you can copy and try yourself today.
CQRS Validation with MediatR Pipeline and FluentValidation in .NET
Learn centralized CQRS validation in .NET using a MediatR pipeline behavior and FluentValidation. Simple words, clear diagrams, and real C# code.
How to Easily Create PDF Documents in ASP.NET Core
A simple, friendly guide to creating PDF documents in ASP.NET Core with QuestPDF, with clear code, diagrams, tables, and tips for invoices and reports.
Getting Started with FastEndpoints for Building Web APIs in .NET
A friendly beginner guide to FastEndpoints in .NET. Learn the REPR pattern, build your first endpoint, add validation, and see how it compares to controllers.
Getting Started with Hot Chocolate GraphQL in ASP.NET Core
A friendly beginner guide to building a GraphQL API in ASP.NET Core with Hot Chocolate. Learn queries, mutations, resolvers, and DataLoaders with simple examples.
Options Pattern Validation in ASP.NET Core With FluentValidation
Validate the options pattern in ASP.NET Core using FluentValidation, IValidateOptions, and ValidateOnStart. Simple steps, diagrams, and full code examples.