Skip to content

Latest commit

 

History

History

CoreEx.Validation

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

CoreEx

The CoreEx.Validation namespace provides an alternative data validation capability. It provides a familiar fluent-style method-chaining experience to other frameworks, taking a slightly different approach to validation configuration, and arguably supports more complex validations.


Composition

At its core a Validator can contain one or more Rules (provides a specific value validation), which can be further conditionally controlled by zero or more Clauses (provides a means to check whether a validation should occur).


Rules

All rules must inherit from PropertyRuleBase which enables the following key capabilities:

Capability Description
Name Gets the underlying property/value name.
Text Gets/sets the friendly text name used in validation messages.
WithMessage Sets (overrides) the error message for the rule.

The following represent access to clauses/conditions (support zero or more) to determine whether the Rule should be invoked:

Method Description
DependsOn Adds a DependsOnClause that ensures that another specified property of the entity must have a non-default value and not have a corresponding validation error.
When Adds a WhenClause where the condition must be true.
WhenOperation Adds a WhenClause that ensures that the ExecutionContext.Current.OperationType is equal to the specified value.
WhenNotOperation Adds a WhenClause that ensures that the ExecutionContext.Current.OperationType is not equal to the specified value.
WhenValue Adds a WhenClause where the condition for the corresponding value must be true.
WhenHasValue Adds a WhenClause where the corresponding value must not be default.

The following represent the available rules:

Rule Description
BetweenRule Provides a comparision validation between a specified from and to value.
CollectionRule Provides collection (IEnumerable) validation including MinCount, MaxCount, per item validation CollectionRuleItem and duplicate checking.
CommonRule Provides for integrating a common validation against a specified property.
ComparePropertyRule Provides a comparision validation against another property within the same entity; also confirms other property has no errors prior to comparison.
CompareValueRule Provides a comparision validation against a specified value.
CompareValuesRule Provides a comparision validation against one or more specified values.
CustomRule Provides a custom validation against a specified property.
DecimalRule Represents a numeric rule that validates DecimalPlaces (fractional-part length) and MaxDigits (being the sum of the integer-part and fractional-part lengths).
DictionaryRule Provides dictionary (IDictionary) validation including MinCount, MaxCount and per item validation DictionaryRuleItem.
DuplicateRule Provides validation where the rule predicate must return false to not be considered a duplicate.
EmailRule Provides e-mail validation.
EntityRule Provides entity validation.
EnumRule Provides Enum validation to ensure that the value has been defined.
EnumValueRule Provides string validation against an Enum value.
ExistsRule Provides validation where the rule predicate must return true or a value to verify it exists.
ImmutableRule Provides validation where the rule predicate must return true to be considered valid (has not been modified).
InteropRule Provides interoperability integration with other validation frameworks.
MandatoryRule Provides mandatory validation; determined as mandatory when it contains its default value.
MustRule Provides validation where the rule predicate must return true to be considered valid.
NoneRule Provides a rule to ensure value is its default value (opposite of MandatoryRule).
NotNullRule Provides validation where the value must not be null.
NumericRule Represents a numeric rule to validate precision, scale and whether negatives are allowed.
NullRule Provides validation where the value must be null.
OverrideRule Provides the ability to override the property value.
ReferenceDataCodeRule Provides validation for a ReferenceDataBase.Code; validates that it exists and that the corresponding ReferenceDataBase.IsValid.
ReferenceDataRule Provides validation for a ReferenceDataBase; validates that the ReferenceDataBase.IsValid.
ReferenceDataSidListRule Provides validation for a ReferenceDataSidListBase including MinCount, MaxCount, per item ReferenceDataBase.IsValid and whether to AllowDuplicates.
StringRule Provides string validation including MinLength, MaxLength and Regex.
WildcardRule Provides string Wildcard validation.

Clauses

The following represent the available clauses:

