8

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?

Community
  • 1
  • 1
Matej
  • 165
  • 3
  • 7

5 Answers5

8

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.

MihaiPopescu
  • 98
  • 1
  • 4
5

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());
Jack A.
  • 4,245
  • 1
  • 20
  • 34
  • 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 Aug 17 '22 at 15:10
  • @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. – Jack A. Aug 17 '22 at 15:22
  • 1
    @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())`. – Jack A. Aug 21 '22 at 01:39
  • 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/ – aurora Aug 22 '22 at 09:27
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.");
        }
    }
Major
  • 5,948
  • 2
  • 45
  • 60
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");
        }
    }
mewmew
  • 11
  • 2
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);
    }
}
Burhan Savci
  • 376
  • 1
  • 4
  • 9