Skip to content

Small .NET and JavaScript library which provides annotation-based conditional validation mechanisms. Given attributes allow to forget about imperative way of step-by-step verification of validation conditions in many cases. Since fields validation requirements are applied as metadata, domain-related code is more condensed.

License

Notifications You must be signed in to change notification settings

jasonhjohnson/ExpressiveAnnotations

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 

Repository files navigation

#ExpressiveAnnotations[annotation-based conditional validation]

Notice: This document describes latest implementation. For previous concept (version < 2.0) take a look at EA1 branch.

ExpressiveAnnotations is a small .NET and JavaScript library, which provides annotation-based conditional validation mechanisms. Given RequiredIf and AssertThat attributes allow to forget about imperative way of step-by-step verification of validation conditions in many cases. This in turn results in less amount of code which is also more condensed, since fields validation requirements are applied as metadata, just in the place of such fields declaration.

###Table of contents

###What is the context behind this implementation?

There are number of cases where the concept of metadata is used for justified reasons. Attributes are one of the ways to associate complementary information with existing data. Such annotations may also define the correctness of data. Declarative validation when compared to imperative approach seems to be more convenient in many cases. Clean, compact code - all validation logic defined within the model scope. Simple to write, obvious to read.

###RequiredIf vs. AssertThat - where is the difference?

  • RequiredIf - if value is not yet provided, check whether it is required (annotated field is required to be non-null, when given condition is satisfied),
  • AssertThat - if value is already provided, check whether the condition is met (non-null annotated field is considered as valid, when given condition is satisfied).

###What are brief examples of usage?

If you'll be interested in comprehensive examples afterwards, take a look inside chosen demo project:

For the time being, to keep your ear to the ground, let's walk through few exemplary code snippets:

using ExpressiveAnnotations.Attributes;

[RequiredIf("GoAbroad == true")]
public string PassportNumber { get; set; }

Above we are saying, that annotated field is required when condition given in the logical expression is satisfied (passport number is required, if go abroad field has true boolean value).

Simple enough, let's move to another variation:

[AssertThat("ReturnDate >= Today()")]
public DateTime? ReturnDate { get; set; }

By the usage of this attribute type, we are not validating field requirement as before - its value is allowed to be null this time. Nevertheless, if some value is already given, provided restriction needs to be satisfied (return date needs to be greater than or equal to the date returned by Today() built-in function).

As shown below, both types of attributes may be combined (moreover, the same type can be applied multiple times for a single field):

[RequiredIf("Details.Email != null")]
[RequiredIf("Details.Phone != null")]
[AssertThat("AgreeToContact == true")]
public bool? AgreeToContact { get; set; }

Literal translation means, that if either email or phone is provided, you are forced to authorize someone to contact with you (boolean value indicating contact permission has to be true). What is more, we can see that nested properties are supported by the expressions parser.

Finally, take a brief look at following construction:

