I've just started using FluentValidation v9.x and am wondering how to go about validating rules in a collection.
Basically if I have a collection of substances
public class Substance
{
public int? SubstanceId { get; set; }
public string SubstanceName { get; set; }
public decimal? SubstanceAmount { get; set; }
public int? SubstanceUnitId { get; set; }
public int? SubstanceRouteId { get; set; }
public DateTimeOffset? SubstanceTime { get; set; }
}
and I want to validate that:
I'm looking to send back one error message for each of the rules if they fail, not for each substance, which is what is happening now with the following:
RuleForEach(x => x.SubstanceList).SetValidator(new SubstanceValidator(RuleSetsToApply));
public class SubstanceValidator : AbstractValidator<Substance>
{
public SubstanceValidator(List<ValidationRule> RuleSetsToApply)
{
string ruleSetName = "SubstanceAmountUnit";
RuleSet(ruleSetName, () => {
RuleFor(x => x.SubstanceAmount).NotNull().NotEmpty();
RuleFor(x => x.SubstanceUnitId).NotNull().NotEmpty().GreaterThan(0);
});
ruleSetName = "SubstanceIngestion";
RuleSet(ruleSetName, () => {
RuleFor(x => x.SubstanceTime).NotNull().NotEmpty();
});
ruleSetName = "SubstanceRoute";
RuleSet(ruleSetName, () => {
RuleFor(x => x.SubstanceRouteId).NotNull().NotEmpty().GreaterThan(0);
});
}
}
So if I have five substances and
How can I accomplish this?
If I've understood the problem correctly, in this scenario I'd define the validation rules on the parent SubstanceList
rather than a Substance
entity validator.
For brevity I'm not considering your additional rule set logic as I don't know enough about it or whether it is actually required. However the following produces the three validation cases you have:
- if SubstanceAmount and SubstanceUnitId has values, and
- if SubstanceRouteId has a value, and
- if SubstanceTime has a value on any of the substance items.
RuleFor(x => x.SubstanceList)
.Must(x => x != null ? x.All(y => y.SubstanceAmount.HasValue && y.SubstanceUnitId.HasValue && y.SubstanceUnitId.Value <= 0) : true)
.WithMessage(x => "One or more substance amounts or unit ids has not been provided, and/or one or more unit ids is less than or equal to 0.");
RuleFor(x => x.SubstanceList)
.Must(x => x != null ? x.All(y => y.SubstanceTime.HasValue) : true)
.WithMessage(x => "One or more substance times has not been provided.");
RuleFor(x => x.SubstanceList)
.Must(x => x != null ? x.All(y => y.SubstanceRouteId.HasValue && y.SubstanceRouteId.HasValue && y.SubstanceRouteId.Value <= 0) : true)
.WithMessage(x => "One or more substance route ids has not been provided or is less than or equal to 0.");
As per your example scenario:
So if I have five substances and
- the first substance fails Rule #2,
- the third fails Rule #1 and #2 and
- the fourth fails on the Rule #3,
I would expect one error for each rule to be returned, even though Rule #2 has failed twice.
When I set up a sequence of Substances with the following conditions:
I get what I believe is the desired output:
Is there another way of doing it? The outcome is to basically distinct the error messages so yeah there are probably other approaches. One that springs to mind if you are invoking Validate
manually is to post process the validation results and ensure the error messages are distinct. I prefer the above approach, it feels more certain and it gives me the opportunity to provide a suitable error message.
Working LINQPad example:
void Main()
{
var fixture = new Fixture();
var substances = new List<Substance>();
substances.Add(fixture.Build<Substance>().Without(x => x.SubstanceTime).Create());
substances.Add(fixture.Create<Substance>());
substances.Add(fixture.Build<Substance>().Without(x => x.SubstanceAmount).Without(x => x.SubstanceTime).Create());
substances.Add(fixture.Build<Substance>().With(x => x.SubstanceRouteId, -1).Create());
substances.Add(fixture.Create<Substance>());
Console.WriteLine(substances);
var foo = new Foo() { SubstanceList = substances };
var validator = new FooValidator();
var validationResult = validator.Validate(foo);
Console.WriteLine(validationResult.Errors.Select(x => x.ErrorMessage));
}
public class Substance
{
public int? SubstanceId { get; set; }
public string SubstanceName { get; set; }
public decimal? SubstanceAmount { get; set; }
public int? SubstanceUnitId { get; set; }
public int? SubstanceRouteId { get; set; }
public DateTimeOffset? SubstanceTime { get; set; }
}
public class Foo
{
public List<Substance> SubstanceList { get; set; }
}
public class FooValidator : AbstractValidator<Foo>
{
public FooValidator()
{
RuleFor(x => x.SubstanceList)
.Must(x => x != null ? x.All(y => y.SubstanceAmount.HasValue && y.SubstanceUnitId.HasValue && y.SubstanceUnitId.Value <= 0) : true)
.WithMessage(x => "One or more substance amounts or unit ids has not been provided, and/or one or more unit ids is less than or equal to 0.");
RuleFor(x => x.SubstanceList)
.Must(x => x != null ? x.All(y => y.SubstanceTime.HasValue) : true)
.WithMessage(x => "One or more substance times has not been provided.");
RuleFor(x => x.SubstanceList)
.Must(x => x != null ? x.All(y => y.SubstanceRouteId.HasValue && y.SubstanceRouteId.HasValue && y.SubstanceRouteId.Value <= 0) : true)
.WithMessage(x => "One or more substance route ids has not been provided or is less than or equal to 0.");
}
}