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.ConditionBuilder
1.<>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.SelectManySingleSelectorIterator
2.MoveNext() at System.Linq.Enumerable.WhereEnumerableIterator1.MoveNext() at FluentValidation.AbstractValidator
1.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)
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);
}
}
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?
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:
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);
}
}
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