Come Undone - Argument Validation for Rocketeers
Join the DZone community and get the full member experience.
Join For FreeValidation is crucial part of development. From argument checks to validating workflows, pipelines, input/output boundaries we run checks to make sure that an application will behave correctly. Personally as a software engineer, I like to see the business processes before the implementation and try to express myself via code in that manner.
From aspect of validation this means that writing code straight forward makes it hard to understand what the poet wanted to say in complex scenarios.
Ex:
////// Create new user in the system /// public int CreateUser(User user){ // check for faulted data if(user.IsNull()) throw new ArgumentException(“user"); // check if the email is valid if(!Regex.IsMatch(user.Email, “^([0-9a-zA-Z]([+-.\w]*[0-9a-zA-Z])*@([0-9a-zA-Z][-\w]*[0-9a-zA-Z]\.)+[a-zA-Z]{2,9})$"))) throw new InvalidDataException("Email is not valid"); // check if the email have been used before if(UserRepository.HasRegisteredWithEmail(user.Email)) throw new InvalidDataException("A user has already registered with the provided email"); // check if the username is not empty and has more then 6 chars if(user.Username.IsNullOrEmpty() && user.Username.Length > 6) throw new InvalidDataException("Username must have at least 7 chars"); // check if the user with that username exists in database if(UserRepository.Existis(user.Username)) throw new InvalidDataException("Username have been taken"); // check user pwd if(user.Password.IsNullOrEmpty() && user.Password.Length > 6) throw new InvalidDataException("Password must have at least 7 chars"); // check is user pass matches with repeat version if(user.Password != user.RepeatPassword) throw new InvalidDataException("Please retype your password. Password does not match"); //... var id = UserRepository.CreateUser(user); return id; }
We are looking at a well documented hard-to-read code. What if we could leverage some patterns that can help us doing validation in a more readable manner?
Ladies and gents, this is where “Specification Pattern” comes to the rescue.
Specification pattern is a particular software design pattern, whereby business rules can be recombined by chaining the business rules together using boolean logic.
To simplify it, this means that we need a validation engine and rules and containers that the engine will know how to execute them against the input data.
Validation engine
A basics contract of the validation engine would consist methods like:
- Register validation rule
- Register validation container
- Execute registered rules against input
- Execute If – execute registered rules if condition is fulfilled
- OnFalse – action to be executed if the validated input is not valid
- Get rules by status - to provide information of successfully v.s unsuccessful rules
- Restore – restore the validation engine to original setup
- Resetup – remove registered rules and clears executed data
////// Validation engine contract /// public interface IValidationEngine{ /// /// Represent the exeution result /// ValidationResultExecutionResult { get; set; } /// /// Register a validation container before execution /// IValidationEngineRegister(IValidationRuleContainer container); /// /// Register a validation rule /// IValidationEngineRegister(IValidationRule validationRule); /// /// Exeute the registered validation rules and validation rule /// containers on the provided entity /// IValidationEngine Execute(T entity); /// /// Exeute the registered validation rules and validation rule /// containers on the provided entity /// IValidationEngine ExecuteIf(T entity, Func condition); /// /// Action to be executed if the result is false /// void OnFalse(Action > action); /// /// Return validation rule by execution status /// List > GetRulesByStatus(ValidationRuleExecutionStatus executionStatus); /// /// Restore the validation engine to initial setup state /// IValidationEngineRestore(); /// /// Remove registered rules and clears executed data /// IValidationEngineReSetup(); }
Specification
Specification would be the single unit that can be validated against input data in the validation engine. A basic specification contract must have:
- Validation expression that will be used for executing against the input data
- Result – hold the value of the execution against input data
- Execute method which will be called from the validation engine
////// Represents validation rule /// public interface IValidationRule{ /// /// Validation expression that must be fullfilled /// Expression > ValidationExpression { get; } /// /// Execution result /// bool Result { get; } ////// Execute the defined expression /// bool Execute(T entity); }
In order to achieve better grouping of specification rules we can use the specification containers.
- Add validation rule – Add validation rule as part of the container
- Get validation rules from container – returns all registered validation rules that will be executed against the input data
////// Contract defining containers for the rules /// public interface IValidationRuleContainer { /// /// Register a rule /// void AddValidationRule(IValidationRulevalidationRule); /// /// Return all validation rules registered in the container /// List> GetValidationRules(); }
Wrapping this up
The engine
public class RuleEvaluator: IValidationEngine { /// /// Private constructor /// private RuleEvaluator() { ValidationRulesForExecutution = new List>(); } /// /// Represent all registered validation rules /// private List> ValidationRulesForExecutution { get; set; } /// /// Represent all registered validation rules /// private List> FalseReturnValidationRules { get; set; } /// /// Represent all registered validation rules /// private List> TrueReturnValidationRules { get; set; } /// /// Represent the exeution result /// public ValidationResultExecutionResult { get; set; } /// /// Restore the validation engine to initial setup state /// public IValidationEngineRestore() { this.TrueReturnValidationRules = new List >(); this.FalseReturnValidationRules = new List >(); return this; } /// /// Create new instance of the rule engine /// public IValidationEngine ReSetup() { this.TrueReturnValidationRules = new List >(); this.FalseReturnValidationRules = new List >(); this.ValidationRulesForExecutution = new List >(); return this; } #region Implementation of IValidationEngine /// /// Register a validation container before execution /// public IValidationEngineRegister(IValidationRuleContainer container) { if (container != null) { var _validationRules = container.GetValidationRules(); if (_validationRules != null) { ValidationRulesForExecutution.AddRange(_validationRules); } } return this; } /// /// Register a validation rule /// public IValidationEngineRegister(IValidationRule validationRule) { if (validationRule != null) { ValidationRulesForExecutution.Add(validationRule); } return this; } /// /// Exeute the registered validation rules and validation rule /// containers on the provided entity public IValidationEngine Execute(T entity) { FalseReturnValidationRules = new List >(); TrueReturnValidationRules = new List >(); // if no rules are defined return false var _isValid = ValidationRulesForExecutution.Count > 0; try { foreach (var _validationRule in ValidationRulesForExecutution) { bool _result; try { _result = _validationRule.Execute(entity); } catch (Exception) { _result = false; } _isValid = _isValid && _result; if (!_isValid) { FalseReturnValidationRules.Add(_validationRule); } else { TrueReturnValidationRules.Add(_validationRule); } } } catch (Exception) { _isValid = false; } ExecutionResult = new ValidationResult (_isValid, FalseReturnValidationRules); return this; } public IValidationEngine ExecuteIf(T entity, Func condition) { var _result = condition.Invoke(entity); if (_result) { Execute(entity); } else { ExecutionResult = new ValidationResult (true, new List >()); } return this; } /// /// Action to be executed if the result is false /// public void OnFalse(Action > action) { if (!ExecutionResult.Result) { action(ExecutionResult); } } /// /// Return validation rule by execution status /// public List > GetRulesByStatus(ValidationRuleExecutionStatus executionStatus) { switch (executionStatus) { case ValidationRuleExecutionStatus.True: return TrueReturnValidationRules; case ValidationRuleExecutionStatus.False: return FalseReturnValidationRules; default: return new List >(); } } #endregion /// /// Create new instance of the rule engine /// public static IValidationEngine New() { return new RuleEvaluator (); } }
Example rule will validate user email address
////// Validate user's email /// public class EmailMatchingSpecification : BaseSpecification{ #region Implementation of IValidationRule /// /// Validation expression that must be fulfilled /// public override Expression > ValidationExpression { get { return x => x.Email.IsMatch(RegexHelper.IsValidEmail, RegexOptions.Singleline | RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture | RegexOptions.CultureInvariant); } } #endregion }
Usage
The refactored method that we’ve used as an example will result in:
////// Create new user in the system ///public int CreateUser(User user){ var engine = RuleEvaluator .New(); engine.Register(new IsValidEmailSpecification()) .Register(new HasAlreadyRegisteredWithEmailSpecification()) .Register(new IsValidUsernameSpecification()) .Register(new IsUsernameInUseSpecification()) .Register(new IsValidPassword()) .Register(new ArePasswordsMatchingSpecification()) .Execute(user); if (!engine.ExecutionResult.Result) { // check the results and create proper exception } //... var id = UserRepository.CreateUser(user); return id; }
Or if we use a specification container:
////// Create new user in the system /// public int CreateUser(User user){ var engine = RuleEvaluator.New(); engine.Register(new NewUserValidationContainer()) .Execute(user); if (!engine.ExecutionResult.Result) { // check the results and create proper exception } //... var id = UserRepository.CreateUser(user); return id; }
Conclusion
Using specification pattern can help us to create reusable validation and more readable code. It is not a golden hammer and can be an overhead for CRUD based applications. If you are starting your next enterprise heavy project, it would be good to think of maintenance, code readability and reusability.
*GitHub
If you want to try and play with the validation engine feel free to fork it on GitHub.
Opinions expressed by DZone contributors are their own.
Trending
-
Introduction To Git
-
Replacing Apache Hive, Elasticsearch, and PostgreSQL With Apache Doris
-
How To Integrate Microsoft Team With Cypress Cloud
-
Transactional Outbox Patterns Step by Step With Spring and Kotlin
Comments