Reputation: 1275
Let's say I have a test class like this:
public class TestClass
{
public Properties[] TestProperties { get; set; }
public Guid Id { get; set; }
public TestClass(Properties[] testProperties)
{
Id = Guid.NewGuid();
TestProperties = testProperties;
}
}
And a Properties class as follows:
public class Properties
{
public Guid Id { get; set; }
public string Name { get; set; }
public Properties(string name)
{
Name = name;
Id = Guid.NewGuid();
}
}
I need to validate that none of my properties Name
at the TestProperties
array is null, like this:
public class TestValidator : AbstractValidator<TestClass>
{
public TestValidator()
{
RuleForEach(x => x.TestProperties)
.Must(y => y.Name != string.Empty && y.Name != null)
.WithMessage("TestPropertie at {CollectionIndex}, can't be null or empty");
}
}
But instead of returning the position of the failing property, at the validation message, I would like to return it's Id
, how can I do so?
Upvotes: 3
Views: 3908
Reputation: 2603
I approached this a little differently, because I wanted a more reusable solution. (I'm validating many different classes in similar ways). Putting the message identification inside the extension with Must<> ties you to the type and could lead to copy&paste. Instead, I pass as an argument to the validation, a Func that returns an identifying string and lets the caller decide how to identify the object being validated.
public static IRuleBuilderOptions<T, string> IsValidStringEnumAllowNullable<T>(this IRuleBuilder<T, string> ruleBuilder, IList<string> validValues, Func<T,string> identifierLookup)
{
return ruleBuilder.Must((rootObject, testValue, context) =>
{
context.MessageFormatter.AppendArgument("AllowableValues", string.Join(", ", validValues));
context.MessageFormatter.AppendArgument("Identifier", identifierLookup(rootObject));
return string.IsNullOrEmpty(testValue) || validValues.Contains(testValue, StringComparer.Ordinal);
}).WithMessage("{Identifier}{PropertyName} with value {PropertyValue} must be one of the allowable values: {AllowableValues}, or null or empty string");
}
And then the calling code where I tell the specific validation message 'how' to identify the object for messaging:
base.RuleForEach(rq => rq.Thingies).ChildRules(x =>
{
x.RuleFor(f => f.MyProperty).IsValidStringEnumAllowNullable(ValidationStrings.AnArrayOfAllowedValues, f => $"Thing[{f.Id}] ");
});
The result of this code is
Thing[1234] My Property with value asdf must be one of the allowable values: Value1, ValidValue2, Somethingelse, or null or empty string
Upvotes: 0
Reputation: 3193
Yes, using the default validators it's possible to inject other property values from the objects into the message.
This can be done by using the overload of WithMessage that takes a lambda expression, and then passing the values to string.Format or by using string interpolation.
There are a couple of ways you can do it. Firstly, as per your current implementation using Must
:
public class TestClassValidator : AbstractValidator<TestClass>
{
public TestClassValidator()
{
RuleForEach(x => x.TestProperties)
.Must(y => !string.IsNullOrEmpty(y.Name))
.WithMessage((testClass, testProperty) => $"TestProperty {testProperty.Id} name can't be null or empty");
}
}
I try to avoid using Must
when possible, if you stick to using the built-in validators you stand a better chance of client-side validation working out of the box (if you're using it in a web app). Using ChildRules
allows you to use the built-in validators and also get the benefit of using the fluent interface:
public class TestClassValidator : AbstractValidator<TestClass>
{
public TestClassValidator()
{
RuleForEach(x => x.TestProperties)
.ChildRules(testProperties =>
{
testProperties.RuleFor(testProperty => testProperty.Name)
.NotNull()
.NotEmpty()
.WithMessage(testProperty => $"TestProperty {testProperty.Id} name can't be null or empty");
});
}
}
I've included the NotNull() validator for verbosity/alignment with the custom error message, however it's not needed as NotEmpty() will cover the null or empty case.
Finally if it was me I'd probably create a separate validator for the Properties
type (should this be Property
?) and use SetValidator
to include it. Splits up the validation concerns, defines the validation for a type once and makes the rules reusable, and makes the validators easier to test. I'm not going to cover that here as that feels beyond the scope of this question but the links below give examples on how to do it.
Child validator doco (SetValidator
usage) here and here
Working samples of the above including tests can be found here.
Upvotes: 4