Complex model validation using Fluent Validation
Full source code is available here.
A common problem is validating an object using a complicated set of rules.
I started using the Fluent Validation package some time back, it is commonly used with MVC and Web API applications but can be used with in any scenario that requires validation. It allows you to easily and quickly build flexible validation rules.
Of course it is possible to apply basic data annotations or build a custom validation attribute.
But out of the box Fluent Validation offers much more elaborate validators like, CreditCard
, EmailAddress
ExclusiveBetween
and many more. It also has a Must
validator that takes a predicate you define. This allows the creation of any complex rule.
For example, the validator can check that the person create request (shown below) has at least one active primary phone and at least one active primary email.
Fluent Validation can also be easily used in a console app, with data annotations you would have to jump through hoops to get validation working.
1 public class PersonCreateRequestValidator : AbstractValidator<PersonCreateRequest>
2 {
3 public PersonCreateRequestValidator()
4 {
5 RuleFor(r => r.Firstname).NotEmpty();
6 RuleFor(r => r.Lastname).NotEmpty();
7
8 RuleFor(r => r.Addresses).NotNull();
9 RuleFor(r => r.Addresses).NotEmpty();
10
11 RuleFor(r => r.Addresses).Must(HaveAnActiveAndPrimary).WithMessage("One (and only one) address must be primary and active");
12 RuleFor(r => r.Phones).Must(HaveAnActiveAndPrimary).WithMessage("One (and only one) phone must be primary and active");
13 }
14
15 private bool HaveAnActiveAndPrimary(IEnumerable<IActivePrimary> items)
16 {
17 if (items == null)
18 {
19 return false;
20 }
21
22 int activePrimary = items.Count(p => p.IsActive && p.IsPrimary);
23 return (activePrimary == 1);
24 }
25 }
This a simple example of usage -
1 var personCreateRequest = new PersonCreateRequest
2 {
3 PersonId = Guid.NewGuid(),
4 Firstname = "Tom",
5 Lastname = "Travers",
6 Addresses = new List<Address>() { new Address { Street = "Main", IsActive = true, IsPrimary = false, }, new Address{Street = "Boylston", IsActive = true, IsPrimary = true} },
7 Phones = new List<Phone>() { new Phone { PhoneNumber = "124", IsActive = true, IsPrimary = false } }
8 };
9
10 var validator = new PersonCreateRequestValidator();
11 ValidationResult result = validator.Validate(personCreateRequest);
Validating against multiple parts of the request
If you have the slightly more complicated scenario where you need either an active and primary phone or an active and primary address, add the following predicate -
1 private bool AtLeastOneActiveAndPrimaryBetweenPhoneAndAddress(PersonCreateRequest p)
2 {
3 bool addressActiveAndPrimary = HaveAnActiveAndPrimary(p.Addresses);
4 bool phoneActiveAndPrimary = HaveAnActiveAndPrimary(p.Phones);
5
6 bool result = addressActiveAndPrimary || phoneActiveAndPrimary;
7
8 return result;
9 }
And call it like so -
1 public PersonCreateRequestValidator()
2 {
3 RuleFor(r => r).Must(AtLeastOneActiveAndPrimaryBetweenPhoneAndAddress).WithName("request");
Of course you can’t now call Must(HaveAnActiveAndPrimary)
on just the phones and addresses.
Models
1 public class PersonCreateRequest
2 {
3 public Guid RequestId { get; set; }
4 public Guid PersonId { get; set; }
5 public string Firstname { get; set; }
6 public string Lastname { get; set; }
7 public List<Address> Addresses { get; set; }
8 public List<Phone> Phones { get; set; }
9 }
1 public class Address : IActivePrimary
2 {
3 public string Street { get; set; }
4 public string City { get; set; }
5 public string State { get; set; }
6 public bool IsPrimary { get; set; }
7 public bool IsActive { get; set; }
8 }
1 public class Phone : IActivePrimary
2 {
3 public string PhoneNumber { get; set; }
4 public bool IsPrimary { get; set; }
5 public bool IsActive { get; set; }
6 }
1 public interface IActivePrimary
2 {
3 bool IsPrimary { get; set; }
4 bool IsActive { get; set; }
5 }
Full source code is available here.