ASP.Net MVC – brak wywołania ValidationProvidera

W życiu każdego programisty kiedyś nastaje ten dzień. Dzień w którym coś nie działa i nawet po głębokim zagłębieniu się w kod projektu oraz dokumentację bibliotek zewnętrznych nie udaje się namierzyć przyczyny…

Mi także przydarzyło się coś takiego. Część modeli wykorzystywanych w projekcie opartym o ASP.Net MVC (nie pamiętam już której wersji konkretnie) miała zdefiniowane ModelValidor’y dostarczane przez przygotowane przez programistów ModelValidatorProvider’y. Niestety jeden, jedyny model z jakiegoś powodu nie wykonywał przypisanego do siebie walidatora, nie dochodziło nawet do wywołania konstruktora. Przebudowa klasy modelu, tak aby walidacja odbywała się przez zaimplementowany interfejs IValidatableObject niestety także nie pomagała.
Jedynym tropem było to, że walidacja zaczynała działać po wyrzuceniu z zarejestrowanych dostawców walidacji dostawcy o typie DataAnnotationsModelValidatorProvider. Wniosek z tego był taki, że powinienem się zbadać proces walidacji modeli w aplikacji.
Na szczęście ASP.Net MVC posiada otwarte źródła, które możemy podejrzeć i zbadać jak właściwie działa.
Udało mi się namierzyć miejsce w którym jest uruchamiana walidacja modeli. Dzieje się to w klasie DefaultModelBinder, w metodzie OnModelModified (jest wirtualna, więc można ją nadpisać jeśli ktoś sobie życzy).
Wywoływana jest w niej między innymi metoda

        public static ModelValidator GetModelValidator(ModelMetadata metadata, ControllerContext context)
        {
            return new CompositeModelValidator(metadata, context);
        }

klasy statycznej ModelValidator.

 

A oto prywatna klasa CompositeModelValidator:

        private class CompositeModelValidator : ModelValidator
        {
            public CompositeModelValidator(ModelMetadata metadata, ControllerContext controllerContext)
                : base(metadata, controllerContext)
            {
            }

            private static ModelValidationResult CreateSubPropertyResult(ModelMetadata propertyMetadata, ModelValidationResult propertyResult)
            {
                return new ModelValidationResult
                {
                    MemberName = DefaultModelBinder.CreateSubPropertyName(propertyMetadata.PropertyName, propertyResult.MemberName),
                    Message = propertyResult.Message
                };
            }

            public override IEnumerable<ModelValidationResult> Validate(object container)
            {
                bool propertiesValid = true;

                ModelMetadata[] properties = Metadata.PropertiesAsArray;

                // Performance sensitive loops
                for (int propertyIndex = 0; propertyIndex < properties.Length; propertyIndex++)
                {
                    ModelMetadata propertyMetadata = properties[propertyIndex];
                    foreach (ModelValidator propertyValidator in propertyMetadata.GetValidators(ControllerContext))
                    {
                        foreach (ModelValidationResult propertyResult in propertyValidator.Validate(Metadata.Model))
                        {
                            propertiesValid = false;
                            yield return CreateSubPropertyResult(propertyMetadata, propertyResult);
                        }
                    }
                }

                if (propertiesValid)
                {
                    foreach (ModelValidator typeValidator in Metadata.GetValidators(ControllerContext))
                    {
                        foreach (ModelValidationResult typeResult in typeValidator.Validate(container))
                        {
                            yield return typeResult;
                        }
                    }
                }
            }
        }

Zwróćmy uwagę na metodę Validate(object container). Widzimy w niej wyraźnie kolejność wywoływania walidacji. Najpierw sprawdzane są kolejne właściwości (properties) modelu przy użyciu m. in. atrybutów walidacyjnych (ValidationAttribute). Dopiero gdy wszystkie pola są prawidłowe następuje walidacja modelu jako całości, czyli wywoływana jest metoda IValidatableObject.Validate(…) oraz wszystkie obiekty ModelValidator dostarczone przez ModelValidationProvider(y).

Ostatecznie okazało się, że w klasie modelu tkwił błąd, na który nie zwróciłem w ogóle uwagi. Otóż istniało tam pole, które nie było do niczego wykorzystywane i było oznaczone atrybutem [Required]. Przy każdym zapisie formularza zostawała do niego przypisana pusta wartość. W efekcie powodowało to, że nie dochodziło do walidacji całego obiektu, zatrzymywała się ona na walidacji właściwości. Nie miała szansy dotrzeć do tego momentu.

Własnoręczne zbadanie procesu walidacji modelu nauczyło mnie, że warto spojrzeć w kod bibliotek i frameworków z których korzystamy. Choćby po to by je lepiej zrozumieć.

Ciekawostką jest, że w trakcie pobieżnego przeglądania książki ASP.NET MVC 4. Zaawansowane programowanie, Adama Freemana nie napotkałem na wzmianki dotyczące kolejności walidacji.

Link do źródła ASP MVC: https://git01.codeplex.com/aspnetwebstack.git

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *