Here's a write up on a validation framework that I've been working on. It works great for me validating user input and is also useful in Web Services and Web API projects. All the Source Code is available, and there is a convenient NuGet Package.

What is LightVx?

LightVx is a validation framework for .Net. With a fluent API and many built in validators, it helps save time, reduce rework and provide a straight forward approach to validation.

Where can I find it?

Why LightVx?

Validation is one of those essential parts in building secure applications. Simple !null checks and the odd TryParse get you part of the way. Then there's business domain logic around specially crafted identifiers with fixed formats or checksums. While we can build helpers and methods and get some level of re-use, the variety of calls ends up being less cohesive and requires the developer has a fair knowledge of the codebase. The idea behind LightVx is to bring all of the validation together using a consistent implementation. This has the added bonus of making our validators flexible and allow them to be used in a variety of combinations.

Current Supported Frameworks

  • .Net Standard 2.0
  • .Net Framework 4.0
  • .Net Framework 4.5

Additional frameworks will be added over time, but if you need support for one sooner please let me know in the comments.

Let's start with a simple example

Let's say you have a method: public void Save(string firstName, string lastName, int age)
and you want to make sure the following are true:

  • firstName - contains a value greater than 2 characters and less than 50
  • lastName - same as above
  • age - only allow an age of greater than 18 and less than or equal to 100

Usage 1: Using the Fluent API

       private void Save(string firstName, string lastName, int age)
        {
            List<string> allErrors = new List<string>();
            Validator.Eval(firstName, "First Name")
                .HasLength(2, 50)
                .Fail((errors, validators) =>
                {
                    allErrors.AddRange(errors);
                });
            Validator.Eval(lastName, "Last Name")
                .HasLength(2, 50)
                .Fail((errors, validators) =>
                {
                    allErrors.AddRange(errors);
                });
            Validator.Eval(age, "Age")
                .Min(18)
                .Max(100)
                .Fail((errors, validators) =>
                {
                    allErrors.AddRange(errors);
                });
            if (allErrors.Count > 0)
            {
                //...validation handling process here
            }
        }

Usage 2: Storing the result and accessing it later

By storing the result of the validation chain, you can then access it as shown below:

            var result = Validator.Eval(age, "Age")
                .Required()
                .IsNumeric()
                .Min(18)
                .Max(100);
            ...
            if (result.IsValid == false)
            {
                string fieldName = result.FieldName;
                List<string> errorMessages = result.ErrorMessages;
                List<IValidator> validators = result.Validators;
                //... handle invalid data
            }

Usage 3: Using a validator directly
Validators can be called directly.

            var nameValidator = new LengthValidator(2,50);
            if (!nameValidator.Validate(firstName, "First Name", out errorMessage))
            {
                //... handle invalid data here
            }

The Success and Fail methods

When calling the Fluent API you can have a Success or Fail method called as part of the chain. Similar to how we might build a promise in JavaScript, this provides an inline way to handle error messages.

Example Usage: Calling Success and Fail methods

 var input = "https://github.com/TjWheeler/LightVx"; //user input to be validated
            var result = Validator.Eval(input, "Source Url")
                .Required()
                .IsUrl()
                .Success((() =>
                {
                    Console.WriteLine("Validation succeeded");
                }))
                .Fail((errors, validators) =>
                {
                    Console.WriteLine(string.Join(",", errors));
                    // Validation failed, put your failure logic here
                });
		if(result.IsValid == false)
		{
		    return;
		}

Note: You can't call return from the success or fail methods to eject from the main method.

Fluent API methods

  • Required() - must not be null or empty string
  • HasLength(int min, int? max)
  • IsAlphaNumeric()
  • IsAlphaText()
  • IsDecimal()
  • IsEmailAddress()
  • IsNumeric()
  • IsPhoneNumber()
  • IsSafeText()
  • IsUrl()
  • Min(int value)
  • Min(double value)
  • Min(decimal value)
  • Max(int/double/decimal value)
  • IsEmpty()
  • IsNotEmpty()
  • IsNull()
  • IsNotNull()
  • HasMinLength(int minLength)
  • HasMaxLength(int maxLength)

