[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
[HttpPost]
public IActionResult Create(Product product)
{
// [ApiController] tự động check ModelState.IsValid
// Nếu invalid, tự động return 400 Bad Request
// Xử lý khi valid
return CreatedAtAction(nameof(GetById), new { id = product.Id }, product);
}
[HttpGet("{id}")]
public IActionResult GetById(int id)
{
return Ok(new { id });
}
}
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"errors": {
"Name": ["Name is required"],
"Price": ["Price must be between 0.01 and 9999.99"]
}
}
public class MinimumAgeAttribute : ValidationAttribute
{
private readonly int _minimumAge;
public MinimumAgeAttribute(int minimumAge)
{
_minimumAge = minimumAge;
}
protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
{
if (value is DateTime birthDate)
{
var age = DateTime.Today.Year - birthDate.Year;
if (birthDate > DateTime.Today.AddYears(-age)) age--;
if (age < _minimumAge)
{
return new ValidationResult($"Must be at least {_minimumAge} years old");
}
}
return ValidationResult.Success;
}
}
// Sử dụng
public class UserRegistration
{
[Required]
public string Name { get; set; } = string.Empty;
[MinimumAge(18, ErrorMessage = "You must be at least 18 years old")]
public DateTime DateOfBirth { get; set; }
}
public class DateRangeAttribute : ValidationAttribute
{
private readonly string _startDateProperty;
private readonly string _endDateProperty;
public DateRangeAttribute(string startDateProperty, string endDateProperty)
{
_startDateProperty = startDateProperty;
_endDateProperty = endDateProperty;
}
protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
{
var startDateProperty = validationContext.ObjectType.GetProperty(_startDateProperty);
var endDateProperty = validationContext.ObjectType.GetProperty(_endDateProperty);
if (startDateProperty?.GetValue(validationContext.ObjectInstance) is DateTime start &&
endDateProperty?.GetValue(validationContext.ObjectInstance) is DateTime end)
{
if (start >= end)
{
return new ValidationResult("Start date must be before end date");
}
}
return ValidationResult.Success;
}
}
// Sử dụng
[DateRange("StartDate", "EndDate", ErrorMessage = "Invalid date range")]
public class Event
{
public DateTime StartDate { get; set; }
public DateTime EndDate { get; set; }
}
public class Order : IValidatableObject
{
public int Id { get; set; }
public DateTime OrderDate { get; set; }
public DateTime? ShipDate { get; set; }
public List<OrderItem> Items { get; set; } = new();
public string? CouponCode { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
// Cross-property validation
if (ShipDate.HasValue && ShipDate <= OrderDate)
{
yield return new ValidationResult(
"Ship date must be after order date",
new[] { nameof(ShipDate) });
}
// Collection validation
if (!Items.Any())
{
yield return new ValidationResult(
"Order must have at least one item",
new[] { nameof(Items) });
}
// Business rule validation
if (Items.Count > 10 && string.IsNullOrEmpty(CouponCode))
{
yield return new ValidationResult(
"Orders with more than 10 items require a coupon code",
new[] { nameof(CouponCode) });
}
}
}
public class OrderItem
{
public int ProductId { get; set; }
public int Quantity { get; set; }
public decimal Price { get; set; }
}
public static class CustomValidators
{
public static IRuleBuilderOptions<T, string> MustBeValidPhoneNumber<T>(
this IRuleBuilder<T, string> ruleBuilder)
{
return ruleBuilder.Must(phone =>
phone != null && phone.All(char.IsDigit) && phone.Length >= 10 && phone.Length <= 15)
.WithMessage("Phone number must be 10-15 digits");
}
public static IRuleBuilderOptions<T, string> MustBeStrongPassword<T>(
this IRuleBuilder<T, string> ruleBuilder)
{
return ruleBuilder
.MinimumLength(8).WithMessage("Password must be at least 8 characters")
.Matches("[A-Z]").WithMessage("Password must contain uppercase letter")
.Matches("[a-z]").WithMessage("Password must contain lowercase letter")
.Matches("[0-9]").WithMessage("Password must contain digit")
.Matches("[^a-zA-Z0-9]").WithMessage("Password must contain special character");
}
}
// Sử dụng
public class UserRegistrationValidator : AbstractValidator<UserRegistration>
{
public UserRegistrationValidator()
{
RuleFor(x => x.Phone).MustBeValidPhoneNumber();
RuleFor(x => x.Password).MustBeStrongPassword();
}
}
// Data Annotations cho simple validation
public class Product
{
[Required]
public string Name { get; set; } = string.Empty;
[Range(0.01, 9999.99)]
public decimal Price { get; set; }
}
// FluentValidation cho complex validation
public class ProductValidator : AbstractValidator<Product>
{
public ProductValidator()
{
RuleFor(x => x.Name)
.MustAsync(BeUniqueName).WithMessage("Product name must be unique");
RuleFor(x => x.Price)
.MustAsync(NotExceedBudget).WithMessage("Price exceeds budget limit");
}
}
// ✅ Validate sớm
public async Task<IActionResult> Create(Product product)
{
// Validation xảy ra trước khi xử lý business logic
if (!ModelState.IsValid)
return BadRequest(ModelState);
// Chỉ chạy khi valid
await _service.ProcessAsync(product);
return Ok();
}