Clause Description
DependsOnClause Represents a depends on test clause; in that another specified property of the entity must have a non-default value to continue.
WhenClause Represents a when test clause; in that the condition must be true to continue.

Extension Methods

The rules are generally not instantiated directly, but accessed via pre-defined extension methods to provide a more simplified, natural, experience using fluent-style (method-chaining) approach to development.

The following represent the available extension methods:

Extension method Description Underlying rule
AreValid() Adds a reference data list validation. ReferenceDataSidListRule
Between() Adds a between comparision validation. BetweenRule
Collection() Adds a collection validation. CollectionRule
CompareProperty() Adds a property comparison validation. ComparePropertyRule
CompareValue() Adds a value comparison validation. CompareValueRule
CompareValues() Adds a values comparison validation. CompareValuesRule
Currency() Adds a currency validation for a decimal using a NumberFormatInfo. DecimalRule
Custom() Adds a custom validation. CustomRule
Default() Adds a property value override where the current value is the default for the Type. OverrideRule
Dictionary() Adds a dictionary validation. DictionaryRule
Duplicate() Adds a duplicate validation. DuplicateRule
Empty() Adds a none validation. NoneRule
Email() Adds an e-mail validation. EmailRule
EmailAddress() Adds an e-mail validation. EmailRule
Entity() Adds an entity validation. EntityValidationRule
Entity().With<TValidator>() Adds an entity validation. EntityValidationRule
EntityCollection() Adds an entity collection validation. EntityCollectionValidationRule
Enum() Adds an enum validation for an Enum Type. EnumRule
Enum().As<TEnum>() Adds an enum validation for a string Type. EnumValueRule
Equal() Adds an equal value comparison validation. CompareValueRule
ExclusiveBetween() Adds an exclusive between comparision validation. BetweenRule
Exists() Adds an exists validation. ExistsRule
GreaterThan() Adds a greater than value comparison validation. CompareValueRule
GreaterThanOrEqualTo() Adds a greater than or equal to value comparison validation. CompareValueRule
Immutable() Adds an immutable validation. ImmutableRule
InclusiveBetween() Adds an inclusive between comparision validation. BetweenRule
IsInEnum() Adds an enum validation for an Enum Type. EnumRule
IsValid() Adds a reference data validation. ReferenceDataRule
LessThan() Adds a less than value comparison validation. CompareValueRule
LessThanOrEqualTo() Adds a less than or equal to value comparison validation. CompareValueRule
Length() Adds a string exact length validation. StringRule
Mandatory() Adds a mandatory validation. MandatoryRule
Matches() Adds a Regex validation. StringRule
MaximumLength() Adds a string maximum length validation. StringRule
MaximumCount() Adds an ICollection maximum count validation. CollectionRule
MinimumLength() Adds a string minimum length validation. StringRule
MinimumCount() Adds an ICollection minimum count validation. CollectionRule
Must() Adds a must validation. MustRule
NotEmpty() Adds a mandatory validation. MandatoryRule
NotEqual() Adds a not equal value comparison validation. CompareValueRule
NotNull() Adds a not null validation. NotNullRule
None() Adds a none validation. NoneRule
Numeric() Adds a numeric validation. NumericRule or DecimalRule
Null() Adds a null validation. NullRule
Override Adds a property value override. OverrideRule
RefData().As<TRef>() Adds a reference data validation for a string Type. ReferenceDataCodeRule
RefDataCode Adds a reference data code validation. ReferenceDataCodeRule
String() Adds a string validation. StringRule
Wildcard() Adds a string wildcard validation. WildcardRule

Additional extension methods included are as follows:

Extension method Description
Text() Updates the rule friendly name text used in validation messages.
Common() Provides for integrating a common validation against a specified property.
Validate() Enables (sets up) validation for a value.

Error messages

All error messages are managed as an embedded resources accessible via the ValidatorStrings class; as follows:

