Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 976
Anon Anon Avatar asked Mar 24 '21 15:03

Anon Anon


2 Answers

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.

like image 186
gunr2171 Avatar answered Sep 20 '22 18:09

gunr2171


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

like image 38
Mong Zhu Avatar answered Sep 17 '22 18:09

Mong Zhu