Data Annotations and Custom Validation Attributes
Both are used to define validation rules for model properties.
1. Data Annotations
Data Annotations are built-in validation attributes placed on model properties.
public class EmployeeViewModel
{
[Required(ErrorMessage = "Name is required.")]
[StringLength(50, MinimumLength = 3)]
public string Name { get; set; } = string.Empty;
[EmailAddress]
public string Email { get; set; } = string.Empty;
[Range(18, 60)]
public int Age { get; set; }
}
Common Data Annotations
| Attribute | Purpose |
|---|---|
[Required] |
Value is mandatory |
[StringLength] |
Controls maximum and minimum length |
[Range] |
Validates numeric range |
[EmailAddress] |
Validates email format |
[Phone] |
Validates phone format |
[Compare] |
Compares two properties |
[RegularExpression] |
Validates using a pattern |
2. Custom Validation Attribute
Create a custom attribute when built-in annotations cannot handle a specific rule.
Example: Employee joining date cannot be in the future.
using System.ComponentModel.DataAnnotations;
public class NotFutureDateAttribute : ValidationAttribute
{
protected override ValidationResult? IsValid(
object? value,
ValidationContext validationContext)
{
if (value is DateTime date && date > DateTime.Today)
{
return new ValidationResult(
ErrorMessage ??
"Date cannot be in the future.");
}
return ValidationResult.Success;
}
}
Apply the Custom Attribute
public class EmployeeViewModel
{
[Required]
public string Name { get; set; } = string.Empty;
[NotFutureDate(
ErrorMessage = "Joining date cannot be in the future.")]
public DateTime JoiningDate { get; set; }
}
How Validation Runs
Form submission
↓
Model binding
↓
Built-in and custom attributes execute
↓
Errors are stored in ModelState
↓
ModelState.IsValid is checked
[HttpPost]
public IActionResult Create(EmployeeViewModel model)
{
if (!ModelState.IsValid)
return View(model);
return RedirectToAction("Index");
}
Data Annotations vs Custom Attributes
| Data Annotations | Custom validation attributes |
|---|---|
| Built into .NET | Created by the developer |
| Used for common rules | Used for application-specific rules |
| Less code | Requires a class derived from ValidationAttribute |
| Commonly supports client validation | Custom client validation needs additional implementation |
Key Points
- Data Annotations handle common validation rules.
- Custom validation attributes handle reusable, specialized rules.
- A custom attribute derives from
ValidationAttribute. - Override
IsValid()to implement the validation rule. - Validation errors are added to
ModelState. - Complex business rules should usually remain in the domain or service layer, not only in validation attributes.
When Would You Use FluentValidation?
Use FluentValidation when validation rules become complex and Data Annotations make the model difficult to maintain.
Suitable Scenarios
- Complex validation rules
- Conditional validation
- Validation involving multiple properties
- Reusable validation rules
- Separate validation for create and update operations
- Validators requiring dependency injection
- Keeping validation outside ViewModels or DTOs
- Unit-testing validation rules independently
Example
public class EmployeeValidator
: AbstractValidator<EmployeeViewModel>
{
public EmployeeValidator()
{
RuleFor(x => x.Name)
.NotEmpty()
.MaximumLength(50);
RuleFor(x => x.Email)
.NotEmpty()
.EmailAddress();
RuleFor(x => x.Age)
.InclusiveBetween(18, 60);
RuleFor(x => x.ManagerName)
.NotEmpty()
.When(x => x.IsManager);
}
}
Data Annotations vs FluentValidation
| Data Annotations | FluentValidation |
|---|---|
| Built into .NET | Third-party library |
| Attributes placed on properties | Rules stored in separate validator classes |
| Best for simple validation | Best for complex validation |
| Limited conditional rules | Supports conditional and cross-property rules |
| Can clutter large models | Keeps models clean |
| Less flexible for testing | Validators are easy to unit test |
| Good for small applications | Useful for large, layered applications |
When Not to Use It
For a small form with basic rules, Data Annotations are usually sufficient:
[Required]
[StringLength(50)]
public string Name { get; set; }
Key Points
- Use FluentValidation when validation is complex, conditional or reusable.
- It keeps validation separate from the model.
- It supports clear, strongly typed and testable rules.
- It is useful in Clean Architecture because validators can live in the Application layer.
- Do not put core business rules only in FluentValidation; enforce critical business rules in the domain or service layer as well.
Yes, FluentValidation Requires a NuGet Package
For ASP.NET Core MVC, install these packages:
dotnet add package FluentValidation
dotnet add package FluentValidation.DependencyInjectionExtensions
The second package provides dependency-injection registration such as AddValidatorsFromAssemblyContaining().
Register Validators in Program.cs
using FluentValidation;
builder.Services.AddValidatorsFromAssemblyContaining<
EmployeeValidator>();
Inject and Use the Validator
public class EmployeeController : Controller
{
private readonly IValidator<EmployeeViewModel> _validator;
public EmployeeController(
IValidator<EmployeeViewModel> validator)
{
_validator = validator;
}
[HttpPost]
public async Task<IActionResult> Create(
EmployeeViewModel model)
{
var result = await _validator.ValidateAsync(model);
if (!result.IsValid)
{
foreach (var error in result.Errors)
{
ModelState.AddModelError(
error.PropertyName,
error.ErrorMessage);
}
return View(model);
}
return RedirectToAction("Index");
}
}
Important Note
Avoid starting new projects with:
dotnet add package FluentValidation.AspNetCore
That automatic MVC integration package is deprecated and no longer maintained. Prefer manual asynchronous validation using IValidator<T>. The current core and DI packages are listed on NuGet: FluentValidation and FluentValidation.DependencyInjectionExtensions.