Property Format string
AllowNegativesFormat {0} must not be negative.
BetweenInclusiveFormat {0} must be between {2} and {3}.
BetweenExclusiveFormat {0} must be between {2} and {3} (exclusive).
CollectionNullItemFormat {0} contains one or more items that are not specified.
CompareEqualFormat {0} must be equal to {2}.
CompareGreaterThanEqualFormat {0} must be greater than or equal to {2}.
CompareGreaterThanFormat {0} must be greater than {2}.
CompareLessThanEqualFormat {0} must be less than or equal to {2}.
CompareLessThanFormat {0} must be less than {2}.
CompareNotEqualFormat {0} must not be equal to {2}.
DecimalPlacesFormat {0} exceeds the maximum specified number of decimal places ({2}).
DependsOnFormat {0} is required where {2} has a value.
DictionaryNullKeyFormat {0} contains one or more keys that are not specified.
DictionaryNullValueFormat {0} contains one or more values that are not specified.
DuplicateFormat {0} already exists and would result in a duplicate.
DuplicateValue2Format {0} contains duplicates; {2} value specified more than once.
DuplicateValueFormat {0} contains duplicates; {2} value '{3}' specified more than once.
ExactLengthFormat {0} must be exactly {2} characters in length.
ExistsFormat {0} is not found; a valid value is required.
ImmutableFormat {0} is not allowed to change; please reset value.
InvalidFormat {0} is invalid.
MandatoryFormat {0} is required.
MaxCountFormat {0} must not exceed {2} item(s).
MaxDigitsFormat {0} must not exceed {2} digits in total.
MaxLengthFormat {0} must not exceed {2} characters in length.
MaxValueFormat {0} is greater than the maximum allowed value of {2}.
MinCountFormat {0} must have at least {2} item(s).
MinLengthFormat {0} must be at least {2} characters in length.
MinValueFormat {0} is less than the minimum allowed value of {2}.
MustFormat {0} is invalid.
RegexFormat {0} is invalid.
WildcardFormat {0} contains invalid or non-supported wildcard selection.

The validation framework passes the friendly text name as {0}, and the validating value as {1} for inclusion in the final message output. Higher numbered format strings are applicable to the specific validator rule consuming.


Usage

There are multiple means to leverage the validation framework.


Entity-based validator class

The primary means for an entity-based validator is to inherit from the Validator class (or AbstractValidator). The instance should be instantiated once (and cached) where possible as the underlying property expressions can be a relatively expensive (performance) operation.

Additionally, the OnValidate method can be overridden to add more complex and/or cross-property validations as required.

Each property for the entity is configured using the Property method (or RuleFor) and a corresponding property expression. The property expression is advantageous as the friendly text name can be inferred (in order specified):

  • Use the DisplayAttribute(Name="Product Code") value; will be: "Product Code";
  • Use the property name string CustomerNumber { get; set; } formatted as Sentence Case; will be "Customer Number".
  • The resulting text from above is then passed through the text localization (LText) resource string replacement.

An example is as follows:

public class PersonValidator : Validator<Person>
{
    public PersonValidator()
    {
        Property(x => x.Name).Mandatory().String(maxLength: 50);
        Property(x => x.Birthday).CompareValue(CompareOperator.LessThanEqual, DateTime.Now, "today");
    }

    protected override Task<Result> OnValidateAsync(ValidationContext<Test> context)
    {
        // Check that Amount property has not had an error already; then validate and error.
        context.Check(x = x.Amount, (val) => val <= 100, "{0} must be greater than 100.");

        return Result.SuccessTask;
    }
}

var person = new Person { Name = "Freddie", Birthday = new DateTime(1946, 09, 05), Amount = 150 };

// Validate the value.
var result = await new PersonValidator().ValidateAsync(person);

Entity-based inline validator

The secondary means for an entity-based validator is to define and execute inline. The HasProperty() is used to create a property (or HasRuleFor), with a corresponding action to enable validation configuration.

An example is as follows:

