Rogerio Schmitt
Rogerio Schmitt

Reputation: 1275

How to return a specific property in the message of a RuleForEach validation, using Fluent Validator?

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

Answers (2)

K0D4
K0D4

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

rgvlee
rgvlee

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.

Source

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");
            });
    }
}

ChildRules doco

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

Related Questions