Validate multiple properties with one message - fluentvalidation

I'm trying to validate a class that has three required properties.
If one or more of them is null it should trigger a single validation message.
Is there a idiomatic way to describe this in fluent validator?
I'm looking at dependant rules but the bottom of the documentation's page advises against using them.
Furthermore I still want to validate all three properties. I just don't want duplicate messages.
I noticed RuleSets, but these seem to serve a different purpose.
Alternatively I could create a validator specifically for these three options but without message and then chain the new validator in the original one. Then I think I can give that one a single message.
But that's a lot of ceremony for a system that is built around being readable.
So looking for a readable way to express validation for three fields with a single message as result.

There are 3 main ways you can do this with FluentValidation: Conditions, dependent rules or a custom rule.
Conditions
You can use 3 separate rule declarations with When conditions to ensure that you only ever get a single validation message.
RuleFor(x => x.Property1).NotNull()
.WithMessage("At least one is required");
RuleFor(x => x.Property2).NotNull()
.When(x => x.Property2 != null)
.WithMessage("At least one is required");
RuleFor(x => x.Property3).NotNull()
.When(x => x.Property1 != null && x.Property2 != null)
.WithMessage("At least one is required");
Dependent Rules
RuleFor(x => x.Property1).NotNull()
.WithMessage("At least one is required")
.DependentRules(() => {
RuleFor(x => x.Property2).NotNull()
.WithMessage("At least one is required")
.DependentRules(() => {
RuleFor(x => x.Property3).NotNull().WithMessage("At least one is required");
});
});
I don't particularly like this approach - I think it's hard to read (hence the warning in the documentation), but if you like this approach it'll work just fine.
Custom logic
RuleFor(x => x)
.Must(x => x.Property1 != null && x.Property2 != null && x.Property3 != null)
.WithMessage("At least one is required");
This approach is slightly different as it creates a model-level rule, so the error message will be associated with the entire model, rather than with a specific property.

Stop the validator when the first rule fails by setting the CascadeMode property:
public class MyClassValidator : AbstractValidator<MyClass>
{
public DestinationDeviceValidator()
{
this.CascadeMode = CascadeMode.Stop;
this.RuleFor(x => x.Property1)
.NotNull();
this.RuleFor(x => x.Property2)
.NotNull();
this.RuleFor(x => x.Property3)
.NotNull();
}
}

Related

Conditonal resource attributes

