Monthly Archives: January 2018

FluentValidation and Emails with White Space

Quite often, users will copy and paste an email address into an email text input. And that email address will contain trailing white space. If that gets past your client-side validation code, you need to trim it at the server. That’s usually not a problem. But, if you are using the email validator in FluentValidation, such an address will fail validation and the message sent back to the user will be a generic one that the email address is not valid. It won’t mention a trailing space. And for most users, it can take quite a bit of time to figure out that the email address has some trailing whitespace. If our help-desk is anything to go by, many users didn’t figure that out at all and so it was down to me to address this.

After a lengthy discussion with our domain experts, we decided that we did not want an email address to be considered invalid, just because of a trailing space. Technically it is invalid. But it is easy enough to trim it in the Services layer. But we must first get past server-side validation. So, my existing validator, which was returning an error message about an invalid email address looked something like this:

RuleFor(m => m.NewEmail)
	.EmailAddress()	
	.WithMessage(ValidationConstants.SymbolIsNotAValidEmailAddress, x => x.NewEmail)
	.WithName("NewEmail"); 

As I mentioned above, the EmailAddress() validator will fail the validation of an email address with a trailing space. Before I move onto the custom validator I ended up writing, I just want to quickly note something we tried, but which turned out to be a bad idea. First, I tried the following:

RuleFor(m => m.NewEmail.Trim())
	.EmailAddress()
	.When(m => m.NewEmail != null)
	.WithMessage(ValidationConstants.SymbolIsNotAValidEmailAddress, x => x.NewEmail)
	.WithName("NewEmail"); 

Trimming the email property (in the 1st line) immediately felt wrong. We also ran into a few problems with that approach, which are beyond the scope of this post. But most importantly, mutating the property in the RuleFor lambda is not the way FluentValidation is meant to be used.

The right approach, the way we saw it, was to write a custom validator which would basically validate the email in the same way as the normal EmailAddress validator does, except permitting trailing whitespace. Step 1 in this process was to grab the source of the EmailAddress validator, which can be found here. Step 2 was to create the new validator itself, based on the EmailValidator referred to in Step 1, which I creatively named NonTrimmedEmailValidator:

    public class NonTrimmedEmailValidator : PropertyValidator, IRegularExpressionValidator, IEmailValidator
    {
        private readonly Regex regex;

        const string expression = @"^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-||_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+([a-z]+|\d|-|\.{0,1}|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])?([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$";

        public NonTrimmedEmailValidator()
            : base(new LanguageStringSource("NonTrimmedEmailValidator"))
        {
            regex = new Regex(expression, RegexOptions.IgnoreCase);
        }


        protected override bool IsValid(PropertyValidatorContext context)
        {
            if (context.PropertyValue == null) return true;

            // only interested in whether the trimmed string is a valid email address.
            var trimmedEmail = ((string)context.PropertyValue).Trim();

            if (!regex.IsMatch(trimmedEmail))
            {
                return false;
            }

            return true;
        }

        public string Expression
        {
            get { return expression; }
        }
    }
}

Step 3 was to create an extension method to make it usable like the other fluent validators:

public static class FluentValidationExtensions
{
    public static IRuleBuilderOptions<T, TProperty> NonTrimmedStringIsValidEmailAddressWhenTrimmed<T, TProperty>(this IRuleBuilder<T, TProperty> ruleBuilder)
    {
        return ruleBuilder.SetValidator(new NonTrimmedEmailValidator());
    }
}

Now, we can validate objects which have properties that are email addresses, where the email has a trailing space.

RuleFor(m => m.NewEmail)
	.NonTrimmedStringIsValidEmailAddressWhenTrimmed()
	.WithMessage(ValidationConstants.SymbolIsNotAValidEmailAddress, x => x.NewEmail)
	.WithName("NewEmail");

And, like I am doing anyway, trim the string in the services layer. Now, the user does not get a validation failure when they copy and paste an email address from somewhere that happens to grab a bit of white space on the end.