Bespoke Software Solutions in C#.Net, ASP.Net MVC, JQuery, MEF and more
Posts tagged validation
NBouncer, my Context Aware Validation Framework
Nov 12th
For most people out there using the current Validation Frameworks (DataAnnotations, Nhibernate Validation) are more than enough. But for some, like me, I need mine to be “Context Aware”.
What do I mean with “Context Aware”? Well lets say, for instance, that you are phoning your bank for an insurance policy. The first thing they need is to obtain just enough info to get you in their database so that they can take the application further. Once they have you, then they forward the application where underwriting, risk calculation etc can take place. So the application goes through quite a few states. We could say it has a preliminary, draft, final etc… state at each given moment. The validation and business rules might be completely different depending on what state the agreement is in.
Three more requirements had to be satisfied before I started to plan a prototype.
1. Needs to be VERY simple to use
2. Needs to stay out of my POCO objects (no attributes!)
3. Validation rules are just a subset of business rules. I want to be able to use the validation framework for complex business rules and simple validation and treat them as one layer.
After a few days NBouncer was born. This is just a prototype and more feedback is needed to justify investing more time into. I know there are “Context Aware” rule frameworks out there, but I need mine to be VERY SIMPLE.
Enough rambling, lets see some code!
Lets say we have a Person object, an Agreement object and a Hobby object: (note how clean these are, this is how they will stay!)
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public IList<Hobby> Hobbies = new List<Hobby>();
}
public class Agreement
{
public AgreementState State { get; set; }
}
public class Hobby
{
public string Name { get; set; }
}
The first example is to send a Warning message if the execution action is “Validate”, but to send an Error if the user is trying to “Persist” the person. The only thing we need to do is create a PersonRules class and set an attribute specifying it’s a “RuleProvider” for the “Person” class…
[RuleProviderFor(typeof (Person))]
public class PersonRules : RuleProvider<Person>
{
public PersonRules(RuleContext context)
: base(context)
{
}
protected override void RegisterRules()
{
CreateNameValidationRules();
}
private void CreateNameValidationRules()
{
switch (Context.ExecutionAction)
{
case ExecutionAction.Validate:
RegisterRule(x => string.IsNullOrEmpty(x.Name),
CreateWarning(ErrorMessages.NameRequired, "Name"));
break;
case ExecutionAction.Persist:
RegisterRule(x => string.IsNullOrEmpty(x.Name),
CreateError(ErrorMessages.NameRequired, "Name"));
break;
}
}
}
A more complicated example is when we have custom “Actions” and the rules are dependent of other classes…
[RuleProviderFor(typeof (Person))]
public class PersonRules : RuleProvider<Person>
{
public PersonRules(RuleContext context)
: base(context)
{
}
protected override void RegisterRules()
{
var data = Context.ResolveData<Agreement>();
CreateNameValidationRules();
CreateAgeValidationRules(data);
CreateHobbyValidationRules(data);
}
private void CreateHobbyValidationRules(Agreement agreement)
{
if (Context.CustomAction == CustomActions.UnexpectedExit)
{
RegisterRule(x => x.Hobbies.Count == 0,
CreateError(ErrorMessages.HobbiesCannotBeEmpty, "Hobby"));
}
else
{
switch (Context.ExecutionAction)
{
case ExecutionAction.Persist:
if (agreement.State == AgreementState.Draft)
{
RegisterRule(x => x.Hobbies.Count == 0,
CreateWarning(ErrorMessages.HobbiesCannotBeEmpty, "Hobby"));
}
else if(agreement.State == AgreementState.Final)
{
RegisterRule(x => x.Hobbies.Count == 0,
CreateError(ErrorMessages.HobbiesCannotBeEmpty, "Hobby"));
}
break;
case ExecutionAction.Release:
RegisterRule(x => x.Hobbies.Count == 0,
CreateError(ErrorMessages.HobbiesCannotBeEmpty, "Hobby"));
break;
}
}
}
private void CreateAgeValidationRules(Agreement agreement)
{
switch (Context.ExecutionAction)
{
case ExecutionAction.Persist:
if (agreement.State == AgreementState.Draft)
{
RegisterRule(x => x.Age < 18,
CreateWarning(ErrorMessages.AgeCannotBeLessThan18, "Age"));
}
else if(agreement.State == AgreementState.Final)
{
RegisterRule(x => x.Age < 18,
CreateError(ErrorMessages.AgeCannotBeLessThan18, "Age"));
}
break;
case ExecuteAction.Validate:
RegisterRule(x => x.Age < 18,
CreateWarning(ErrorMessages.AgeCannotBeLessThan18, "Age"));
break;
}
}
}
The only thing left to do is to see how we validate…
public ActionResult EditPerson(Person person)
{
var agreement = new Agreement {State = AgreementState.Draft};
var ruleContext = new RuleContext(ExecutionAction.Persist);
ruleContext.RegisterData(agreement);
var rules = person.GetRuleProvider(ruleContext);
if(rules.Validate())
{
rules.ValidationResult.ForEach(x => ModelState.AddModelError(x.Message, x.TargetProperty);
return View(person);
}
Service.CreateAgreement(person, agreement);
return View(person);
}
As I said before, this is just a prototype and feedback would be appreciated!