[RequiredIf(@"GoAbroad == true
              && (
                     (NextCountry != 'Other' && NextCountry == Country)
                     || (Age > 24 && Age <= 55)
                 )")]
public string ReasonForTravel { get; set; }

Restriction above is slightly more complex than its predecessors, but still can be quickly understood (reason for travel has to be provided if you plan to go abroad and: want to visit the same definite country twice, or are between 25 and 55 years old).

###Declarative vs. imperative programming - what is it about?

With declarative programming you write logic that expresses what you want, but not necessarily how to achieve it. You declare your desired results, but not the necessarily step-by-step.

In our case, this concept is materialized by attributes, e.g.

[RequiredIf("GoAbroad == true && NextCountry != 'Other' && NextCountry == Country",
    ErrorMessage = "If you plan to travel abroad, why visit the same country twice?")]
public string ReasonForTravel { get; set; }

With imperative programming you define the control flow of the computation which needs to be done. You tell the compiler what you want, exactly step by step.

If we choose this way instead of model fields decoration, it has negative impact on the complexity of the code. Logic responsible for validation is now implemented somewhere else in our application, e.g. inside controllers actions instead of model class itself:

    if (!model.GoAbroad)
        return View("Success");
    if (model.NextCountry == "Other")
        return View("Success");
    if (model.NextCountry != model.Country)
        return View("Success");
    
    ModelState.AddModelError("ReasonForTravel", 
        "If you plan to travel abroad, why visit the same country twice?");
    return View("Home", model);
}

###How to construct conditional validation attributes?

#####Signatures

RequiredIfAttribute(
    string expression,
    [bool AllowEmptyStrings] ...) - Validation attribute which indicates that annotated 
                                    field is required when computed result of given logical 
                                    expression is true.
AssertThatAttribute(
    string expression ...)        - Validation attribute, executed for non-null annotated 
                                    field, which indicates that assertion given in logical 
                                    expression has to be satisfied, for such field to be 
                                    considered as valid.

expression        - The logical expression based on which specified condition is computed.
AllowEmptyStrings - Gets or sets a flag indicating whether the attribute should allow empty 
                    or whitespace strings.

#####Implementation

Implementation core is based on top-down recursive descent logical expressions parser, with a single token of lookahead (LL(1)), which runs on the following EBNF-like grammar:

expression => or-exp
or-exp     => and-exp [ "||" or-exp ]
and-exp    => rel-exp [ "&&" and-exp ]
rel-exp    => not-exp [ rel-op not-exp ]
not-exp    => add-exp | "!" not-exp
add-exp    => mul-exp add-exp'
add-exp'   => "+" add-exp | "-" add-exp
mul-exp    => val mul-exp'
mul-exp'   => "*" mul-exp | "/" mul-exp
rel-op     => "==" | "!=" | ">" | ">=" | "<" | "<="
val        => "null" | int | float | bool | string | func | "(" or-exp ")"

Terminals are expressed in quotes. Each nonterminal is defined by a rule in the grammar except for int, float, bool, string and func, which are assumed to be implicitly defined (func can be an enum value as well as constant, property or function name).

Logical expressions should be built according to the syntax defined by grammar, with the usage of following components:

  • binary operators: ||, &&, !,
  • relational operators: ==, !=,<, <=, >, >=,
  • arithmetic operators: +, -, *, /,
  • brackets: (, ),
  • alphanumeric characters with the support of ,, ., _, ' and whitespaces, used to synthesize suitable literals:
    • null literal: null,
    • integer number literals, e.g. 123,
    • real number literals, e.g. 1.5 or -0.3e-2,
    • boolean literals: true and false,
    • string literals: 'in single quotes' (internal quote escape sequence is \', character representing new line no matter the platform is \n),
    • func literals:
      • property names, e.g. SomeProperty,
      • constants, e.g. SomeType.CONST,
      • enum values, e.g. SomeEnumType.SomeValue,
      • function invocations, e.g. SomeFunction(...).

Specified expression string is parsed and converted into expression tree structure. A delegate containing compiled version of the lambda expression described by produced expression tree is returned as a result of the parser job. Such delegate is then invoked for specified model object. As a result of expression evaluation, boolean flag is returned, indicating that expression is true or false.

When working with ASP.NET MVC stack, unobtrusive client-side validation mechanism is additionally available. Client receives unchanged expression string from server. Such an expression is then evaluated using JavaScript eval() method within the context of reflected model object. Such a model, analogously to the server-side one, is basically deserialized DOM form (with some type-safety assurances and registered toolchain methods).

#####Traps

Attention is needed when coping with null because of discrepancies between C# and JavaScript, e.g.

  • null + "text" - in C# "text", in JS "nulltext",
  • 2 * null - in C# null , in JS 0,
  • null > -1 - in C# false , in JS true,
  • and more...

#####Built-in functions

As already noted, there is an option to reinforce expressions with functions, e.g.

[AssertThat("StartsWith(CodeName, 'abc.') || EndsWith(CodeName, '.xyz')")]
public string CodeName { get; set; }

Toolchain functions available out of the box at server- and client-side:

  • DateTime Now()
    • Gets the current local date and time.
  • DateTime Today()
    • Gets the current date with the time component set to 00:00:00.
  • DateTime Date(int year, int month, int day)
    • Initializes a new date to a specified year, month (months are 1-based), and day, with the time component set to 00:00:00.
  • DateTime Date(int year, int month, int day, int hour, int minute, int second)
    • Initializes a new date to a specified year, month (months are 1-based), day, hour, minute, and second.
  • int Length(str)
    • Gets the number of characters in a specified string (null-safe).
  • string Trim(string str)
    • Removes all leading and trailing white-space characters from a specified string (null-safe).
  • string Concat(string strA, string strB)
    • Concatenates two specified strings (null-safe).
  • string Concat(string strA, string strB, strC)
    • Concatenates three specified strings (null-safe).
  • int CompareOrdinal(string strA, string strB)
    • Compares strings using ordinal sort rules. An integer that indicates the lexical relationship between the two comparands is returned (null-safe):
      • -1 - strA is less than strB,
      •  0 - strA and strB are equal,
      •  1 - strA is greater than strB.
  • int CompareOrdinalIgnoreCase(string strA, string strB)
    • Compares strings using ordinal sort rules and ignoring the case of the strings being compared (null-safe).
  • bool StartsWith(string str, string prefix)
    • Determines whether the beginning of specified string matches a specified prefix (null-safe).
  • bool StartsWithIgnoreCase(string str, string prefix)
    • Determines whether the beginning of specified string matches a specified prefix, ignoring the case of the strings (null-safe).
  • bool EndsWith(string str, string suffix)
    • Determines whether the end of specified string matches a specified suffix (null-safe).
  • bool EndsWithIgnoreCase(string str, string suffix)
    • Determines whether the end of specified string matches a specified suffix, ignoring the case of the strings (null-safe).
  • bool Contains(string str, string substr)
    • Returns a value indicating whether a specified substring occurs within a specified string (null-safe).
  • bool ContainsIgnoreCase(string str, string substr)
    • Returns a value indicating whether a specified substring occurs within a specified string, ignoring the case of the strings (null-safe).
  • bool IsNullOrWhiteSpace(string str)
    • Indicates whether a specified string is null, empty, or consists only of white-space characters (null-safe).
  • bool IsDigitChain(string str)
    • Indicates whether a specified string represents a sequence of digits (ASCII characters only, null-safe).
  • bool IsNumber(string str)
    • Indicates whether a specified string represents integer or float number (ASCII characters only, null-safe).
  • bool IsEmail(string str)
    • Indicates whether a specified string represents valid e-mail address (null-safe).
  • bool IsUrl(string str)
    • Indicates whether a specified string represents valid url (null-safe).
  • bool IsRegexMatch(string str, string regex)
    • Indicates whether the regular expression finds a match in the input string (null-safe).
  • Guid Guid(string str)
    • Initializes a new instance of the Guid structure by using the value represented by a specified string.

###What about the support of ASP.NET MVC client-side validation?

Client-side validation is fully supported. Enable it for your web project within the next few steps:

  1. Reference both assemblies to your project: core ExpressiveAnnotations.dll and subsidiary ExpressiveAnnotations.MvcUnobtrusiveValidatorProvider.dll,

  2. In Global.asax register required validators (IClientValidatable interface is not directly implemented by the attributes, to avoid coupling of ExpressionAnnotations assembly with System.Web.Mvc dependency):

    using ExpressiveAnnotations.Attributes;
    using ExpressiveAnnotations.MvcUnobtrusiveValidatorProvider.Validators;
    
    protected void Application_Start()
    {
        DataAnnotationsModelValidatorProvider.RegisterAdapter(
            typeof (RequiredIfAttribute), typeof (RequiredIfValidator));
        DataAnnotationsModelValidatorProvider.RegisterAdapter(
            typeof (AssertThatAttribute), typeof (AssertThatValidator));
  3. Include expressive.annotations.validate.js script in your page (it should be included in bundle below jQuery validation files):

    <script src="/Scripts/jquery.validate.js"></script>
    <script src="/Scripts/jquery.validate.unobtrusive.js"></script>
    ...
    <script src="/Scripts/expressive.annotations.validate.js"></script>

Alternatively, visit the installation section.

###Frequently asked questions

#####What if there is no built-in function I need?

Create it yourself. Any custom function defined within the model class scope at server-side is automatically recognized and can be used inside expressions, e.g.

class Model
{
    public bool IsBloodType(string group) 
    {
        return Regex.IsMatch(group, @"^(A|B|AB|0)[\+-]$");
    }

    [AssertThat("IsBloodType(BloodType)")] // method known here (context aware expressions)
    public string BloodType { get; set; }

If client-side validation is needed as well, function of the same signature (name and the number of parameters) must be available there. JavaScript corresponding implementation should be registered by the following instruction:

<script>    
    ea.addMethod('IsBloodType', function(group) {
        return /^(A|B|AB|0)[\+-]$/.test(group);
    });

Many signatures can be defined for a single function name. Types are not taken under consideration as a differentiating factor though. Methods overloading is based on the number of arguments only. Functions with the same name and exact number of arguments are considered as ambiguous. The next issue important here is the fact that custom methods take precedence over built-in ones. If exact signatures are provided built-in methods are simply overridden by new definitions.

#####How to cope with dates given in non-standard formats?

When values of DOM elements are extracted, they are converted to appropriate types. For fields containing date strings, JavaScript Date.parse() method is used by default. As noted in MDN, the input parameter is:

A string representing an RFC 2822 or ISO 8601 date (other formats may be used, but results may be unexpected)

When some non-standard format needs to be handled, simply override the default behavior and provide your own implementation. E.g. when dealing with UK format dd/mm/yyyy:

<script>
    ea.settings.parseDate = function(str) {
        if (!/^\d{2}\/\d{2}\/\d{4}$/.test(str)) { // in case str format is not dd/mm/yyyy...
            return Date.parse(str); // ...default date parser is used
        }
        var arr = str.split('/');
        var date = new Date(arr[2], arr[1] - 1, arr[0]);
        return date.getTime(); // return milliseconds since January 1, 1970, 00:00:00 UTC
    }

#####What if ea variable is already used by another library?

Use noConflict() method. In case of naming collision return control of the ea variable back to its origins. Old references of ea are saved during ExpressiveAnnotations initialization - noConflict() simply restores them:

<script src="another.js"></script>
<script src="expressive.annotations.validate.js"></script>
<script>
    var expann = ea.noConflict();
    expann.addMethod... // do something with ExpressiveAnnotations
    ea... // do something with original ea variable

#####How to control frequency of dependent fields validation?

When a field value is modified, validation results for some other fields, directly dependent on currenty modified one, may be affected. To control the frequency of when dependent fields validation is triggered, change default ea.settings.dependencyTriggers settings. It is a string containing one or more DOM field event types (such as change, keyup or custom event names), associated with currently modified field, for which fields directly dependent on are validated. Default settings value is 'change paste keyup' (for more information check eventType parameter of jQuery bind() method).

<script>
    ea.settings.dependencyTriggers = 'change'; // mute some excessive activity if you wish

###Installation

Simplest way is using the NuGet Package Manager Console:

  • complete package - both assemblies and the script included (allows complete MVC validation):

    ###PM> Install-Package ExpressiveAnnotations

  • minimal package - core assembly only (MVC-related client-side coating components excluded):

    ###PM> Install-Package ExpressiveAnnotations.dll

###Contributors

GitHub Users

Special thanks to Szymon Malczak

###License

Copyright (c) 2014 Jaroslaw Waliszko

Licensed MIT: http://opensource.org/licenses/MIT

About

Small .NET and JavaScript library which provides annotation-based conditional validation mechanisms. Given attributes allow to forget about imperative way of step-by-step verification of validation conditions in many cases. Since fields validation requirements are applied as metadata, domain-related code is more condensed.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages

  • C# 84.5%
  • JavaScript 14.4%
  • CSS 1.1%