FluentValidation: validate only one property is set
Asked Answered
O

4

8

I'm struggling with implementing a validator for a class, where only one property should be set.

Let's say we have the following class:

public class SomeClass
{
    public DateTime SomeDate {get; set;}
    public IEnumerable<int> FirstOptionalProperty {get; set;}
    public IEnumerable<int> SecondOptionalProperty {get; set;}
    public IEnumerable<int> ThirdOptionalProperty {get; set;}
}

This class has one mandatory property - SomeDate. Other properties are optional and only one can be set e.g if FirstOptionalProperty is set - SecondOptionalProperty and ThirdOptionalProperty should be null, if SecondOptionalProperty is set - FirstOptionalProperty and ThirdOptionalProperty should be null and so forth.

In other words: if one of IEnumerable props is set - other IEnumerables should be null.

Any tips/ideas on implementing validator for such type of class? The only thing I came up with is writing chunks of When rules, but this way of writing code is error prone and result looks ugly.

Oleomargarine answered 24/3, 2021 at 15:24 Comment(2)
so it boils down basically that only 1 of the 3 opional properties is allowed to NotBeNull ? Did I understand you correctly?Sort
@MongZhu If one of those properties set - others should be nullOleomargarine
J
7

You can take advantage of the Must overload that gives you access to the whole class object so that you can do a property validation against other properties. See FluentValidation rule for multiple properties for more details.

public class SomeClassValidator : AbstractValidator<SomeClass>
{
    private const string OneOptionalPropertyMessage = "Only one of FirstOptionalProperty, SecondOptionalProperty, or ThirdOptionalProperty can be set.";

    public SomeClassValidator()
    {
        RuleFor(x => x.FirstOptionalProperty)
            .Must(OptionalPropertiesAreValid)
            .WithMessage(OneOptionalPropertyMessage);

        RuleFor(x => x.SecondOptionalProperty)
            .Must(OptionalPropertiesAreValid)
            .WithMessage(OneOptionalPropertyMessage);

        RuleFor(x => x.ThirdOptionalProperty)
            .Must(OptionalPropertiesAreValid)
            .WithMessage(OneOptionalPropertyMessage);
    }

    // this "break out" method only works because all of the optional properties
    // in the class are of the same type. You'll need to move the logic back
    // inline in the Must if that's not the case.
    private bool OptionalPropertiesAreValid(SomeClass obj, IEnumerable<int> prop)
    {
        // "obj" is the important parameter here - it's the class instance.
        // not going to use "prop" parameter.

        // if they are all null, that's fine
        if (obj.FirstOptionalProperty is null && 
            obj.SecondOptionalProperty is null && 
            obj.ThirdOptionalProperty is null)
        {
            return true;
        }

        // else, check that exactly 1 of them is not null
        return new [] 
        { 
            obj.FirstOptionalProperty is not null,
            obj.SecondOptionalProperty is not null, 
            obj.ThirdOptionalProperty is not null
        }
        .Count(x => x == true) == 1;
        // yes, the "== true" is not needed, I think it looks better
    }
}

You can tweak the check function. As this currently stands, if you set 2 or more of the optional properties, ALL of them will throw an error. That may or may not be fine for your needs.

You could also make a RuleFor ONLY for the first optional property, rather than all of them, as all the properties will be executing the same IsValid code and return the same message, your user might just get a bit confused if they get an error message for OptionalProperty1 but they didn't supply that one.

The downside to this approach is that you need to know at compile time what all your properties are (so you can write the code for it), and you need to maintain this validator if you add/remove optional entries. This downside may or may not be important to you.

Juniper answered 24/3, 2021 at 15:42 Comment(4)
Thanks for the solution idea, I'll use thatOleomargarine
you cannot use XOR for checking if a single property is set only if an odd number of properties are set... here's a related question #14888674Toxicogenic
@JasonC thanks for that. I thought I did testing back when I posted this, but reading your link and doing some more investigation proved me wrong. I've edited my post, and I'll give you an upvote as well.Juniper
You could easily eliminate the first null checks by checking the count is less than 1... .Count(x => x == true) < 1;Toxicogenic
S
2

one thing that comes to my mind is to use reflection here:

SomeClass someClass = new SomeClass
{
    SomeDate = DateTime.Now,
    FirstOptionalProperty = new List<int>(),
    //SecondOptionalProperty = new List<int>() // releasing this breakes the test
};

var info = typeof(SomeClass).GetProperties()
                            .SingleOrDefault(x =>
                                 x.PropertyType != typeof(DateTime) &&
                                 x.GetValue(someClass) != null);

basically if info is null then more than 1 of the optional properties was instantiated

Sort answered 24/3, 2021 at 15:46 Comment(2)
Thought about that, but didn't know how to access instance of class in validator. Thanks to @Juniper 's answer now I know how to get class instance. And since IEnumerable properties are of different types in my case (I simplified those to the same type in the question, but in real case they aren't the same) I guess I'll combine both of your answers. I appreciate your help, thanks.Oleomargarine
@AnonAnon if you combine both it would be nice to see the outcome when you have solved your problem. Please post it then :) The exclusion criteria for the mandatory property is variable, you could also use the name of it if you would have 2 properties with e.g. DateTime type ;)Sort
T
2

I would use a helper function for this.

private static bool OnlyOneNotNull(params object[] properties) =>
    properties.Count(p => p is not null) == 1;

You would use it like this.

SomeClass s = new SomeClass();
/* ... */

if(!OnlyOneNotNull(s.FirstOptionalProperty, s.SecondOptionalProperty, s.ThirdOptionalProperty))
{
    /* handle error case */
}
Toxicogenic answered 18/5, 2021 at 21:7 Comment(1)
The downside of this approach is that the values will be boxed.Knob
E
0

Here's a modified example of @gunr2171 solution

It avoids the downsides they mentioned while still using FluentValidation.

public class SomeClassValidator : AbstractValidator<SomeClass>
{
    private const string OneOptionalPropertyMessage = "Only one of FirstOptionalProperty, SecondOptionalProperty, or ThirdOptionalProperty can be set.";

    public SomeClassValidator()
    {
        // This ends up passing the entire SomeClass object to the function twice...
        // It's kind of a hack but it works.
        RuleFor(x => x)
            .Must(OptionalPropertiesAreValid)
            .WithName(nameof(SomeClass))
            .WithErrorCode($"{nameof(OptionalPropertiesAreValid)}Validator")
            .WithMessage(OneOptionalPropertyMessage);
    }

    // this "break out" method only works because all of the optional properties
    // in the class are of the OptionalPropertiesAreValidsame type. You'll need to move the logic back
    // inline in the Must if that's not the case.
    private bool OptionalPropertiesAreValid(SomeClass obj, SomeClass redundantObj)
    {
        // "obj" is the important parameter here - it's the class instance.
        // not going to use "prop" parameter.

        // if they are all null, that's fine
        if (obj.FirstOptionalProperty is null && 
            obj.SecondOptionalProperty is null && 
            obj.ThirdOptionalProperty is null)
        {
            return true;
        }

        // else, check that exactly 1 of them is not null
        return new [] 
        { 
            obj.FirstOptionalProperty is not null,
            obj.SecondOptionalProperty is not null, 
            obj.ThirdOptionalProperty is not null
        }
        .Count(x => x == true) == 1;
        // yes, the "== true" is not needed, I think it looks better
    }
}
Entropy answered 27/2, 2024 at 20:46 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.