Data Annotations and Custom Validators in C#

I was working merrily on my ASP.NET MVC6 Identity application when I realized that it would be really cool to have a custom validator on my username property. A bit of research showed me how to do it and it isn’t that complicated.

Let’s start from the beginning. At some point I’m going to push my username and password into a ViewModel and I want that to have validation. Here is the validation I want:

using System.ComponentModel.DataAnnotations;

using Grumpy.Wizards.Validation;

namespace Grumpy.Wizards.Areas.Public.ViewModels
{
    public class LoginViewModel
    {
        [Required]
        [Display(Name = "Email Address")]
        [StringLength(254, MinimumLength = 4)]
        [ApplicationUser]
        public string Email { get; set; }

        [Required]
        [StringLength(254, MinimumLength = 5)]
        [DataType(DataType.Password)]
        public string Password { get; set; }
    }
}

The component model handles most of this. You can read about decorators on the ASP.NET website. I’ve included a custom validator in this – the ApplicationUser validator.

I’ve got a namespace for my validators – Grumpy.Wizards.Validation. This is just the Validation directory on disk in my project. Here is a pattern to follow for creating one of these validators:

using System;
using System.ComponentModel.DataAnnotations;

namespace Grumpy.Wizards.Validation
{
    [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
    public class ApplicationUserAttribute : ValidationAttribute
    {
        /// <summary>
        /// Constructor - specify the default error message for the validation.
        /// </summary>
        public ApplicationUserAttribute() : base("{0} is not a valid RFC5321 email address")
        {

        }

        /// <summary>
        /// Validate that the object is of the correct value/form.  
        /// </summary>
        /// <param name="value">The value to be validated</param>
        /// <param name="validationContext">Any options to the validation</param>
        /// <returns>ValidationResult.Success if the value is ok, or a
        ///     ValidationResult(errorMessage) if not</returns>
        protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        {
            bool fValid = false;

            // Do your validation here, setting fValid to true if it is valid

            if (fValid)
            {
                return ValidationResult.Success;
            } else
            {
                return new ValidationResult(this.FormatErrorMessage(validationContext.DisplayName));
            }
        }
    }
}

You can add in anything else you want here, but you absolutely need the IsValid() method. With this pattern you can specify an Error Message to be displayed. If you need to pass something into the validator, you can do that as well. For instance:

using System.ComponentModel.DataAnnotations;

using Grumpy.Wizards.Validation;

namespace Grumpy.Wizards.Areas.Public.ViewModels
{
    public class LoginViewModel
    {
        [Required]
        [Display(Name = "Email Address")]
        [StringLength(254, MinimumLength = 4)]
        [ApplicationUser("admin", ErrorMessage="Provide your correct email address")]
        public string Email { get; set; }

        [Required]
        [StringLength(254, MinimumLength = 5)]
        [DataType(DataType.Password)]
        public string Password { get; set; }
    }
}

Note the “admin” – this can be provided to the constructor like this:

namespace Grumpy.Wizards.Validation
{
    [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
    public class ApplicationUserAttribute : ValidationAttribute
    {
        /// <summary>
        /// Constructor - specify the default error message for the validation.
        /// </summary>
        public ApplicationUserAttribute(string username) 
            : base("{0} is not a valid RFC5321 email address")
        {

        }

There are many more details here, but these are the basics.

Thanks to a great post on Stack Overflow, I had 99% of the class written for me. However, my “default admin user” was being left out. In short, I wanted the username to be valid if it was RFC-5321 compliant (which is the current specification for a correct email address) OR if it was the admin user. Here is my eventual code:

using System;
using System.ComponentModel.DataAnnotations;
using System.Text.RegularExpressions;

namespace Grumpy.Wizards.Validation
{
    /// <summary>
    /// Validate that the provided username is a valid RFC 5321
    /// email address OR is the default username - those are the
    /// only acceptable versions.
    ///
    /// For the reference on RFC-5321, see http://stackoverflow.com/questions/6449367/c-sharp-email-address-validation
    /// </summary>
    [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
    public class ApplicationUserAttribute : ValidationAttribute
    {
        // This is the default username
        private string _defaultUsername = Startup.Configuration.Get("DefaultUser:Username");

        // Holds the Email Address regular expression
        private static Regex rfc5321address = CreateEmailAddressRegex();

        /// <summary>
        /// Constructor - specify the default error message for the validation.
        /// </summary>
        public ApplicationUserAttribute() : base("{0} is not a valid RFC5321 email address")
        {

        }

        /// <summary>
        /// Create a regular expression for validating RFC-5321 email addresses
        /// </summary>
        /// <returns>Regex Object</returns>
        private static Regex CreateEmailAddressRegex()
        {
            // references: RFC 5321, RFC 5322, RFC 1035, plus errata.
            string atom = @"([A-Z0-9!#$%&'*+-/=?^_`{|}~]+)";
            string dot = @"(.)";
            string dotAtom = "(" + atom + "(" + dot + atom + ")*" + ")";
            string dnsLabel = "([A-Z]([A-Z0-9-]{0,61}[A-Z0-9])?)";
            string fqdn = "(" + dnsLabel + "(" + dot + dnsLabel + ")*" + ")";

            string localPart = "(?" + dotAtom + ")";
            string domain = "(?" + fqdn + ")";
            string emailAddrPattern = "^" + localPart + "@" + domain + "$";

            Regex instance = new Regex(emailAddrPattern, RegexOptions.Singleline | RegexOptions.IgnoreCase);
            return instance;
        }

        /// <summary>
        /// Part of the Data Annotation Validation API, this method actually checks
        /// the given value against RFC-5321 (which is the current specification of
        /// the username.
        /// </summary>
        /// <param name="value">The value to be validated</param>
        /// <param name="validationContext">Any options to the validation (like error message)</param>
        /// <returns>Validation result (basically a boolean or error message)</returns>
        protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        {
            var s = value.ToString();

            if (s.ToLowerInvariant().Equals(_defaultUsername.ToLowerInvariant()))
            {
                // The provided string is the default username - bypass the checks
                return ValidationResult.Success;
            }

            bool fValid = string.IsNullOrEmpty(s);
            if (!fValid)
            {
                Match m = rfc5321address.Match(s);
                if (m.Success)
                {
                    string emailAddr = m.Value;
                    string localPart = m.Groups["localpart"].Value;
                    string domain = m.Groups["domain"].Value;
                    bool fLocalPartLengthValid = localPart.Length >= 1 && localPart.Length = 1 && domain.Length = 1 && emailAddr.Length <= 256;

                    fValid = fLocalPartLengthValid && fDomainLengthValid && fEmailAddrLengthValid;
                }
            }

            if (fValid)
            {
                return ValidationResult.Success;
            } else
            {
                return new ValidationResult(this.FormatErrorMessage(validationContext.DisplayName));
            }
        }
    }
}

The regular expression checking comes from the Stack Overflow article (and how I wish there was a curated blog of the best tips from Stack Overflow!). I’ve then added a specific test against the default username (which I receive from the Startup Configuration).