Basic Request Validation
Let's continue the create book API by adding
[ApiController]
[Route("[controller]")]
public class BookController(ILogger<BookController> logger) : Controller
{
//...
[HttpPost]
public IActionResult Create(CreateBookRequest request)
{
logger.LogInformation("BookController.Create - Request {@request}", request");
//TODO create book
var id = 1;
logger.LogInformation("BookController.Index - Response {@response}", id);
return Ok(id);
}
}
The class CreateBookRequest doesn't exist, right click it or with the cursor on it press Opt + Enter
and choose Create type 'CreateBookRequest'
It will generate the following code
public class CreateBookRequest
{
}
With cursor on it, press Cmd + R + O
and move to folder Dtos
. DTO stands for data transfer objects which as the name suggests, have no logic.
Add these 2 fields with validation attribute [Required]
public class CreateBookRequest
{
[Required]
public string Name { get; set; }
[Required]
public int PublicationYear { get; set; }
}
Call the API and notice the response states The Name is required
while PublicationYear was not stated as required. This is because int's default value is 0. \
[MoreThanZero] attribute
To validate numbers like int, long, decimal and double, we can add a custom attribute. In Utilities folder, add MoreThanZero.cs
public class MoreThanZeroAttribute : RequiredAttribute
{
private readonly string _propertyName;
public MoreThanZeroAttribute([CallerMemberName] string propertyName = "")
{
ErrorMessage = $"{propertyName} must be more than 0";
_propertyName = propertyName;
}
protected override ValidationResult? IsValid(object value, ValidationContext validationContext)
{
switch (value)
{
case int and > 0:
case decimal and > 0:
case long and > 0:
case double and > 0:
return ValidationResult.Success;
default:
return new ValidationResult(ErrorMessage, new List<string> { _propertyName });
}
}
}
Back in CreateBookRequest, for PublicationYear, use [MoreThanZero]
[MoreThanZero] //<<
public int PublicationYear { get; set; }
Test the API again, PublicationYear will be validated as need to be more than zero.
[DateRequired]
public class DateRequiredAttribute : RequiredAttribute
{
private readonly string _propertyName;
public DateRequiredAttribute([CallerMemberName] string propertyName = "")
{
ErrorMessage = $"{propertyName} is required";
_propertyName = propertyName;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (value is DateTime dateTime)
{
if (dateTime != DateTime.MinValue)
{
return ValidationResult.Success;
}
}
return new ValidationResult(ErrorMessage, new List<string> { _propertyName });
}
}
[GuidRequired]
public class GuidRequiredAttribute : RequiredAttribute
{
private readonly string _propertyName;
public GuidRequiredAttribute([CallerMemberName] string propertyName = "")
{
ErrorMessage = $"{propertyName} is required and must be GUID";
_propertyName = propertyName;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (value == null || string.IsNullOrWhiteSpace(value.ToString()) || !Guid.TryParse(value.ToString(), out _))
{
return new ValidationResult(ErrorMessage, new List<string> { _propertyName });
}
return ValidationResult.Success;
}
}
[ListRequired]
public class ListRequiredAttribute : ValidationAttribute
{
private readonly int _minCount;
public ListRequiredAttribute(int minCount = 1)
{
_minCount = minCount;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (value == null || (value is IList list && list.Count < _minCount))
{
return new ValidationResult($"{validationContext.DisplayName} must have at least {_minCount} item(s)",
new List<string> { validationContext.MemberName });
}
return ValidationResult.Success;
}
}