var person = new Person { Name = "Freddie", Birthday = new DateTime(1946, 09, 05);

// Create an entity-based validator on the fly.
var result = await Validator.Create<Test>()
    .HasProperty(x => x.Name, p => p.Mandatory().String(maxLength: 50))
    .HasProperty(x => x.Birthdar, p => p.CompareValue(CompareOperator.LessThanEqual, DateTime.Now, "today"))
    .ValidateAsync(person);

Value-based validator

Values, both entity and non-entity, can be validated directly. Examples are as follows:

var person = new Person { Name = "Freddie", Birthday = new DateTime(1946, 09, 05);

// Validate an entity value; being the Person class.
var pv = new PersonValidator();
await person.Validate().Entity(pv).RunAsync();

// Validate a value (e.g. a string, int, DateTime, etc.) without an entity-based validator.
await person.Name.Validate().Mandatory().String(maxLength: 10).RunAsync(throwOnError: true);

Validation chaining

As demonstrated in the prior examples the validation supports fluent-style (method-chaining) for the underlying rules and clauses.

When the validation is executed the rules will be invoked in the order in which they are specified, and conditionally invoked where succeeding clauses (optional) are specified for a rule (to the right of). A validation may have zero or more clauses before the first rule, then a rule with zero or more succeeding clauses, followed by zero or more rules, etc.

The following is a property that will only perform any succeeding rules once the DependsOn clause results in true; otherwise, no rules will be executed. Where true then the CompareProperty will be executed:

Property(x => x.DateTo).DependsOn(x => x.DateFrom).CompareProperty(CompareOperator.GreaterThanEqual, x => x.DateFrom);

The following is a property that will always execute the Mandatory rule, and only the CompareProperty rule where the DependsOn clause results in true:

Property(x => x.DateTo).Mandatory.CompareProperty(CompareOperator.GreaterThanEqual, x => x.DateFrom).DependsOn(x => x.DateFrom);

Common validations

To support reusablility of property validations a CommonValidator is used to enable. This allows for the validation logic to be defined once, and reused (shared) across multiple validations. This validator also enables validation to be configured for non-entities (e.g. intrinisic types).

An example is as follows:

var cv = CommonValidator<string> _cv = Validator.CreateCommon<string>(v => v.String(5).Must(x => x.Value != "XXXXX"));

var v = Validator.Create<TestData>()
    .HasProperty(x => x.Text, p => p.Mandatory().Common(cv));

Examples

The following represents a number of additional examples demonstrating property validation scenarios:

// The integer is mandatory, must be positive, and has a max value of 999, and must have a value greater of 10.
Property(x => x.Integer).Mandatory().Numeric(allowNegatives: false, maxDigits: 3).CompareValue(CompareOperator.GreaterThan, 10);

// The decimal should be treated as a positive currency with default decimal places (NumberFormatInfo.CurrentInfo.CurrencyDecimalDigits).
Property(x => x.Amount).Currency(allowNegatives: true);

// The decimal must be positive, with a max value of 999.999, and max three decimal places (max digits includes decimal places).
Property(x => x.Amount2).Numeric(allowNegatives: false, maxDigits: 6, decimalPlaces: 3);

// The Date From must be greater than Now; with specified text "today" to include in error message.
Property(x => x.DateFrom).CompareValue(CompareOperator.GreaterThan, DateTime.Now, "today");

// The Date To must be greater than the Date From where the Date From (DependsOn) has a value 
// (also DependsOn will not validate where the dependent field has previously failed).
Property(x => x.DateTo).DependsOn(x => x.DateFrom).CompareProperty(CompareOperator.GreaterThanEqual, x => x.DateFrom);

// When can be used to conditionalise a previous rule; so Name is mandatory only when the Integer value is 50; also, max length is 50.
Property(x => x.Name).Mandatory().When(x => x.Integer == 50).String(maxLength: 50);

// Must can used for more complex logic, as in the condition 'must' be true otherwise the value is considered invalid.
Property(x => x.Amount).Must(x => x.Integer > 10);

// The phone number will be validated against the defined regex. 
Property(x => x.PhoneNo).String(new Regex(@"\+0\d{9}|\+0[1-9]\d{12}|0[1-9]\d{8}|00[1-9]\d{9}|00[1-9]\d{13}"));

// The Gender (which is a Reference Data entity) is mandatory and must be considered valid.
Property(x => x.Gender).Mandatory().IsValid();

// Check the sub entity exists (mandatory) and is valid (using defined validator).
Property(x => x.SubTest).Mandatory().Entity().With(test2Validator);

// Check the sub entity collection (exists), has 1-4 items in the collection, and each is valid (using defined validator).
Property(x => x.SubTesters).Mandatory().Collection(minCount: 1, maxCount: 4, item: CollectionRuleItem.Create<Test2>(test2Validator));

The following demonstrates the mixing of both entity-based options:

public class PersonValidator : Validator<Person>
{
    private static readonly Validator<Address> _addressValidator = Validator.Create<Address>()
        .HasProperty(x => x.Street, p => p.Mandatory().String(50))
        .HasProperty(x => x.City, p => p.Mandatory().String(50));

    /// <summary>
    /// Initializes a new instance of the <see cref="PersonValidator"/>.
    /// </summary>
    public PersonValidator()
    {
        Property(x => x.FirstName).Mandatory().String(50);
        Property(x => x.LastName).Mandatory().String(50);
        Property(x => x.Gender).Mandatory().IsValid();
        Property(x => x.Birthday).Mandatory().CompareValue(CompareOperator.LessThanEqual, () => DateTime.Now, "Today");
        Property(x => x.Address).Entity().With(_addressValidator);
    }
}

Advanced

There are additional features that enable more advanced / complex validation scenarios.


Conditional rule set

The RuleSet represents a conditional validation rule for an entity, in that it groups one or more rules together that only get invoked where a specified condition results in true.

The following demonstrates RuleSet usage for an Entity-based validator class:

public class TestItemValidator : Validator<TestItem>
{
    public TestItemValidator()
    {
        RuleSet(x => x.Value.Code == "A", () => 
        {
            Property(x => x.Text).Mandatory().Must(x => x.Text == "A");
        });

        RuleSet(x => x.Value.Code == "B", () =>
        {
            Property(x => x.Text).Mandatory().Must(x => x.Text == "B");
        });
    }
}

The following demonstrates HasRuleSet usage for an Entity-based inline validator:

var v = Validator.Create<TestItem>()
    .HasRuleSet(x => x.Value.Code == "A", y =>
    {
        y.Property(x => x.Text).Mandatory().Must(x => x.Text == "A");
    })
    .HasRuleSet(x => x.Value.Code == "B", (y) =>
    {
        y.Property(x => x.Text).Mandatory().Must(x => x.Text == "B");
    });

Include/inherit validators

Where entities leverage inheritence, having the corresponding validators include the base (parent) classes validations rules can be advantageous (versus codifying the rules multiple times). The IncludeBase method enables a base validator to be included within another validator's rule set.

The following is an example of using the IncludeBase method:

var r = Validator.Create<TestData>()
    .IncludeBase(testDataBaseValidator)
    .HasProperty(x => x.CountB, p => p.Mandatory().CompareValue(CompareOperator.GreaterThan, 10))
    .Validate(new TestData { CountB = 0 });

Consolidating multiple validators

The MultiValidator enables the validation of multiple values there is a need to consolidate the results into a single set of Messages (and/or ValidationException).

An example is as follows:

var result = await MultiValidator.Create()
    .Add(person.Validate(nameof(value)).Mandatory().Entity(personValidator))
    .Add(other.Validate(nameof(other)).Mandatory().Entity(otherValidator))
    .RunAsync();