Anon Anon
Anon Anon

Reputation: 349

FluentValidation: validate only one property is set

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.

Upvotes: 8

Views: 3844

Answers (4)

Nick M
Nick M

Reputation: 61

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
    }
}

Upvotes: 0

gunr2171
gunr2171

Reputation: 17520

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.

Upvotes: 7

Jason C
Jason C

Reputation: 152

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 */
}

Upvotes: 2

Mong Zhu
Mong Zhu

Reputation: 23732

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

Upvotes: 2

Related Questions