Validation is throwing "Unable to cast object of type 'BaseModel' to type 'DerivedModel'."

2

I'm seeing a strange issue with FluentValidation (v8.2.0) when I try to compose validators:

System.InvalidCastException HResult=0x80004002 Message=Unable to cast object of type 'BaseModel' to type 'DerivedModel'. Source=FluentValidation StackTrace: at FluentValidation.Internal.ConditionBuilder1.<>c__DisplayClass2_0.<When>g__Condition|0(ValidationContext context) in C:\Projects\FluentValidation\src\FluentValidation\Internal\ConditionBuilder.cs:line 62 at FluentValidation.Internal.PropertyRule.<Validate>d__67.MoveNext() in C:\Projects\FluentValidation\src\FluentValidation\Internal\PropertyRule.cs:line 270 at System.Linq.Enumerable.SelectManySingleSelectorIterator2.MoveNext() at System.Linq.Enumerable.WhereEnumerableIterator1.MoveNext() at FluentValidation.AbstractValidator1.Validate(ValidationContext`1 context) in C:\Projects\FluentValidation\src\FluentValidation\AbstractValidator.cs:line 115 at TestApp.Program.d__4.MoveNext() in C:\Users\john\Documents\Visual Studio 2017\Projects\TestApp\TestApp\Program.cs:line 76 at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at TestApp.Program.(String[] args)

My model and validators:

public class BaseModel
{
    public string Name { get; set; }
    public bool IsAlive { get; set; }
}

public class DerivedModel : BaseModel
{
    public int Age { get; set; }
}

public class BaseModelValidator : AbstractValidator<BaseModel>
{
    public BaseModelValidator()
    {
        RuleFor(o => o.Name).Length(1, 20);
    }
}

public class DerivedModelValidator : AbstractValidator<DerivedModel>
{
    public DerivedModelValidator(BaseModelValidator baseValidator)
    {
        foreach (var rule in baseValidator)
        {
            AddRule(rule);
        }

        RuleFor(o => o.Age).GreaterThanOrEqualTo(0);
    }
}

The code I'm using:

var baseModelValidator = new BaseModelValidator();
var derivedModelValidator = new DerivedModelValidator(baseModelValidator);

var baseModel = new BaseModel
{
    IsAlive = true,
    Name = "test2"
};
Console.WriteLine(baseModelValidator.Validate(baseModel).IsValid);

As you can see, I'm using the BaseModelValidator to validate BaseModel, and nowhere does this reference DerivedModel.

Funnily enough, if I remove the line var derivedModelValidator = new DerivedModelValidator(baseModelValidator);, it works without issue.

What is causing this exception and how to I resolve it?

c#
fluentvalidation

1 Answer

2

I was actually seeing this issue sporadically in my web application - 99% of the time it would work fine, but once in a while I would get this issue. I reached out to Jeremy Skinner, the author or FluentValidation, and he explained what's happening:

Rules are intrinsicly tied to the validators that define them. They are not designed to be copied from one validator to another. They are inherently tied to the validator that defined them, and the type they were defined against.

Each condition block has a unique ID associated with it (that allows the result of the condition to be cached, so it's only executed one and not for each rule inside it). When you copy rules from one validator to another, the condition is brought across as well.

In short: you can't share a single rule object between validators.

The offending piece of code is this block from DerivedModelValidator:

foreach (var rule in baseValidator)
{
    AddRule(rule);
}

Jeremy offered two different solutions to this problem:

1. Derive both validator classes from a common base validator class that contains the shared rules:

public abstract class CommonModelValidator<T> : AbstractValidator<T> where T : BaseModel
{
    protected CommonModelValidator()
    {
        RuleFor(o => o.Name).Length(1, 20);
    }
}

public class BaseModelValidator : CommonModelValidator<BaseModel>
{
}

public class DerivedModelValidator : CommonModelValidator<DerivedModel>
{
    public DerivedModelValidator(BaseModelValidator baseValidator) 
        : base()
    {
        RuleFor(o => o.Age).GreaterThanOrEqualTo(0);
    }
}

2. Compose DerivedModelValidator from BaseModelValidator using SetValidator:

public class BaseModelValidator : AbstractValidator<BaseModel>
{
    public BaseModelValidator()
    {
        RuleFor(o => o.Name).Length(1, 20);
    }
}

public class DerivedModelValidator : AbstractValidator<DerivedModel>
{
    public DerivedModelValidator(BaseModelValidator baseValidator)
    {
        RuleFor(o => o).SetValidator(baseValidator);
        RuleFor(o => o.Age).GreaterThanOrEqualTo(0);
    }
}

User contributions licensed under CC BY-SA 3.0