10

I'm trying to validate this property in MVC model, which can contain zero or more email addresses delimited by comma:

public class DashboardVM
{
    public string CurrentAbuseEmails { get; set; }
    ...
}

The question is how do I do this using the built-in fluent validation rule for Email Address? For now I have a solution using Must and regular expression which works, but I don't find it .. elegant enough.

    public DashboardVMValidator()
    {
        RuleFor(x => x.CurrentAbuseEmails).Must(BeValidDelimitedEmailList).WithMessage("One or more email addresses are not valid.");
    }

    private bool BeValidDelimitedEmailList(string delimitedEmails)
    {
        //... match very very long reg. expression
    }

So far the closest solution including RuleFor(...).EmailAddress() was creating a custom Validator below and call Validate on each email from the string, but that didn't work for some reason (AbuseEmailValidator wasn't able to get my predicate x => x - when calling validator.Validate on each email).

public class AbuseEmailValidator : AbstractValidator<string>
{
    public AbuseEmailValidator()
    {
        RuleFor(x => x).EmailAddress().WithMessage("Email address is not valid");
    }
}

Is there way to do this in some simple manner? Something similar to this solution, but with one string instead of list of strings, as I can't use SetCollectionValidator (or can I?): How do you validate against each string in a list using Fluent Validation?

5 Answers 5

9

You can try something like this:

public class InvoiceValidator : AbstractValidator<ContractInvoicingEditModel>
{
    public InvoiceValidator()
    {
        RuleFor(m => m.EmailAddressTo)
            .Must(CommonValidators.CheckValidEmails).WithMessage("Some of the emails   provided are not valid");
    }
}

public static class CommonValidators
{
    public static bool CheckValidEmails(string arg)
    {
        var list = arg.Split(';');
        var isValid = true;
        var emailValidator = new EmailValidator();

        foreach (var t in list)
        {
            isValid = emailValidator.Validate(new EmailModel { Email = t.Trim() }).IsValid;
            if (!isValid)
                break;
        }

        return isValid;
    }
}
public class EmailValidator : AbstractValidator<EmailModel>
{
    public EmailValidator()
    {
        RuleFor(x => x.Email).EmailAddress();
    }
}

public class EmailModel
{
    public string Email { get; set; }
}

It seems to work fine if you use an intermediary poco. My emails are separated by ";" in this case. Hope it helps.

Sign up to request clarification or add additional context in comments.

Comments

7

As of version 9, FluentValidation supports this without requiring custom validators using the Transform and ForEach methods.

In versions 9.0-9.4, you would write it like this:

RuleFor(x => x.List)
    .Transform(list => list.Split(','))
    .ForEach(itemRule => itemRule.EmailAddress());

In version 9.5 and up, RuleFor isn't used with Transform, so you write it like this:

Transform(x => x.List, list => list.Split(','))
    .ForEach(itemRule => itemRule.EmailAddress());

To handle nulls, use null coalesce operator in the Transform delegate:

list => (list ?? "").Split(',')

To handle whitespace, you may want to trim each item in the list. You can add a Select clause:

list => (list ?? "").Split(',')
    .Select(item => item.Trim())

If you want to ignore empty items, add a Where clause:

list => (list ?? "").Split(',')
    .Select(item => item.Trim())
    .Where(item => !string.IsNullOrEmpty(item))

To require that there is at least one item in the list, add the NotEmpty rule. So the final version 9.5+ code will look like:

Transform(x => x.List, 
    list => (list ?? "").Split(',')
        .Select(item => item.Trim())
        .Where(item => !string.IsNullOrEmpty(item)))
    .NotEmpty()
    .ForEach(itemRule => itemRule.EmailAddress());

For FluentValidation 11+, the Transform style methods are being deprecated. Their recommended practice is to move the parsing logic into the model being validated. An example based on the OP would look something like this:

// Model Class
public class DashboardVM
{
    public string CurrentAbuseEmails { get; set; }
    public List<string> CurrentAbuseEmailsParsed {
        get {
            return (CurrentAbuseEmails ?? "").Split(',')
                .Select(item => item.Trim())
                .Where(item => !string.IsNullOrEmpty(item))
                .ToList();
        }
    }
    ...
}

// Validator (note that the Parsed property is validated)
RuleFor(x => x.CurrentAbuseEmailsParsed)
    .NotEmpty()
    .ForEach(itemRule => itemRule.EmailAddress());

Alternatively a custom validator can be used, as described in other answers.

5 Comments

That's really a clever solution, the only problem with this is, that it apparently changes the PropertyName in case of an error. When validation fails, the PropertyName of the fail becomes eg.: "List[2]" (if the third address in the list fails) - is there any way to avoid this and get the PropertyName "List" in the validation result?
@aurora I don't have bandwidth to try it out right now, but I'm reasonably sure that adding WithMessage after ForEach will work; at least as a workaround.
@aurora I had a minute to look at this and I see what you mean about the PropertyName property of the ValidationFailure. This behavior can be changed by calling the OverrideIndexer extension method in the ForEach rule. So the full ForEach rule becomes: .ForEach(itemRule => itemRule.OverrideIndexer((a,b,c,d) => "").EmailAddress()).
Wow - thanks so much for having a look into this. With OverrideIndexer it works like a charm. Now i can remove my custom validator and replace it with your solution. Thanks! \m/
Transform will be deprecated in Fluent Validations 12 and above. Refer docs.fluentvalidation.net/en/latest/transform.html and github.com/FluentValidation/FluentValidation/issues/2072 for alternate recommendations
1

The provided answer above is good but quite old. So some of the code won't work with never versions of FluentValidation Nuget package. At least I got build errors. Also the solution can be more sophisticated. Recommend to use this:

Model:

public sealed class Email
{
    public string From { get; set; }

    /// <summary>
    /// Email address(es) to (can be settable separated list, default: ;)
    /// </summary>
    public string To { get; set; }

    //.....

    /// <summary>
    /// Separator char for multiple email addresses
    /// </summary>
    public char EmailAddressSeparator { get; set; }

    public Email()
    {
        EmailAddressSeparator = ';';
    }
}

Custom validator:

public static class CommonValidators
{
    public static bool CheckValidEmails(Email email, string emails)
    {
        if(string.IsNullOrWhiteSpace(emails))
        {
            return true;
        }

        var list = emails.Split(email.EmailAddressSeparator);
        var isValid = true;

        foreach (var t in list)
        {
            var email = new EmailModel { Email = t.Trim() };
            var validator = new EmailModelValidator();

            isValid = validator.Validate(email).IsValid;
            if (!isValid)
            {
                break;
            }
        }

        return isValid;
    }

    private class EmailModel
    {
        public string Email { get; set; }
    }
    private class EmailModelValidator : AbstractValidator<EmailModel>
    {
        public EmailModelValidator()
        {
            RuleFor(x => x.Email).EmailAddress(EmailValidationMode.AspNetCoreCompatible).When(x => !string.IsNullOrWhiteSpace(x.Email));
        }
    }
}

Usage:

    public class EmailValidator : AbstractValidator<Email>
    {
        public EmailValidator()
        {
            RuleFor(x => x.To).NotEmpty()
                .Must(CommonValidators.CheckValidEmails)
                .WithMessage($"'{nameof(To)}' some of the emails provided are not a valid email address.");
        }
    }

Comments

1

I wanted something a bit simpler and to be able to chain with condtions like .When(), .Unless() and .WithMessage(). So i built upon Burhan Savcis solution with an extension method:

public static class ValidatorExtensions
{
    public static IRuleBuilderOptions<T, string> CheckValidEmails<T>(this IRuleBuilder<T, string> ruleBuilder, string separator)
    {
        var emailValidator = new EmailValidator();

        return ruleBuilder.Must(emails => emails.Split(separator).All(email => emailValidator.Validate(email.Trim()).IsValid));
    }

    private class EmailValidator : AbstractValidator<string>
    {
        public EmailValidator()
        {
            RuleFor(x => x).EmailAddress();
        }
    }
}

In my case I have a CRQS-command for exporting data with some more input options, including a dropdown selecting export type (file/email/other options).

    public class Command : IRequest<Result>
    {
        public string EmailAddress{ get; set; }
        public ExportType ExportType{ get; set; }

    }

And then use it like this:

    public class Validator : AbstractValidator<Command>
    {
        public Validator()
        {
            RuleFor(c => c.ExportOptions.EmailAddress).CheckValidEmails(",").When(c => c.ExportType == ExportType.Email).WithMessage("One or more email addresses are not valid");
        }
    }

Comments

0

You can write a custom validator extension. In this way, you can define whatever separator you want, use it for every string property not only specific property, and add a different message based on condition.

You can learn more about custom validators from the documentation: https://docs.fluentvalidation.net/en/latest/custom-validators.html

Custom validator extension:

public static class ValidatorExtensions
{
    public static IRuleBuilderInitial<T, string> CheckValidEmails<T>(this IRuleBuilder<T, string> ruleBuilder, string separator)
    {
        bool isValid;
        var emailValidator = new EmailValidator();
        return ruleBuilder.Custom((emailsStr, context) =>
        {
            if (string.IsNullOrWhiteSpace(emailsStr))
            {
                context.AddFailure($"'{context.DisplayName}' must not be empty");
                return;
            }

            var emails = emailsStr.Split(separator);
            foreach (var email in emails)
            {
                isValid = emailValidator.Validate(email.Trim()).IsValid;
                if (!isValid)
                {
                    context.AddFailure($"'{email}' is not a valid email address");
                    break;
                }
            }
        });
    }

    private class EmailValidator : AbstractValidator<string>
    {
        public EmailValidator()
        {
            RuleFor(x => x).EmailAddress();
        }
    }
}
       

If you want the separator as a model property then you can write the extension like this:

public static IRuleBuilderInitial<T, string> CheckValidEmails<T>(this IRuleBuilder<T, string> ruleBuilder, Func<T, string> separatorSelector)
    {
        if (separatorSelector == null)
            throw new ArgumentNullException(nameof(separatorSelector), $"{nameof(separatorSelector)} cannot be null");
        
        bool isValid;
        var emailValidator = new EmailValidator();
        return ruleBuilder.Custom((emailsStr, context) =>
        {
            if (string.IsNullOrWhiteSpace(emailsStr))
            {
                context.AddFailure($"'{context.DisplayName}' must not be empty");
                return;
            }

            var separator = separatorSelector.Invoke((T) context.InstanceToValidate);
            var emails = emailsStr.Split(separator);
            foreach (var email in emails)
            {
                isValid = emailValidator.Validate(email.Trim()).IsValid;
                if (!isValid)
                {
                    context.AddFailure($"'{email}' is not a valid email address");
                    break;
                }
            }
        });
    }

                                                     

Sample Model:

public class EmailsModel
{

    /// <summary>
    /// emails separated by ;
    /// </summary>
    public string Emails { get; set; }

    public string EmailsSeparator { get; set; } = ";";
}

Usage:

public class EmailsModelValidator : AbstractValidator<EmailsModel>
{
    public EmailsModelValidator()
    {
        RuleFor(x => x.Emails).CheckValidEmails(";");
        RuleFor(x => x.Emails).CheckValidEmails(x => x.EmailsSeparator);
    }
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.