To install packages, I feed in data from Hiera into a for loop. Some packages require additional arguments. For packages that do not require an argument, I've set the value to undef, however, Chocolatey reads undef and complains.
How do I get the package resource to ignore the install_options attribute when it is blank or undef?
Hiera snippet:
profile::business::packages:
office365business:
version: latest
provider: chocolatey
arguments: ['/productid:O365BusinessRetail']
xmind:
version: latest
provider: chocolatey
arguments: undef
slack:
version: latest
provider: chocolatey
arguments: undef
Class example:
class profile::business(
Hash $packages,
){
if $::kernel == 'windows' {
$packages.each | $key, $value | {
package { "install_${key}" :
name => $key,
ensure => $value['version'],
provider => $value['provider'],
install_options => $value['arguments'],
notify => Reboot['after_profile_business'],
}
}
reboot { 'after_profile_business' :
apply => finished,
message => 'Reboot: Business profile applied.'
}
}
}
The best I can come up with is using an if clause to apply different instances of the package resource with or without install_options, depending on the value of arguments:
$packages.each | $key, $value | {
if $value['arguments'] != 'undef' {
package { "install_${key}" :
name => $key,
ensure => $value['version'],
provider => $value['provider'],
install_options => $value['arguments'],
notify => Reboot['after_profile_admin'],
}
} else {
package { "install_${key}" :
name => $key,
ensure => $value['version'],
provider => $value['provider'],
notify => Reboot['after_profile_admin'],
}
}
}
However, this seems rather clunky and I'm hoping someone might be able to show me a better way?
I've seen the Puppet Selector condition example, but I do not know if this will work for me.
T.I.A
This YAML fragment ...
arguments: undef
... sets the value of the 'arguments' key to the string 'undef'. That doesn;t mean the same thing on the Puppet side as the Puppet literal undef.
There are solutions. All of the best, IMO, revolve around representing absence of data via bona fide absence of data. That avoids any need for special reserved words. So suppose your data looked like this, instead:
profile::business::packages:
office365business:
version: latest
provider: chocolatey
arguments: ['/productid:O365BusinessRetail']
xmind:
version: latest
provider: chocolatey
slack:
version: latest
provider: chocolatey
Note that there is no entry bearing the arguments key where there are in fact no arguments to specify. If you have been rigorous and thorough about defining data types, then you may need to adjust your data type for these data to accommodate that, but so much the better because that would better describe the actual data semantics. That data modification probably resolves your issue by itself, because looking up a key that does not exist in a hash that does exist should yield undef (and there's also dig() if the undefinedness can occur at a higher level of a deep data structure).
Consider also, however, that Puppet has a shortcut for declaring that resource property values are drawn from a hash. That won't quite fit your present data because your keys are not the same as the needed property names, but you could either change the keys in your data or map them at the Puppet level. The latter might look like this:
# Defining the key / property name mappings here makes them clear, and is easy to
# change if you need to update the mappings
$mappings = { 'version' => 'ensure', 'arguments' => 'install_options' }
$packages.each |$package, $properties| {
# map the keys appearing in the data to Puppet property names, based on
# the hash defined above
$filtered_props = $properties.reduce({}) |$memo, $pair| {
$mapped_key = $pair[0] in $mappings ? { true => $mappings[$pair[0]], default => $pair[0] }
$memo + { $mapped_key => $pair[1] }
}
# one declaration covering all cases
package { "install_${package}" :
name => $package,
provider => $value['provider'],
notify => Reboot['after_profile_admin'],
* => $filtered_props,
}
}

Fluent Validator for when another property is true

I am trying to use FluentValidation to validate the property 'Username' if another property 'Found' is true.
Object Contains:
string Username
bool Found
RuleFor(x => x.Username)
.NotEmpty().DependentRules(() => {
RuleFor(y => y.Found).Equals(true); //Not valid syntax
})
.WithMessage("Not Found");
Unfortunately, there seems to be no easy way to do this?
Use the When clause.
RuleFor(x => x.Username).NotEmpty().When(x => x.Found);
Working example
The dependent rules is a bit different; basically the rules specified in the dependent rules block will only be tested if the rule they're attached to passes.
As per the doco
RuleFor(x => x.Surname).NotNull().DependentRules(() => {
RuleFor(x => x.Forename).NotNull();
});
Here the rule against Forename will only be run if the Surname rule passes.

CascadeMode StopOnFirstFailure doesn't work

public class Validator : AbstractValidator<Query>
{
public Validator()
{
CascadeMode = CascadeMode.StopOnFirstFailure;
RuleFor(x => x.A).NotEmpty();
RuleFor(x => x.B).NotEmpty();
RuleFor(x => x).MustAsync(...);
}
}
I'like to construct validator which dones't call MustAsync when above rules are not met. Unfortunately settings CascadeMode to StopOnFirstFailure in validator doesn't do the work.
As stated by the author
That's the correct behaviour - CascadeMode only affects validators
within the same rule chain. Independent calls to RuleFor are separate,
and not dependent on the success or failure of other rules.
See this.
So it would apply for this case
Rulefor(x => x.A)
.NotEmpty()
.Length(10);
=> the Length validation would only be applied if A is not empty.
So you'll have to use a When extension in your MustAsync rule, checking if A and B are not empty (or an if around this rule).

Using FluentValidation's WithMessage method with parameters with a WarningMessage

I am new at FluentValidation in general. I am writing a validator, and I can't seem to figure out how to do a .WithMessage with a WarningMessage instead of an ErrorMessage and use params.
I can do this:
RuleFor(x => x.Endorsement)
.Must((coverage, endorsement) => HaveCoveragePerAcreOverMinimum(_coverage, coverage))
.When(x => (!HaveSpecialRequest(_coverage) && !HavePermissionsToOverrideLimits()))
.WithMessage("Some error message {0}", x => x.MyError);
But that sets it as an ErrorMessage and I need a Warning Message. I tried this but no dice:
RuleFor(x => x.Endorsement)
.Must((coverage, endorsement) => HaveCoveragePerAcreOverMinimum(_coverage, coverage))
.When(x => (!HaveSpecialRequest(_coverage) && !HavePermissionsToOverrideLimits()))
.WithMessage(new WarningMessage("Some warning message {0}", x => x.MyError));
There's no direct implementation of Warning message in FluentValidation (or Mvc's ModelState).
In FluentValidation, you've got a
WithState() extension method that you can use for this purpose.
First, you can create an enum
public enum ValidationErrorLevel
{
Error,
Warning
}
Then, you can write a few extension methods, in a static class, to use warnings and errors.
One to use in your Validator classes
public static IRuleBuilderOptions<T, TProperty> AsWarning<T, TProperty>(this IRuleBuilderOptions<T, TProperty> rule)
{
return rule.WithState<T, TProperty>(x => ValidationErrorLevel.Warning);
}
You can use it this way
RuleFor(x => x.Endorsement)
.Must((coverage, endorsement) => HaveCoveragePerAcreOverMinimum(_coverage, coverage))
.When(x => (!HaveSpecialRequest(_coverage) && !HavePermissionsToOverrideLimits()))
.WithMessage("Some error message {0}", x => x.MyError)
.AsWarning();
Few other to use to manage your validation results
public static IList<ValidationFailure> GetWarnings(this ValidationResult result)
{
return result.Errors.Where(m => m.CustomState != null && Convert.ToInt32(m.CustomState) == (int)ValidationErrorLevel.Warning).ToList();
}
public static IList<ValidationFailure> GetErrors(this ValidationResult result)
{
return result.Errors.Except(result.GetWarnings()).ToList();
}
Then, you should use
validator.Validate(<someclass>)
instead of
validator.ValidateAndThrow(<someclass>)
var results = validator.Validate(<someclass>);
You can then put errors in ModelState, for example
foreach (var error in result.GetErrors()) {
ModelState.AddModelError(error.PropertyName, error.ErrorMessage);
}
and do something else for Warnings, for example put it in TempData
TempData["warnings"] = new List<string>();
((List<string>)TempData[WarningMessageKey]).AddRange(result.GetWarnings().Select(warning => warning.ErrorMessage));
Then you can display them like any other TempData.
Validation (in general) attaches errors to the ModelState - which in itself is an object so if you put a break point on the if(ModelState.IsValid) line you can look at the other properties of the ModelState.
The thing with errors (and error messages) they are either there or not. If there is no issue there is no error message and if there is an issue an error message will be added.
If I were you, if the model state is not valid in your controller I would get all error messages using
var allErrors = ModelState.Values.SelectMany(v => v.Errors);
and then iterate over each one and decide what you want to do with it.
I hope this answers or at least helps as I am not totally sure what you are asking / trying to get to.

