1

Closed

SetValidator with When not work valid in MVC 4

description

I have models and validators:
[Validator(typeof(AdressValidator))]
    public class Adress {
        public string PostalCode { get; set; }
        public string Street { get; set; }
    }

    [Validator(typeof(PersonValidator))]
    public class Person
    {
        public Adress Adress { get; set; }
        public bool IsAdress2 { get; set; }
        public Adress Adress2 { get; set; }
    }

    public class AdressValidator : AbstractValidator<Adress> {
        public AdressValidator() {
            RuleFor(p => p.PostalCode).NotEmpty();
            RuleFor(p => p.Street).NotEmpty();
        }
    }

    public class PersonValidator : AbstractValidator<Person>
    {
        public PersonValidator()
        {
            RuleFor(p => p.Adress).SetValidator(new AdressValidator());
            RuleFor(p => p.Adress2).SetValidator(new AdressValidator()).When(p=>p.IsAdress2);
        }
    }
And controlers:
 public class HomeController : Controller
    {
        //
        // GET: /Home/

        public ActionResult Index() {
            Person person = new Person() {
                Adress = new Adress() {
                    PostalCode = "12-100",
                    Street = "Street 13"
                },
                IsAdress2 = false,
                Adress2 = new Adress()
            };

            return View(person);
        }

        [HttpPost]
        public ActionResult Index(Person person)
        {
            if (ModelState.IsValid) {
                return RedirectToAction("OK");
            }
            return View(person);
        }

        public ActionResult Ok() {
            return View();
        }
    }
Adress2 is always validated.

In Attachments is solution without lib and packages.

file attachments

Closed Apr 29, 2013 at 12:43 PM by JeremyS

comments

wojtpl2 wrote Apr 28, 2013 at 6:42 AM

I create TestCase in ModelBinderTester.cs:
        [Validator(typeof(TestModel7Validator))]
        public class TestModel7 {
            public TestModel Model { get; set; }
            public bool IsModel2 { get; set; }
            public TestModel Model2 { get; set; } 
        }

        public class TestModel7Validator : AbstractValidator<TestModel7>
        {
            public TestModel7Validator()
            {
                RuleFor(p => p.Model).SetValidator(new TestModelValidator());
                RuleFor(p => p.Model2).SetValidator(new TestModelValidator()).When(p => p.IsModel2);
            }
        }

        [Test]
        public void Should_not_valid_complex_property_with_when_is_false() {
            var form = new FormCollection() {
                {"Model.Name", "Name"}, 
                {"IsModel2", "false"},
                {"Model2.Name", null}
            };
            var modelstate = new ModelStateDictionary();

            var firstContext = new ModelBindingContext
            {
                //ModelName = "first",
                ModelMetadata = CreateMetaData(typeof(TestModel7)),
                ModelState = modelstate,
                FallbackToEmptyPrefix = true,
                ValueProvider = form.ToValueProvider(),
            };

            binder.BindModel(controllerContext, firstContext);

            modelstate.IsValidField("Model.Name").ShouldBeTrue();
            modelstate.IsValidField("IsModel2").ShouldBeTrue();
            modelstate.IsValidField("Model2.Name").ShouldBeTrue();
            modelstate.IsValid.ShouldBeTrue();
        }

wojtpl2 wrote Apr 29, 2013 at 6:37 AM

I have a workaround. If IsModel2 is false, you can not send variables from model2:
var form = new FormCollection() {
            {"Model.Name", "Name"}, 
            {"IsModel2", "false"}
            /*,{"Model2.Name", null}*/
        };

JeremyS wrote Apr 29, 2013 at 9:07 AM

Hi

So the issue here is that you're using the [Validator] attribute incorrectly.

In your code, you're using both SetValidator and the ValidatorAttribute, which will cause each Address property to be validated twice.

When you use the [Validator] attribute, this tells MVC to instantiate an AddressValidator every time it encouters a property of type Address. In your case, this isn't what you want, because you're also explicitly defining the AddressValidator in the SetValidator call. So MVC executes validation for each Address property twice:
  • When MVC's model binding process encouters the Address property, it will create an execute an AddressValidator, completely independently of the PersonValidator (so the .When clause is completely ignored, as the PersonValidator hasn't event been invoked yet)
  • MVC will then ask FluentValidation to create and invoke the PersonValidator, which this time will take the When clause into account.
Solution: Remove the [Validator] attribute from the Address class. You only want to put the attribute on the class that is the top level in the hierarchy, (in this case, Person).

wojtpl2 wrote Apr 29, 2013 at 10:21 AM

Thanks for your help.

