FluentValidation 未正确验证电子邮件地址列表?
FluentValidation not validating Email Address List(s) correctly?
{Version 8.0.0}
为什么这个测试会通过?
测试:
[Test]
public void Validation_NullTo_ShouldThrowModelValidationException()
{
var config = new EmailMessage
{
Subject = "My Subject",
Body = "My Body",
To = null
};
EmailMessageValidator validator = new EmailMessageValidator();
ValidationResult results = validator.Validate(config);
if (!results.IsValid)
{
foreach (var failure in results.Errors)
{
Console.WriteLine("Property " + failure.PropertyName + " failed validation. Error was: " + failure.ErrorMessage);
}
}
// results.Errors is Empty and resuts.IsValid is true here. Should be false with at least one Error message.
Assert.True(results.IsValid);
}
如果我像这样更改消息的结构,验证会正常失败。
var config = new EmailMessage
{
Subject = "My Subject",
Body = "My Body"
};
config.To = new EmailAddressList();
验证者:
public class EmailAddressListAtLeastOneRequiredValidator : AbstractValidator<EmailAddressList>
{
public EmailAddressListAtLeastOneRequiredValidator()
{
RuleFor(model => model)
.NotNull()
.WithMessage(AppMessages.Validation.AtLeastOneShouldBeDefined.ParseIn(nameof(EmailAddressList.Value)));
RuleFor(model => model.Value)
.NotNull()
.WithMessage(AppMessages.Validation.AtLeastOneShouldBeDefined.ParseIn(nameof(EmailAddressList.Value)));
RuleFor(model => model.Value.Count)
.GreaterThan(0)
.WithMessage(AppMessages.Validation.AtLeastOneShouldBeDefined.ParseIn(nameof(EmailAddressList.Value)));
When(model => model.Value?.Count > 0, () =>
{
RuleFor(model => model.Value)
.NotEmpty()
.WithMessage(AppMessages.Validation.AtLeastOneShouldBeDefined.ParseIn(nameof(EmailAddressList.Value)));
});
}
}
public class EmailAddressListValidator : AbstractValidator<EmailAddressList>
{
public EmailAddressListValidator()
{
RuleFor(model => model.Value).SetCollectionValidator(new EmailAddressValidator());
}
}
public class EmailAddressValidator : AbstractValidator<EmailAddress>
{
public EmailAddressValidator()
{
When(model => model.Value != null, () =>
{
RuleFor(model => model.Value)
.EmailAddress()
.WithMessage(AppMessages.Validation.ValueCannotBeNullOrEmpty.ParseIn(nameof(EmailAddress)));
});
}
}
public class EmailMessageValidator : AbstractValidator<EmailMessage>
{
public EmailMessageValidator()
{
RuleFor(model => model.To).SetValidator(new EmailAddressListAtLeastOneRequiredValidator());
When(model => model.Cc?.Value?.Count > 0, () =>
{
RuleFor(model => model.Cc).SetValidator(new EmailAddressListValidator());
});
When(model => model.Bcc?.Value?.Count > 0, () =>
{
RuleFor(model => model.Bcc).SetValidator(new EmailAddressListValidator());
});
RuleFor(model => model.Subject)
.NotEmpty().WithMessage(AppMessages.Validation.ValueCannotBeNullOrEmpty.ParseIn(nameof(EmailMessage.Subject)))
.MaximumLength(100).WithMessage(AppMessages.Validation.ValueLengthCannotBeGreaterThan.ParseIn(nameof(EmailMessage.Subject), 100));
RuleFor(model => model.Body)
.NotEmpty().WithMessage(AppMessages.Validation.ValueCannotBeNullOrEmpty.ParseIn(nameof(EmailMessage.Body)));
}
}
EmailMessage 和 EmailAddressList 类:
public class EmailMessage : IEmailMessage
{
public EmailAddressList To { get; set; } = new EmailAddressList();
public EmailAddressList Cc { get; set; } = new EmailAddressList();
public EmailAddressList Bcc { get; set; } = new EmailAddressList();
public string Subject { get; set; }
public string Body { get; set; }
}
public class EmailAddressList : ModelValidation, IEnumerable<EmailAddress>
{
public List<EmailAddress> Value { get; set; } = new List<EmailAddress>();
public EmailAddressList()
: base(new EmailAddressListValidator())
{
}
public EmailAddressList(string emailAddressList)
: base(new EmailAddressListValidator())
{
Value = Split(emailAddressList);
}
public EmailAddressList(IValidator validator)
: base(validator ?? new EmailAddressListValidator())
{
}
public List<EmailAddress> Split(string emailAddressList, char splitChar = ';')
{
return emailAddressList.Contains(splitChar)
? emailAddressList.Split(splitChar).Select(email => new EmailAddress(email)).ToList()
: new List<EmailAddress> { new EmailAddress(emailAddressList) };
}
public string ToString(char splitChar = ';')
{
if (Value == null)
return "";
var value = new StringBuilder();
foreach (var item in Value)
value.Append($"{item.Value};");
return value.ToString().TrimEnd(';');
}
public void Add(string emailAddress, string displayName = "")
{
Value.Add(new EmailAddress(emailAddress, displayName));
}
public void Add(EmailAddress emailAddress)
{
Value.Add(emailAddress);
}
public IEnumerator<EmailAddress> GetEnumerator()
{
return Value.GetEnumerator();
}
[ExcludeFromCodeCoverage]
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
这是 Jeremy Skinner 的回答...
This behaviour is normal and correct. Complex child validators set
with SetValidator can only be invoked if the target property is not
null. As the To property is null, the child validator won't be
invoked. What you should be doing is combining the SetValidator with a
NotNull check:
RuleFor(model => model.To) .NotNull().WithMessage("...");
.SetValidator(new EmailAddressListAtLeastOneRequiredValidator());
...and then remove the RuleFor(model => model).NotNull() from the
EmailAddressListAtLeastOneRequiredValidator as this will never be
executed. Overriding PreValidate is not relevant here. Using
PreValidate like this is only relevant when trying to pass a null to
the root validator. When working with child validators, they can never
be invoked with a null instance (which is by design), and the null
should be handled by the parent validator.
{Version 8.0.0}
为什么这个测试会通过?
测试:
[Test]
public void Validation_NullTo_ShouldThrowModelValidationException()
{
var config = new EmailMessage
{
Subject = "My Subject",
Body = "My Body",
To = null
};
EmailMessageValidator validator = new EmailMessageValidator();
ValidationResult results = validator.Validate(config);
if (!results.IsValid)
{
foreach (var failure in results.Errors)
{
Console.WriteLine("Property " + failure.PropertyName + " failed validation. Error was: " + failure.ErrorMessage);
}
}
// results.Errors is Empty and resuts.IsValid is true here. Should be false with at least one Error message.
Assert.True(results.IsValid);
}
如果我像这样更改消息的结构,验证会正常失败。
var config = new EmailMessage
{
Subject = "My Subject",
Body = "My Body"
};
config.To = new EmailAddressList();
验证者:
public class EmailAddressListAtLeastOneRequiredValidator : AbstractValidator<EmailAddressList>
{
public EmailAddressListAtLeastOneRequiredValidator()
{
RuleFor(model => model)
.NotNull()
.WithMessage(AppMessages.Validation.AtLeastOneShouldBeDefined.ParseIn(nameof(EmailAddressList.Value)));
RuleFor(model => model.Value)
.NotNull()
.WithMessage(AppMessages.Validation.AtLeastOneShouldBeDefined.ParseIn(nameof(EmailAddressList.Value)));
RuleFor(model => model.Value.Count)
.GreaterThan(0)
.WithMessage(AppMessages.Validation.AtLeastOneShouldBeDefined.ParseIn(nameof(EmailAddressList.Value)));
When(model => model.Value?.Count > 0, () =>
{
RuleFor(model => model.Value)
.NotEmpty()
.WithMessage(AppMessages.Validation.AtLeastOneShouldBeDefined.ParseIn(nameof(EmailAddressList.Value)));
});
}
}
public class EmailAddressListValidator : AbstractValidator<EmailAddressList>
{
public EmailAddressListValidator()
{
RuleFor(model => model.Value).SetCollectionValidator(new EmailAddressValidator());
}
}
public class EmailAddressValidator : AbstractValidator<EmailAddress>
{
public EmailAddressValidator()
{
When(model => model.Value != null, () =>
{
RuleFor(model => model.Value)
.EmailAddress()
.WithMessage(AppMessages.Validation.ValueCannotBeNullOrEmpty.ParseIn(nameof(EmailAddress)));
});
}
}
public class EmailMessageValidator : AbstractValidator<EmailMessage>
{
public EmailMessageValidator()
{
RuleFor(model => model.To).SetValidator(new EmailAddressListAtLeastOneRequiredValidator());
When(model => model.Cc?.Value?.Count > 0, () =>
{
RuleFor(model => model.Cc).SetValidator(new EmailAddressListValidator());
});
When(model => model.Bcc?.Value?.Count > 0, () =>
{
RuleFor(model => model.Bcc).SetValidator(new EmailAddressListValidator());
});
RuleFor(model => model.Subject)
.NotEmpty().WithMessage(AppMessages.Validation.ValueCannotBeNullOrEmpty.ParseIn(nameof(EmailMessage.Subject)))
.MaximumLength(100).WithMessage(AppMessages.Validation.ValueLengthCannotBeGreaterThan.ParseIn(nameof(EmailMessage.Subject), 100));
RuleFor(model => model.Body)
.NotEmpty().WithMessage(AppMessages.Validation.ValueCannotBeNullOrEmpty.ParseIn(nameof(EmailMessage.Body)));
}
}
EmailMessage 和 EmailAddressList 类:
public class EmailMessage : IEmailMessage
{
public EmailAddressList To { get; set; } = new EmailAddressList();
public EmailAddressList Cc { get; set; } = new EmailAddressList();
public EmailAddressList Bcc { get; set; } = new EmailAddressList();
public string Subject { get; set; }
public string Body { get; set; }
}
public class EmailAddressList : ModelValidation, IEnumerable<EmailAddress>
{
public List<EmailAddress> Value { get; set; } = new List<EmailAddress>();
public EmailAddressList()
: base(new EmailAddressListValidator())
{
}
public EmailAddressList(string emailAddressList)
: base(new EmailAddressListValidator())
{
Value = Split(emailAddressList);
}
public EmailAddressList(IValidator validator)
: base(validator ?? new EmailAddressListValidator())
{
}
public List<EmailAddress> Split(string emailAddressList, char splitChar = ';')
{
return emailAddressList.Contains(splitChar)
? emailAddressList.Split(splitChar).Select(email => new EmailAddress(email)).ToList()
: new List<EmailAddress> { new EmailAddress(emailAddressList) };
}
public string ToString(char splitChar = ';')
{
if (Value == null)
return "";
var value = new StringBuilder();
foreach (var item in Value)
value.Append($"{item.Value};");
return value.ToString().TrimEnd(';');
}
public void Add(string emailAddress, string displayName = "")
{
Value.Add(new EmailAddress(emailAddress, displayName));
}
public void Add(EmailAddress emailAddress)
{
Value.Add(emailAddress);
}
public IEnumerator<EmailAddress> GetEnumerator()
{
return Value.GetEnumerator();
}
[ExcludeFromCodeCoverage]
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
这是 Jeremy Skinner 的回答...
This behaviour is normal and correct. Complex child validators set with SetValidator can only be invoked if the target property is not null. As the To property is null, the child validator won't be invoked. What you should be doing is combining the SetValidator with a NotNull check:
RuleFor(model => model.To) .NotNull().WithMessage("...");
.SetValidator(new EmailAddressListAtLeastOneRequiredValidator());...and then remove the RuleFor(model => model).NotNull() from the EmailAddressListAtLeastOneRequiredValidator as this will never be executed. Overriding PreValidate is not relevant here. Using PreValidate like this is only relevant when trying to pass a null to the root validator. When working with child validators, they can never be invoked with a null instance (which is by design), and the null should be handled by the parent validator.