Учим ModelState корректно проверять Nullable поля в Asp.Net MVC

Учим ModelState корректно проверять Nullable поля в Asp.Net MVC

Учим ModelState корректно проверять Nullable поля в Asp.Net MVCAsp.Net MVC содержит один из достаточно полезных инструментов - валидация моделей данных. Речь идет о ModelState и пространстве имен DataAnnotations с его атрибутами, за счет которых можно достаточно просто организовать серверную и клиентскую проверку полей ваших моделей. Если ваши типы данных являются простыми (или классами) и всегда содержат данные, то особых проблем при его использовании у вас не возникнет. Однако, если вы попытаетесь использовать Nullable типы, такие как "int?", "float?", "long?" и так далее, то с удивлением обнаружите, что стоит не прийти с клиентской части значению для поля (или пустое поле), то валидация модели всегда будет проваливаться с ошибкой о том, что поле необходимо. При этом совершенно не важно, указан ли атрибут Required или нет.

Поиск обходного решения для Asp.Net MVC ModelState и Nullable полей

После некоторых поисков оказалось, что по умолчанию для всех Nullable типов полей выставляется атрибут Required. Таким образом, если вы создаете модель на основе полей таблиц, то, при наличии хотя бы одного null-поля (тот же int для необязательных вторичных ключей), валидация полей, а именно параметр ModelState.IsValid, в случае значения null, всегда будет возвращать false. Это несколько неожиданно, учитывая, что база данных MSSQL создана той же компаний и что она легко поддерживает такие поля и типы связей. 

Как бы то ни было, ошибаться может каждый, поэтому следующим шагом стал поиск обходных путей. И все следующие решения сводились примерно к следующему:

  1. Выставлять специальный параметр в global.asax.cs для провайдера, чтобы приложение отключало выставление атрибутов [Required] для Nullable полей.
  2. Реализовывать дополнительные валидаторы для каждого из типов или один, но с огромным switch-ом (if-else)
  3. Создавать собственный провайдер или байндеры для моделей данных, учитывающих эту тонкость
  4. Изменять ModelState, например, в ActionFilterAttribute
  5. Ставить заглушки

Обходные пути для решения проблемы Nullable полей для Asp.Net MVC на практике

Если у вас всего одна модель или такой случай возникает всего один раз, то вы легко можете позволить себе использовать простые заглушки вида:

if (Item.id != null && ModelState.IsValid) {
//...

Однако, когда если у вас таких полей достаточно много, то заглушки будут разрастаться с невероятной скоростью, поэтому 5 вариант отпадает сразу. Необходимо более комплексное решение, которое легко внедряется и не потребует тонны лишнего кода. Поэтому, прежде всего, проверялся первый пункт с параметром в global.asax.cs, но, к сожалению, эффекта не было. Поведение валидации полей осталось прежним. После некоторых попыток запустить приложение с этим переключателем, желание копаться в том, почему переключатель не производит эффекта как-то само собой отпало, как нерациональное ("если фикс к багу не пашет, лучше оставить его на крайний случай"). Возможно, у вас этот кусок кода приведет к чему то полезному:

protected void Application_Start()
{
    DataAnnotationsModelValidatorProvider
    .AddImplicitRequiredAttributeForValueTypes = false;
    //...

Следующим решением стал просмотр собственных валидаторов. Имеющиеся решения либо охватывали один тип данных, либо были настолько большими по размеру кода, что было сразу понятно - если вылезет ошибка, то времени на ее исправление уйдет море (а как бы ради бага с Nullable полями это слишком). Кроме того, такие валидаторы необходимо выставлять ко всем Nullable полям, что так же подразумевает массу дополнительного кода. Да и стоит атрибут хотя бы раз не указать, так у вас сразу появится неплохой отложенный баг, который обязательно вскроется в ненужный момент времени. Поэтому такое решение так же отпало.

Обходной путь через создание собственного провайдера или байндера данных обладает, как минимум, двумя большими недостатками по отношению к решаемой проблеме. Во-первых, это масса кода, а в случае байндеров под каждую модель - это невероятная масса кода. Во-вторых, создание таких вещей должно быть оправдано реальной необходимостью. А Nullable поля вряд ли тянут на такую необходимость. Проще, очевиднее и быстрее использовать заглушки. 

Из рассматриваемых путей осталось только ручное изменение наполнения ModelState. Однако, оставался вопрос как его проводить так, чтобы решение легко внедрялось, было относительно независимым и в то же время комплексным. Другими словами, решение из разряда "сделал > без проблем используется до сих пор". Если пытаться менять ModelState в самих экшенах, то это будет сродни заглушкам. Поэтому состояние модели должно изменяться до того, как начнет исполняться экшен (чтобы код в экшене не зависел от багов). А это ничто иное, как ActionFilterAttribute и иже подобные. В итоге, был реализован следующий атрибут, который легко применяется как к отдельным экшенам, так и к контроллерам, что позволяет достаточно просто и комплексно решать проблему.

public class NormalValidateNullableFieldsAttribute : ActionFilterAttribute

{

    public override void OnActionExecuting(ActionExecutingContext filterContext)

    {

        // Получаем ModelState, т.е. состояние модели

        var modelState = filterContext.Controller.ViewData.ModelState;

        // Получаем провайдер

        var valueProvider = filterContext.Controller.ValueProvider;

        // Получаем список параметров метода

        var actionParameters = filterContext.ActionParameters;

 

        // Получаем все Property для которых валидация произошла с ошибкой

        var keysWithNoIncomingValue = modelState.Keys.Where(x => !valueProvider.ContainsPrefix(x) && modelState[x].Errors.Count > 0);

        

        // Проходимся по каждому из ключей

        foreach (var key in keysWithNoIncomingValue)

        {

            // Если элемент составной, т.е. является частью модели

            if (key.Split('.').Length > 1)

            {

                // Получаем свойства поля

                var member = actionParameters[key.Split('.')[0]].GetType().GetMember(key.Split('.')[1])[0];

                // Если изначальным типом был тип Nullable<>

                if (((System.Reflection.PropertyInfo)member).PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>)

                    // И у поля не установлено кастомный атрибут Required

                    && member.GetCustomAttributes(typeof(RequiredAttribute), true).Length == 0)

                {

                    // То очищаем ошибку

                    modelState[key].Errors.Clear();

                }

            }

        }

    }

}

Примечание: Кроме того, вы можете создать базовый контролер и повесить на него атрибут. В таком случае, вам вообще не придется больше волноваться о Nullable полях.

Теперь, вы знаете как научить ModelState корректно проверять Nullable поля в Asp.Net MVC.

Добавлено. А если у вас проблемы не только с проверкой Nullable полей, но и с необязательными полями, к которым приходят поля с формы с пустыми значениями или со значением "null" (например, поле идентификатор, как int ID и подобные, которое не является обязательным с точки зрения логики вашего приложения, но является обязательным с точки зрения описания модели), то можно использовать следующий код:

public class NormalValidateNullableFieldsAttribute : ActionFilterAttribute

{

    public override void OnActionExecuting(ActionExecutingContext filterContext)

    {

        // Получаем ModelState, т.е. состояние модели

        var modelState = filterContext.Controller.ViewData.ModelState;

        // Получаем провайдер

        var valueProvider = filterContext.Controller.ValueProvider;

        // Получаем список параметров метода

        var actionParameters = filterContext.ActionParameters;

 

        // Получаем все Property для которых валидация произошла с ошибкой

        var keysWithNoIncomingValue = modelState.Keys.Where(

                x => (

                        // Параметра в коллекциях нет

                        !valueProvider.ContainsPrefix(x) 

                        || (

                            // Или параметр есть, но с пустым значением

                            valueProvider.ContainsPrefix(x)

                            && (

                                valueProvider.GetValue(x).AttemptedValue == "null"

                                || valueProvider.GetValue(x).AttemptedValue == ""

                            )

                        ) 

                    )

                    && modelState[x].Errors.Count > 0).ToList();

        //var keysWithNoIncomingValue = modelState.Keys.Where(x => !valueProvider.ContainsPrefix(x) && modelState[x].Errors.Count > 0).ToList();

        

        // Проходимся по каждому из ключей

        foreach (var key in keysWithNoIncomingValue)

        {

            // Если элемент составной, т.е. является частью модели

            if (key.Split('.').Length > 1)

            {

                // Получаем свойства поля

                var member = actionParameters[key.Split('.')[0]].GetType().GetMember(key.Split('.')[1])[0];

                // Если тип является Generic

                if (((System.Reflection.PropertyInfo)member).PropertyType.IsGenericType)

                {

                    // Если изначальным типом был тип Nullable<>

                    if (((System.Reflection.PropertyInfo)member).PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>)

                        // И у поля не установлено кастомный атрибут Required

                        && member.GetCustomAttributes(typeof(RequiredAttribute), true).Length == 0)

                    {

                        // То очищаем ошибку

                        modelState[key].Errors.Clear();

                    }

                }

                else

                {

                    // Если у поля не установлен кастомный атрибут Required

                    if (member.GetCustomAttributes(typeof(RequiredAttribute), true).Length == 0)

                    {

                        // То очищаем ошибку

                        modelState[key].Errors.Clear();

                    }

                }

            }

        }

    }

}

Данный атрибут будет проверять не только Nullable поля, но и поля, у которых не стоит атрибут проверки Required, но к которым пришли пустые поля с формы или из строки запроса. Фраза может вам показаться сложной, но если вы взгляните на изменения в коде, то поймете, что все достаточно просто.

Социальные сети

☕ Понравился обзор? Поделитесь с друзьями!

Добавить комментарий / отзыв
Комментарий - это вежливое и наполненное смыслом сообщение (правила).



* Нажимая на кнопку "Отправить", Вы соглашаетесь с политикой конфиденциальности.
Социальные сети
Программы (Freeware, OpenSource...)