Angular Business Rules & Validation Strategies
Today, most web applications are not simple — especially when they include forms, APIs, and a database. Modern web applications now contain complex business rules and data validation requirements. Most business applications collect a fair amount of data. This means that the application or software must verify or validate that the information is correct before making expensive API calls. In addition to this, there may be other business rules that must be evaluated to ensure that the business logic is correct.
Therefore, what is your strategy to implement business rules and data validation? Most likely if you have five developers on your development team you will have five different ways to manage business rules and data validation. Over a period of time, the variance in the implementation of this type of code will create technical debt that will be difficult to overcome. If the application supports revenue generation then it is essential that the code is conventional, consistent, repeatable, and for sure testable.
Game Plan and Strategy
Hope is not a good strategy. If you hope the code will just work and the business rules will be fine without any thought or planning, good luck!
There are different sources in your application that may require validation. The validation and business rule strategy may include the Angular form validators. However, this is just the first layer of validation and business rule processing of the application. As the code progresses towards making an API call there are other opportunities for validation and business rule processing. Most likely there will be different types of validation using different mechanisms.
I like to imagine a set of safety nets that are layered depending on the layer of the application (UI/UX, UI Services, domain service layers, business logic, and data access/HTTP calls). This is where it gets interesting. In the lower layers of your application, you cannot use the reactive form Validators
that implements the ValidatorFn
interface. Therefore you might want to consider using a rule engine to create a more conventional way to manage validation and business rules.
Type of Validation and Rules
- form input (synchronous)
- asynchronous validation of form input that may include an API operation
- business logic with business rules
- data validation before an API call
Occasionally you might need to create a custom validator for form input. This custom validator might be synchronous or async. Either way, you will need to write some custom code. The validator must also be testable to verify the results. It should be a pure function — see an example here.
Motivation
Two core principles of great software design are Separation of Concerns (SoC) and Single Responsibility. Business rules and validation are an integral part of most business applications. There are rules and validations that must occur during the processing of business logic. Most applications will combine the business logic with rules and data validation — when this happens, testing and maintaining applications becomes more difficult.
A rule engine allows the application to have a good Separation of Concerns (SOR). A good rule engine allows you to:
- Quickly start using out-of-the-box rules that are already implemented.
- Create custom rules that are either simple or composite.
- Create rules that can be reused throughout the application. Code reuse eliminates copy/paste of common rules.
- Use a single ValidationContext to add rules, execute rules, and evaluate the rule results.
- Use a consistent pattern and mechanism to implement your business rules and data validation.
- Integrate rules with Reactive Form custom validators (async or sync).
The following diagram shows a strongly-typed Typescript rule engine that allows applications to implement simple or sophisticated business rules as well as data validation. It contains a set of common rules ready for use; as well as a framework and set of classes to create custom rules for Angular applications, components, services, and library projects.
Do not worry about the complexity of the diagram below. The diagram shows the internal design of the rule engine. The
ValidationContext
provides a simple API to add rules, execute rules, and to retrieve rule results.
The rule engine is using a simple design pattern called Composite. This was the first library I created using TypeScript in 2016. This was a turning point for me. I was currently using AngularJS. However, the new version of Angular 2 was just released and I was interested in its capabilities. One of my concerns was that the new version of angular was using TypeScript. Imagine that was a real concern that I had. I was a little apprehensive about adding a new programming language to my current list.
My thought was that if I could create a reusable library using TypeScript then I would learn Angular 2 along with TypeScript but with a focus on creating reusable libraries for my enterprise web applications.
International JavaScript Conference (New York 2021)
Well, five years later I am still focused on reusable libraries and Angular 2 or rather just Angular. Next month I will present at the International JavaScript Conference in New York (September 27–30, 2021) on the topic of Custom Angular Libraries.
Why use a rule engine?
Here are some reasons for using a rule engine. The biggest reason is below.
- Provides a consistent way to implement business and validation rules and provides a consistent mechanism to retrieve the results.
Most rule engines will come with predefined rules out of the box. Good rule engines will also have an API/Framework to allow you to create or compose custom rules using existing and other rules. This kind of capability is a game-changer.
The rule engine I use has a set of predefined rules ready to go.
- AreEqual
- AreNotEqual
- GuidIsValid
- IsFalse
- IsTrue
- IsNullOrUndefined
- IsNotNullOrUndefined
- Max
- Min
- Range
- StringIsRegExMatch
- StringIsNotNullEmptyRange
- StringIsNullEmptyRange
More motivation and reasons to use a rule engine:
- You can create a reusable library of custom rules and use them in one or more applications.
- Combine default with one or more custom rules to create a
CompositeRule
- a rule that contains other rules (ruleset). - Each rule has a
Priority
property to execute rule sets in a specified sequence. - Take advantage of Typescript classes to quickly create
simple
orcomposite
(nested) rules using the API that is part of the framework. - Use the
ValidationContext
to simply add, execute, and retrieve rule results. - Code faster using Fluent API style syntax — be more productive.
- Using the
CompositeRule
base class, you can create a rule that contains other rules of eithersimple
orcomposite
types. The rule execution algorithm manages the complexity - now you can create rules and reuse rules to match your business logic.
ValidationContext
The ValidationContext is the container object for rules. It allows the developer to add, execute and retrieve the results of the evaluated rules.
- Add rules by calling the
addRule()
function. - Execute rules by calling the
renderRules()
function. - Retrieve the results (a list of RuleResult items) using the
results
public property.
The following code snippet shows import statements and initialization of the ValidationContext
as a member in the class.
The following shows the entire ValidationContext
class with its implementation details. It is straightforward, you make the calls in the following sequence:
addRule(..)
: Add rules that you want to evaluate.renderRules()
: Renders all rules added to the ValidationContext.- Determine the state of the validation by using either:
hasRuleViolations()
orisValid()
. Each returns a boolean value indicating the status of the validation context. - Retrieve the
ValidationContext.results
which is a array ofRuleResult
items.
ValidationContext
The ValidationContext
is the entry point into using the rule engine. It is small but powerful.
Adding Rules to the ValidationContext
Using an initialized [ValidationContext] object, you can add rules using a Fluent API syntax. The following example uses existing rules.
A rule requires:
- Name: the name of the rule.
- Message: the text to display if the rule fails.
Here is an example of a business action (unit of work) for adding a contact
. It uses the ValidationContext
to add a set of specific rules to validate the action. If all of the rules evaluate without any errors, the API is called with the contact DTO object.
Focus on the
preValidateAction()
method. This is where the rules are implemented for adding a new contact to the application.
Custom Rules
Notice that the rules for adding a new Contact
contains an EmailAddressFormatIsValidRule
rule.
Custom rules inherit from either a
CompositeRule
or aSimpleRule
. All rules inherit from theRulePolicy
class and are executed and evaluated the same way. ARuleResult
is returned when a simple or composite rule is evaluated.
Rules with Rules (Composite)
A composite rule is a collection of simple and composite rules.
Notice that this composite rule contains another composite rule: StringIsRegExMatch
. Rules can be an Object-Graph of rules - a rule chain. Where each rule or group of rules (Composite) must evaluate without any errors for the rule or context to be valid. If any rule fails within the ValidationContext the ValidationContextState.State
is Failure
.
Executing Rules
When you have added one or more rules to an instance of a ValidationContext
, you are ready to execute the rules. The renderRules()
method will return the ValidationContext
- each of the rules is evaluated against their specified targets. You are now ready to evaluate the rule results.
this.validationContext.renderRules()
Evaluation Rule Results
After the rules are executed, you can examine the rule results. Each rendered rule will have a result — either valid or not valid, based on the rule, criteria, and target value(s).
Many times it is useful to filter or extract the failed rules from the ValidationContext
. The following code snippet shows how you would extract failed rules that are marked as displayable (i.e., e.rulePolicy.isDisplayable
) into a list of ServiceMessage
items.
RulePolicy
The RulePolicy
is the base class for all rule types. The angular-rules-engine contains (2) types of rule implementations:
- Simple
- Composite.
These rule types form the basis of all rules in the rule engine. The rule engine uses the Composite Design Pattern — click here for more information about this pattern. All rules have the following properties of information.
- isValid: Use to indicate the status of the rule after evaluation.
- message: Use to provide a message for failed rules.
- name: Use to create a name that identifies the specified rule.
- priority: Use to assign a numeric value to the rule. Rules are sorted by priority and executed in the same sort sequence.
- result: The output of an executed rule. It contains the result and
RulePolicy
information. - isDisplayable: Use to indicate if the rule result is displayable to the caller. Default value is
false
. You must explicitly provide a [true] value for this when initializing a new rule. - renderType: Currently the only option is
RenderType.EvaluateAllRules
. - severity: Use to indicate the severity (Exception, Warning, or Information) if the rule evaluation is not valid.
- source: Use to indicate the source or location of the rule.
The following RulePolicy
class is the base class for all rules. Each of the default rules also extends either the SimpleRule
or the CompositeRule
. And each of these classes extends from the RulePolicy
class. The allows all rules to have common behavior and execution strategies.
IRuleComponent
This interface is just infrastructure for the rule engine. It provides the contract that all rules will contain an execute()
method - this provides a consistent mechanism to begin the process of all rules that implement this interface.
export interface IRuleComponent {
execute(): RuleResult;
}
RuleResult
The output of an executed rule is a RuleResult
object that contains the rule (rulePolicy
), an indicator for the rule's state (isValid
), and a message to be used if the rule has failed.
Simple Rules
The main difference between SimpleRule
and a CompositeRule
is how they are rendered during their execution. A simple rule has a single evaluation with a single result.
The following code is the IsTrue
rule. This rule evaluates the target and creates a new RuleResult
in the render()
method. Basically, the result is based on the evaluation of the target value. This render()
method returns a single result. This is much different from a composite rule discussed later that has to return a RuleResult
for each rule in a list of rules.
Composite Rules
A composite rule is a rule that contains a list of rules to be evaluated. A rule in this list can be a rule that extends from either SimpleRule
or CompositeRule
. This allows for a more complex implementation of rules - it is a very powerful pattern. You can have a rule that contains a list of rules, where one of those rules may be a CompositeRule
, where one of those rules in the composite rule is a composite side-by-side with other simple and complex rules.
You are creating a rule tree where all rules will have to evaluate to be valid for the container rule to be valid. This pattern allows a developer to create new custom rules and then use those rules with the default rules to orchestrate a rule implementation against a target object or value.
The CompositeRule
extends from RulePolicy
which has the responsibility of calling render()
on each rule. In the case of a composite rule, this method will iterate through the list of rules and call the execute()
method of each rule. Then the results are processed to determine if any of the rules failed.
The following shows an implementation of a composite rule. Basically, this rule is using (2) default rules, both of which are also composite rules. All rules within each composite must evaluate to true for this rule to be valid.
The Range
rule used in the composite rule above uses (2) simple rules to form the composite. Each rule has to evaluate to true for the entire rule to be valid.
Conclusion
There are lots of details in the implementation of a rule engine. However, remember that you only need to initialize a ValidationContext
, add rules, and then call the renderRules()
to evaluate the ruleset and provide a list of RuleResult
items. It is that simple. Happy rule rendering.
For more information on how this cross-cutting concern library is used within a CLEAN Architecture approach with SOLID principles — please visit www.AngularArchitecture.com