Never trust user input. Before executing any business logic or touching the database, you must aggressively validate the incoming JSON payload. In ASP.NET Core, the easiest way to accomplish this is using built-in Data Annotations directly on your DTOs.
Place validation attributes from the System.ComponentModel.DataAnnotations namespace directly onto your DTO properties.
public class CreateUserDto
{
[Required(ErrorMessage = "First name is mandatory.")]
[StringLength(50, MinimumLength = 2, ErrorMessage = "Name must be between 2 and 50 characters.")]
public string FirstName { get; set; }
[Required]
[EmailAddress(ErrorMessage = "Invalid email format.")]
public string Email { get; set; }
[Range(18, 120, ErrorMessage = "User must be at least 18 years old.")]
public int Age { get; set; }
[RegularExpression(@"^(?=.*[a-z])(?=.*[A-Z])(?=.*d).{8,}$",
ErrorMessage = "Password must have at least 8 chars, 1 upper, 1 lower, and 1 number.")]
public string Password { get; set; }
}
If you do not have the [ApiController] attribute on your class, the controller action will physically execute even if the data is invalid. You must manually check if (!ModelState.IsValid) return BadRequest(ModelState); at the top of every single method.
The framework intercepts the incoming request before your code ever runs. If the JSON fails the Data Annotation rules, the framework instantly halts the request and returns an HTTP 400 Bad Request with a detailed JSON array outlining the exact errors.
Sometimes, built-in attributes aren't enough. You can create custom Reusable attributes by inheriting from ValidationAttribute.
// 1. Create the custom logic
public class MustBeFutureDateAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (value is DateTime date)
{
if (date < DateTime.UtcNow)
return new ValidationResult("The date must be in the future.");
return ValidationResult.Success;
}
return new ValidationResult("Invalid date format.");
}
}
// 2. Apply it to the DTO
public class EventDto
{
[Required]
[MustBeFutureDate]
public DateTime EventStartDate { get; set; }
}
Q: "If a user submits an empty JSON payload `{}` to a CreateUser POST endpoint, but I forgot to put the `[Required]` attribute on a value-type property like `public int Age { get; set; }`, what happens?"
Architect Answer: "Because `int` is a non-nullable value type in C#, the JSON deserializer will silently assign it its default value, which is `0`. The model will be considered 'Valid' (since 0 is technically an integer), and your database will incorrectly save the Age as 0. To prevent this silent failure and force validation to catch missing data, you must make the DTO property nullable `public int? Age { get; set; }` AND apply the `[Required]` attribute. This forces the framework to throw a validation error if the field is missing entirely."