Raven Index creation gets compilation error CS1977 when using FindLastIndex

I'm trying to write an index that looks at our logs to find entities that are unavailable due to undergoing a part of the process but not a second part of the process which then makes them available. Our import process happens first, then our clean process. Entities that have gone through our clean process are then considered available. I want to essentially find out those entities that have undergone the import process (therefore resetting them) after the last time the cleaning occurred.
I came up with this potential map:
AddMap<EntityLog>(docs => docs.Where(doc => doc.Details != null)
.Where(doc => doc.Details.Any(d => d.QueueMessage != null && d.QueueMessage.JobsToDo.Contains(JobType.Import)
&& d.Finished != null))
.Where(doc => doc.Details.Any(d => d.QueueMessage != null && d.QueueMessage.JobsToDo.Contains(JobType.Clean)
&& d.Finished != null))
.Where(doc => doc.Details.FindLastIndex(d => d.QueueMessage.JobsToDo.Contains(JobType.Clean) && d.Finished != null) <
doc.Details.FindLastIndex(d => d.QueueMessage.JobsToDo.Contains(JobType.Import) && d.Finished != null))
.Select(found => new
{
found.EntityID,
User = found.Details.Select(d => d.User)
}));
It compiles in VS, but when I try to make this index, I get the following error:
CS1977 "Cannot use a lambda expression as an argument to a dynamically dispatched operation without first casting it to a delegate or expression tree type"
From some selective commenting, it seems to be the line where I'm using FindLastIndex. And from research, I know that this compiler error comes in when you're trying to do stuff with untyped objects that you're not supposed to. From looking at the output Raven-made code, it uses dynamic a lot, so I'm guessing its something related to that. I did try using this as an alternative to that line:
.Where(doc => Array.FindLastIndex(doc.Details.ToArray(), d => d.QueueMessage.JobsToDo.Contains(JobType.Clean) && d.Finished != null) <
Array.FindLastIndex(doc.Details.ToArray(), d => d.QueueMessage.JobsToDo.Contains(JobType.Import) && d.Finished != null))
But no joy, I still get the same error message.
How do I get this code to work? Or do I need to take a completely different approach?
Any help appreciated.
FindLastIndex is a method on List, but we don't know what the type is on the server side, so that isn't available.
Use the extension method on IEnumerable, instead.
Just a create a get only property on your EntityLog that uses the FindLastIndex then the results of that information is what will be serialized to the database and easy to search upon.
This is a rather common pattern I follow when working with Lists.
public class Foo {
public List<Phones> Phones { get;set; }
public Phone PrimaryPhone {
get { return Phones.SingleOrDefault(x=> x.Primary == true)) }
}
}
Note, if EntityLog is an already existing document collection, you will need to Load each log and save it back to the database.