Reputation: 184
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?
Upvotes: 10
Views: 5718
Reputation: 4453
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.
Upvotes: 7
Reputation: 11
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");
}
}
Upvotes: 1
Reputation: 396
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);
}
}
Upvotes: 0
Reputation: 6688
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.");
}
}
Upvotes: 1
Reputation: 108
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.
Upvotes: 9