But in my project I have Validator Factory with an Inversion of Control Container. I haven't manual add [Validator] attribute. In this case, can't be done so easily. But I have idea what to do.

I use AutoFrac.
public class AutoFacValidatorFactory : IValidatorFactory
    {
        private readonly IComponentContext _container;

        public AutoFacValidatorFactory(IComponentContext provider)
        {
            _container = provider;
        }

        public IValidator<T> GetValidator<T>()
        {
            return (IValidator<T>)GetValidator(typeof(T));
        }

        public IValidator GetValidator(Type type)
        {
            var genericType = typeof(IValidator<>).MakeGenericType(type);
            object validator;
            if (_container.TryResolve(genericType, out validator))
                return (IValidator)validator;
            return null;
        }
    }
    public static class FluentValidationConfig
    {
        public static void BootstrapFluentValidation()
        {
            var builder = new ContainerBuilder();
            builder.RegisterType<AutoFacValidatorFactory>().As<IValidatorFactory>().SingleInstance();
            AssemblyScanner.FindValidatorsInAssembly(typeof (PojazdModelValidator).Assembly)
                           .ForEach(result => builder.RegisterType(result.ValidatorType).As(result.InterfaceType).SingleInstance()); 

            var container = builder.Build();
            ModelValidatorProviders.Providers.Add(new FluentValidationModelValidatorProvider(container.Resolve<IValidatorFactory>()));
            DataAnnotationsModelValidatorProvider.AddImplicitRequiredAttributeForValueTypes = false;
        }
    }

JeremyS wrote Apr 29, 2013 at 10:41 AM

If you're using an IoC container, rather than relying on auto-registration I'd suggest that you explicitly register only those validators that you require to be automatically validated (ie the top-level validators).

wojtpl2 wrote Apr 29, 2013 at 11:49 AM

ok. Maybe can you add something like [NotAssemblyScaner] attribute?

wojtpl2 wrote Apr 29, 2013 at 12:04 PM

   public class NotAssemblyScanner : Attribute {
        
    }
in AssemblyScanner:
        private IEnumerable<AssemblyScanResult> Execute() {
            var openGenericType = typeof(IValidator<>);

            var query = from type in types
                        let interfaces = type.GetInterfaces()
                        let genericInterfaces = interfaces.Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == openGenericType)
                        let matchingInterface = genericInterfaces.FirstOrDefault()
                        where matchingInterface != null __&& !type.GetCustomAttributes(typeof(NotAssemblyScanner), false).Any()__
                        select new AssemblyScanResult(matchingInterface, type);

            return query;
        }
in AssenblyScannerTest:

region License

// Copyright (c) Jeremy Skinner (http://www.jeremyskinner.co.uk)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// The latest version of this file can be found at http://www.codeplex.com/FluentValidation

endregion

namespace FluentValidation.Tests {
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;

[TestFixture]
public class AssemblyScannerTester {

    [Test]
    public void Finds_validators_for_types() {
        var scanner = new AssemblyScanner(new[] { typeof(Model1Validator), typeof(Model2Validator)__, typeof(Model3Validator)__ });
        var results = scanner.ToList();

        results[0].ValidatorType.ShouldEqual(typeof(Model1Validator));
        results[0].InterfaceType.ShouldEqual(typeof(IValidator<Model1>));

        results[1].ValidatorType.ShouldEqual(typeof(Model2Validator));
        results[1].InterfaceType.ShouldEqual(typeof(IValidator<Model2>));

     __   results.Count.ShouldEqual(2);__
    }

    [Test]
    public void ForEach_iterates_over_types() {
        var scanner = new AssemblyScanner(new[] { typeof(Model1Validator), typeof(Model2Validator)__, typeof(Model3Validator)__ });
        var results = new List<AssemblyScanner.AssemblyScanResult>();

        scanner.ForEach(x => results.Add(x));
        results.Count.ShouldEqual(2);
    }

    public class Model1 {

    }

    public class Model2 {

    }
__ public class Model3
    {

    }

    public class Model1Validator:AbstractValidator<Model1> {

    }

    public class Model2Validator:AbstractValidator<Model2> {

    }

    [NotAssemblyScanner]
    public class Model3Validator : AbstractValidator<Model2>
    {

    }
}__
}

wojtpl2 wrote Apr 29, 2013 at 12:09 PM

sorry for above but I did not know that's how it works.

JeremyS wrote Apr 29, 2013 at 12:43 PM

Thanks.

This is out-of-scope for FluentValidation itself - it's more to do with how you interact with the IoC container than FluentValidation. I'll leave it here in case other people find it useful, but isn't something that will be included in FluentValidation itself