Built-in Validators

  • Aggregate - Combines other validators
  • AlphaNumeric - Alphabetical or Numbers
  • AlphaText - a to Z and spaces
  • Decimal - a decimal value
  • Email - email address
  • Url - Uri/Url
  • Empty - will match an empty string
  • HexColor - a valid hex color
  • IsNull - matches null
  • Length - matches string length. Supply min and max value.
  • MaxLength - matches a max string length
  • MinLength - matches a min string length
  • Max - validates a number is not greater than x. If input is an Array or ICollection then it will validate against number of items.
  • Min - validates a number is not less than x. If input is an Array or ICollection then it will validate against number of items.
  • NotEmpty - must not be empty string
  • Numeric - numbers only
  • PhoneAndLength - combine phone number validator and length validator
  • PhoneNumber - attempts to validate a phone number with optional braces
  • SafeText - very restrictive validator that allows a to Z, space, hyphen and apostrophe.
  • Url - validates against a valid url

Extending and creating your own validators

In the following example, we are inheriting from an AggregateValidator, this allows us to combine validators.

Creating a Post Code Validator by combining other validators.

//Step 1: Add the custom validator
public class PostCodeValidator : AggregatedValidator
    {
        public PostCodeValidator()
        {
            AddValidator(new LengthValidator(4, 4));
            AddValidator(new NumericValidator());
        }
    }

//Step 2: Add an extension method - this will integrate your validator with the Fluent API
public static class PostCodeValidatorExtension
    {
        public static ValidatorFluent IsPostCode(this ValidatorFluent fluentApi)
        {
            fluentApi.AddValidator(new PostCodeValidator());
            return fluentApi;
        }
    }

//Step 3: Call it to validate input

    public void ExampleCustomValidator()
    {
        string input = "...";
        var isValid = Validator.Eval(input, "MyFieldName")
            .Required()
            .IsPostCode()
            .Fail(((errors, validators) =>
            {
                Console.WriteLine("Example failure: " + string.Join(";", errors));
            })).IsValid;
    }

Creating your own validator

Create a class and inherit ValidatorBase. The only method you need to implement is Validate. There are some base methods that will make it easy to validate using Regular Expressions. Here's an example of one of the built in validators.

    /// <summary>
    ///     Validate Email Addresses
    /// </summary>
    public class EmailValidator : ValidatorBase
    {
        protected override string Expression => @"^([\&\w-\.]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([\w-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?)$";

        protected override void Validate()
        {
            if (_Input == null || (string) _Input == string.Empty)
            {
                Succeed();
                return;
            }

            if (SingleMatch((string) _Input))
                Succeed();
            else
                Fail("is not a valid email address.");
        }
    }

When using Regular expressions you can also use the HasMatch and MatchCount base methods.

Breaking down the framework

The IValidator Interface

At it's core, an Interface IValidator defines the blueprint for any validator.

When creating an IValidator you can inherit from ValidatorBase which will do most of the work for you with the exception of a single Abstract Validate(object input) method.

The Validation Helper

The Validation helper named Validator is used to provide access to the fluent API and some additional helper methods.

The Eval methods provide the fluent API. You must pass in the value to be validated, and optionally pass in a field name. When passing in a field name it will be used to construct the validation error messages.
For example:

var result = Validator.Eval(input, "My Field")
                .IsNotEmpty();

The IsNotValid and IsValid helper methods will allow you to call a single Validator. Note that the validator must have a default constructor.
For example:

            var input = "ABC";
            string errorMessage;
            if (Validator.IsNotValid<AlphaTextValidator>(input, "First Name", out errorMessage))
            {
                //...
            }

The Fluent API

When using the Validator.Eval method you can chain multiple calls together, this looks like:

This is the most convenient way to call the validators, and has an advantage over the helper methods in that it can accept parameters for the validator constructors.

Conclusion

The most convenient way to access the validators is through the Fluent API using Validator.Eval. You can easily add your own validators, and using Extension methods you can also add them to the Fluent API with an extension. The ValidatorBase class is a base implementation of IValidator and has some baked in Regular Expression support to help make building validators quick.

Hopefully this is helpful to others out there striving to build secure applications. If you have any suggestions for improvements, please leave a comment.