A while ago I wrote about a simple .NET Validation Framework. A lot has changed since then, and more elaborate rules had to be implemented in various projects.
So let me introduce you to the next version of .NET Validation and Business Rules (Micro) Application Block.
You can download latest release of Lokad Shared Libraries to get started right away.
Key features:
- Simple to get started with
- Rules for the domain objects
- Rule Reusability
- Predefined Validation Rules
- Behavior Extensibility
- Proper object nesting
- Designed for DDD and UI level validation
- Rules simplify your unit tests
- Multiple reporting levels
- Production-proof and active development
- Small codebase
- Open Source
Let’s walk over them.
Simple to get stated with
Simplest scenario is when you add a rule to your everyday argument validation. So instead of:
// just check for null
Enforce.Argument(() => username);
we could have more thorough checks:
// Check for null and add an email and length constraints
// exception ArgumentException will be thrown if something goes wrong
Enforce.Argument(() => username,
StringIs.Limited(3, 64), StringIs.ValidEmail);
The rules above are self-descriptive, are not they? You do not need to check the contents of the StringRules.Limit to figure out what it does.
Note, how we are using lambdas to reference our arguments. This allows us to grab argument name and value at the same time. See IL-reflection post, if interested in more details.
Additionally, all the rules are strongly typed. So the compiler and ReSharper will not let you to use rules improperly.
Rules for the domain objects
It is really simple to define custom business rules or compose rules together.
Let’s talk code:
// Rule definition in MyRules class
public static void ValidIdentity(Identity identity, IScope scope)
{
scope.Validate(() => identity.Username,
StringIs.Limited(3, 256), StringIs.ValidEmail);
scope.Validate(() => identity.Password,
StringIs.Limited(3, 64));
}
And we can reference this rule just like as any other:
// simple rule usage
Enforce.Argument(() => identity, MyRules.ValidIdentity);
Again, note how we use C# lambdas to reference arguments and properties. This let’s us include all information about failures, should they happen.
Rule Reusability
You just need to define rule once and then you can reuse it in multiple places.
Having rules in one place of your .NET project simplifies maintenance a lot. It also reduces the number of bugs or invalid requests passing through. So your application will be more reliable and secure.
Additionally, it is rather easy to define descriptive messages for the failures in one place (they can even be localized). And then for every failure you’ll have them.
Predefined Validation Rules
There already are some certain rules coded in:
- Common rules:
- Is.NotDefault – ValueType being validated must be initialized
- Is.NotEqual{T}(T item) – Value can not be equal to {0}
- Is.Within{T}(T minValue, T maxValue) – Value should be within the {0} and {1} (inclusive)
- Is.Between{T}(T lowerBound, T upperBound) – Same as above, but the item can not be equal to the boundaries.
- Is.GreaterThan{T}(T item) – Value must be greater than {0}
- Is.LessThan{T}(T item) – Value must be less than {0}
- Is.AtLeast{T}(T item) – Value must be greater than {0} or equal
- Is.AtMost{T}(T item) – Value must be less than {0} or equal
- String rules:
- StringIs.ValidEmail – String should represent a valid email address
- StringIs.Limited(int minLength, int maxLength) – String should have length between {0} and {1}
- StringIs.Without(params char{} chars) – String can’t contain any of {0}
- DateTime rules:
- DateIs.ValidSqlDate – DateTime should be greater than 1753-01-01
- Misc rules:
- DoubleIs.Valid – double is not a NaN or infinity
If that’s not enough for you, there’s an expression based rule that you can use to build any rule quickly and have detailed description auto-generated for it:
- Is.True{T}(Expression{Func{T,bool}} expression) – Expression should be true.
Here’s how you can use it:
var compiledRule =
Is.True<Visitor>(visitor => visitor.Programs.Exists(p => p.Active));
Enforce.That(() => myVisitor, compiledRule);
The exception will look like:
myVisitor: Expression should be true:
visitor => visitor.Programs.Exists(p => p.Active).
There will be more rules added to the Shared Libraries as the need arises.
BTW, can you think of any?
Additionally, there are samples of other rules specific to the Lokad SDK. They are open source and you could use them as samples. See the Lokad.Api.Core.ApiRules
Behavior Extensibility
It is nice to throw a consistent error message as soon as the problem is discovered. But sometimes it would be nice to change the behavior of your rules depending on the circumstances or deployment scenario.
For example:
- While performing server-side validation – throw exception as soon as the problem encountered
- While performing client-side validation for the development builds – throw exception if there are problems only after capturing all possible problems
- Write all “Warning”-level failures to the log and throw exception on any error encountered for the production automation service
And, obviously, it would be nice to use all the existing rule libraries without any modification in any of these scenarios.
In order to get this flexibility you just need to write your argument validation code like this:
Message[] void GetMessages(Identity identity, Guid[] messageIDs)
{
using (var scope = _scopes.Get("GetMessages"))
{
scope.Validate(identity, "identity", MyRules.ValidIdentity);
// we are applying rules to the array here
scope.ValidateMany(messageIDs, "messageIDs",
Limits.GetMessages, Is.NotDefault);
}
// remaining code goes here
}
Where _scopes is a variable of INamedProvider{IScope}. It could be implemented and instantiated by you, by the Inversion of Control Container or you can simply pass in one of the existing scope providers:
- Scope.ForValidation – runs as many rules as possible before throwing RuleException with the full description of the problems being encountered
- Scope.ForEnforce – throws RuleException as soon as the first problem is encountered
- Scope.ForEnforceArgument – same as before, but throws the ArgumentException (that’s the one being used by Enforce.Argument)
Or you can easily create your own IScope implementation for custom rule behaviors. The interface requires just 3 methods to be implemented.
For example, custom scope wiring allows to capture rule messages like this:
var messages = Scope.GetMessages(() => order, DomainRules.ValidOrder);
This allows to process business rules against some object and capture the output properly.
You can check out the post on message capturing for additional information.
Proper object nesting
If we try passing identity object with the invalid name to the method above then the error message will contain the full path to this name:
GetMessages.identity.Username – String must have length between 4 and 64
Although this might not be apparent from the outside, but the framework and the scopes are responsible for keeping track of the validation path, current error level and the behavior rules.
Designed for DDD and UI level validation
For more information, check out the article with the reference implementation.
Rules simplify your unit tests
Take a look at these NUnit statements in C#:
Assert.IsTrue(visitor.Programs.Exists(p => p.Active),
"'visitor' should have at least one active program");
Assert.IsTrue(visitor.Programs.Length > 1,
"'visitor' should have more than one program");
It looks much better this way, does not it:
RuleAssert.That(() => visitor,
v => v.Programs.Exists(p => p.Active),
v => v.Programs.Length > 1);
With this approach, we don’t have to bother about writing description, mentioning the exact name the name of the variable and keeping these in sync. The RuleException, being thrown, will contain all the information that we need:
visitor: Expression should be true: v => v.Programs.Exists(p => p.Active).
Multiple reporting levels
Rules support Warning and Error levels. Custom behavior could be bound to any of these by implementing your own scopes.
Production-proof and active development
This concept of rules has been tested and polished by multiple xLim implementations (including long-running distributed business workflows that include checking and sending requests to 3rd party services that had horrible error reporting).
These rules are being currently used by the Lokad SDK for .NET. They are developed and maintained as a part of it.
Check out ApiRules file for yourself.
Small codebase
The codebase for this entire rule and validation framework is so small that it does not deserve a separate project:
- Interface part:
- RuleException
- RuleLevel Enum
- IScope with 3 members and INamedProvider with 1 method
- Rule delegate
- Implementation:
- Scopes: TrackScope, EnforceScope, ValidationScope, EnforceArgumentScope, MessageScope
- Common rules: Is, StringIs, DateIs, DoubleIs
- Helper static classes and extension shortcuts: Scope, RuleExtensions, IScopeExtensions
I consider small codebase to be the advantage.
Open Source
BSD License. Approved by OSI.
What to do next?
- Grab the latest version of Lokad.Shared.dll from Lokad-Stack.zip, add System.Rules namespace reference and start playing
- Check the Lokad Shared Libraries for the:
- Latest versions
- Documentation links
- Access to the sources
- Feel free to ask questions, tell about your rule ideas and just send any feedback
So what do you think? Please, do not hesitate to drop a comment.