Reputation: 2646
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?
Upvotes: 2
Views: 2716
Reputation: 47
The Validate.Errors result is very easy to select/query from using LINQ. I try to keep my validation rules very cut/dry.
For instance, I have the following classes:
[Serializable]
[XmlRoot(ElementName ="Config")]
public class Config
{
[XmlElement("Machine")]
public required Machine[] Machine { get; set; }
}
public class Machine
{
[XmlElement]
public required string Name { get; set; }
[XmlElement]
public required string IPAddress { get; set; }
[XmlElement]
public int Port { get; set; }
[XmlElement]
public required string Database { get; set; }
public override string ToString()
{
return Name + " - " + IPAddress + " : " + Port;
}
}
public class ConfigValidator : AbstractValidator<Config>
{
public ConfigValidator()
{
RuleFor(config => config.Machine)
.NotNull()
.NotEmpty()
.Must(x => x.Length > 0);
RuleForEach(x => x.Machine)
.SetValidator(new MachineValidator());
}
}
public class MachineValidator : AbstractValidator<Machine>
{
public MachineValidator()
{
RuleFor(machine => machine.Name)
.NotNull()
.NotEmpty();
RuleFor(machine => machine.IPAddress)
.NotNull()
.NotEmpty()
.Matches("^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$").WithMessage("IPAddress format must match ###.###.###.### (ex: 127.0.0.1).");
RuleFor(machine => machine.Port)
.NotNull()
.NotEmpty()
.LessThan(10000).WithMessage("Port numbers can only be 4 digits long and greater than 0.")
.GreaterThan(0);
RuleFor(machine => machine.Database)
.NotNull()
.NotEmpty();
}
}
You can then use a small method (or single line of code) to get the distinct errors as a string. I use this to create a single string (with breaks) I can add to an alert popup message:
public static string GetConfigErrors(Config config)
{
var configErrors = ConfigValidator.Validate(config).Errors.Count > 0
? string.Join( Environment.NewLine, ConfigValidator.Validate(config).Errors.Select(x => x.ErrorMessage).Distinct() )
: string.Empty;
return configErrors;
}
It removes any duplicates from the the error message list. For instance, if there are multiple bad IPAdresses or multiple bad Port values - I'll only see a single error message re: IPAdress and a single error message re: Port. The WithMessage extension is nice, because you can make it more informative than what the generic output would otherwise be. You can even use conditions to switch which Validator or which Rule is used to validate your property.
Upvotes: 0
Reputation: 3213
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.");
}
}
Upvotes: 2