Over a million developers have joined DZone.

Come Undone - Argument Validation for Rocketeers

Is iPaaS solving the right problems? Not knowing the fundamental difference between iPaaS and dPaaS could cost you down the road. Brought to you in partnership with Liaison Technologies.

Validation 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
    /// 
    ValidationResult ExecutionResult { get; set; }

    /// 
    /// Register a validation container before execution
    /// 
    IValidationEngine Register(IValidationRuleContainer container);

    /// 
    /// Register a validation rule
    /// 
    IValidationEngine Register(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
    /// 
    IValidationEngine Restore();

    /// 
    /// Remove registered rules and clears executed data
    /// 
    IValidationEngine ReSetup();
}

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(IValidationRule validationRule);

    /// 
    /// 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 ValidationResult ExecutionResult { get; set; }

    /// 
    /// Restore the validation engine to initial setup state
    /// 
    public IValidationEngine Restore()
    {
        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 IValidationEngine Register(IValidationRuleContainer container)
    {
        if (container != null)
        {
            var _validationRules = container.GetValidationRules();
            if (_validationRules != null)
            {
                ValidationRulesForExecutution.AddRange(_validationRules);
            }
        }

        return this;
    }

    /// 
    /// Register a validation rule
    ///
    public IValidationEngine Register(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.

Discover the unprecedented possibilities and challenges, created by today’s fast paced data climate and why your current integration solution is not enough, brought to you in partnership with Liaison Technologies.

Topics:

Opinions expressed by DZone contributors are their own.

The best of DZone straight to your inbox.

SEE AN EXAMPLE
Please provide a valid email address.

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.
Subscribe

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}