FluentValidation to znakomita biblioteka ułatwiająca walidację danych. Pozwala zdefiniować reguły walidacji w całkiem zgrabny sposób. Dla podstawowych scenariuszy zawiera gotowe komunikaty błędu w wielu wersjach językowych. Niestety posiada też pewną kuriozalną cechę.
Jeśli do metody Validate zostanie przekazana wartość null, zostanie rzucony wyjątek z informacją „Cannot pass null model to Validate”. Co na ten temat mówi autor biblioteki?
„This is actually by design. The purpose of FluentValidation is to validate properties on objects, which by definition requires a non-null instance in order to work.”
https://github.com/JeremySkinner/FluentValidation/issues/486#issuecomment-307696164
Czyli jak to często w świecie IT bywa: „It’s Not a Bug, It’s a Feature.” Bywają jednak sytuacje, że null jest wartością spodziewaną i chcemy zwrócić błąd walidacji spójny z pozostałymi regułami. Można to zrobić nadpisując metodę Validate walidatora:
public class Product
{
public string Title { get; set; }
public decimal Price { get; set; }
}
public class ProductValidator : AbstractValidator<Product>
{
public ProductValidator()
{
CascadeMode = CascadeMode.StopOnFirstFailure;
RuleFor(x => x.Title)
.NotEmpty()
.MaximumLength(50);
RuleFor(x => x.Price)
.GreaterThan(0);
}
public override ValidationResult Validate(ValidationContext<Product> context)
{
return context.InstanceToValidate == null
? new ValidationResult(new[] { new ValidationFailure(nameof(Product), "Object cannot be null") })
: base.Validate(context);
}
}
Wadą rozwiązania jest utrata wsparcia wielojęzycznego. Można to poprawić korzystając z wbudowanego zestawu komunikatów błędów, w tym przypadku wykorzystywanego przez walidator reguły NotNull:
public override ValidationResult Validate(ValidationContext<Product> context)
{
string propertyName = nameof(Product);
var errorMessage = ValidatorOptions.LanguageManager.GetStringForValidator<NotNullValidator>()
.Replace("{PropertyName}", propertyName);
return context.InstanceToValidate == null
? new ValidationResult(new[] { new ValidationFailure(propertyName, errorMessage) })
: base.Validate(context);
}
Nadpisanie metody Validate nie zadziała dla projektów WebAPI i MVC. Tutaj w przypadku przekazania do kontrolera pustej wartości proces walidacji po prostu się nie odbędzie. Rozwiązaniem może być utworzenie nowego filtru, który dla pustego parametru zwróci BadRequest z odpowiednim komunikatem błędu:
public class ModelStateFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
if (actionContext.ActionArguments.First().Value == null)
{
actionContext.Response = actionContext.Request
.CreateResponse(HttpStatusCode.BadRequest,
new
{
Message = "The request is invalid.",
ModelState = new
{
product = new[]
{
"Object cannot be null"
}
}
});
}
}
}
Pozostaje oznaczyć metodę kontrolera powyższym filtrem lub zarejestrować w klasie WebApiConfig:
[HttpPost]
[ModelStateFilter]
public IHttpActionResult Create(Product product)
{
if (!ModelState.IsValid)
{
return ResponseMessage(Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState));
}
//...
return Ok();
}