Rethinking your business validation rules
Last night Tom Hollander and co. announced the release of the December CTP of Enterprise Libraries 3.0 (on CodePlex no less!). I've been watching and waiting for this (even bugging Tom to get a "sneak peek") but it's reality now and something you can play with. After all, opening Christmas presents isn't the only thing geeks do over the holidays. Don't use this for production apps, but if you've got something going into production in the next 6 months, its something to consider.
One of the main features they wanted to deliver (based on user feedback) was a validation engine. Something that would allow you to validate business entities with little effort. Last night I gave the validators a whirl to see how they worked. It's pretty slick and ends up being a nice cleanup of your domain from clutter (which is always a good thing).
Let's say I have an Account class and the name of the account has some rules around it. Names are required and can only be between 5 and 8 characters. This is a simple example, but one you should get the idea from.
First I'll write a set of quick tests for my Account class. Four tests, one creates a valid Account, two test the length bounds, and one creates an Account with a null name.
6 [TestClass]
7 public class AccountTests
8 {
9 private Account _account;
10
11 [TestInitialize]
12 public void SetUp()
13 {
14 _account = new Account();
15 }
16
17 [TestMethod]
18 public void CanCreateValidAccountWithName()
19 {
20 _account.AccountName = "C123456";
21 Assert.AreEqual(true, _account.IsValid);
22 }
23
24 [TestMethod]
25 public void AccountNameMustCannotBeLessThanFiveCharacters()
26 {
27 _account.AccountName = "A";
28 Assert.AreEqual(false, _account.IsValid);
29 }
30
31 [TestMethod]
32 public void AccountNameCannotExceedEightCharacters()
33 {
34 _account.AccountName = "123456789";
35 Assert.AreEqual(false, _account.IsValid);
36 }
37
38 [TestMethod]
39 public void AccountNameIsRequired()
40 {
41 Account account = new Account();
42 account.AccountName = null;
43 Assert.AreEqual(false, account.IsValid);
44 }
45 }
Now we'll create the Account class in a more "traditional" sense, checking for business rules validation during the setter and setting a flag called IsValid along the way.
3 public class Account
4 {
5 private string _accountName;
6 private bool _isValid;
7
8 public string AccountName
9 {
10 set
11 {
12 if(string.IsNullOrEmpty(value))
13 _isValid = false;
14 else if(value.Length < 5)
15 _isValid = false;
16 else if(value.Length > 8)
17 _isValid = false;
18 else
19 {
20 _accountName = value;
21 _isValid = true;
22 }
23 }
24 get { return _accountName; }
25 }
26
27 public bool IsValid
28 {
29 get
30 {
31 return _isValid;
32 }
33 }
34 }
Great, our tests pass and all is good in the world. However our setter looks kind of ugly and could be improved. Maybe we could create some private methods called from our setter, or you could throw a BusinessRulesException but it's still ugly no matter how you refactor this.
Enter the new hotness of the Validation Application Block of Enterprise Library 3.0. Rather than writing all those "if" statements and conditional checking, we can specify our validation through attributes decorated on the property. And our IsValid property can be changed to use the new ValidationFactory classes:
6 public class Account
7 {
8 private string _accountName;
9
10 [NullValidator]
11 [StringLengthValidator(5, 8)]
12 public string AccountName
13 {
14 set { _accountName = value; }
15 get { return _accountName; }
16 }
17
18 public bool IsValid
19 {
20 get
21 {
22 IValidator<Account> validator = ValidationFactory.CreateValidator<Account>();
23 ValidationResults results = validator.Validate(this);
24 return results.IsValid;
25 }
26 }
27 }
Makes for reading your domain code much easier and it's a breeze to write. Also (at least IMHO) it makes for reading the intent of the business rules much easier. You can also create your own validators and there are a set of built-in ones in the December CTP drop (String Length, String Range, Date Range, Valid Number, etc. more coming later) so there's much more to it than this simple example but I find it all very slick and easy to use.