DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports Events Over 2 million developers have joined DZone. Join Today! Thanks for visiting DZone today,
Edit Profile Manage Email Subscriptions Moderation Admin Console How to Post to DZone Article Submission Guidelines
View Profile
Sign Out
Refcards
Trend Reports
Events
Zones
Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones AWS Cloud
by AWS Developer Relations
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones
AWS Cloud
by AWS Developer Relations
Building Scalable Real-Time Apps with AstraDB and Vaadin
Register Now

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

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

Come Undone - Argument Validation for Rocketeers

Marjan Nikolovski user avatar by
Marjan Nikolovski
·
Jun. 13, 14 · Interview
Like (0)
Save
Tweet
Share
1.91K Views

Join the DZone community and get the full member experience.

Join For Free

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.

Engine

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

Partner Resources

X

ABOUT US

  • About DZone
  • Send feedback
  • Careers
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 600 Park Offices Drive
  • Suite 300
  • Durham, NC 27709
  • support@dzone.com

Let's be friends: