Давненько ничего не писал про программирование, время исправиться. В нашем любимом .NET очень часто на собесах любят спрашивать про типы данных, про ссылочные и значимые, где большинство отвечает уже на автомате про хранение на стеке / кучи, передача значения или ссылки на значение, боксинг / анбоксинг и аллокации при преобразовании – база, в общем. Недавно столкнулся с одним багом, который прям хорошо так подсвечивает разницу между этими типами, а разбор такого бага на собесе гарантировано покажет интервьюеру, что вы прям хорошо понимаете разницу.
Итак, есть стандартный контроллер с необходимым атрибутом [ApiController], унаследован от базового ControllerBase – все как нужно.
На вход в контроллер придет модель
public class CreateModel
{
[Required]
public string Name { get; set; }
[Required]
public UserStatus Status { get; set; } //enum
[Required]
public bool IsActive { get; set; }
[Required]
public int Percent { get; set; }
}
С первого взгляда все сделано правильно – необходимым поля помечены атрибутом [Required], все поля не могут быть пустыми / nullable, мы теперь ожидаем что эндпоинт будет отклонять запрос, если одно из полей будет пустым. Запускаем приложение, открываем свагер, убираем из модели поле Name, получаем ожидаемый ответ 400 и отправляем МР со спокойной душой.
Но, как вы понимаете, здесь спрятана коварная ошибка, которая в лучшем случае всплывет на тестах. Из-за того что string – ссылочный тип, у него допускается значение null при инициализации. Если ничего не передать с фронта, то при обработке запроса и привязке к моделе, ASP .NET оставит это поле незаполненным, т. е. как есть, а дальше уже вступает в дело атрибут [Required], Name отсутствует и запрос отклоняется. А вот с остальными полями enum UserStatus, bool IsActive и int Percent ситуация интереснее. Они все значимые типы, а у значимых типов не может быть значения null по умолчанию. Поэтому парсер ASP .NET Core читает входящий JSON, видит отсутствие поля UsingService или IsActive и инициализирует эти свойства в вашей модели их значениями по умолчанию. А для int это 0, для bool = false, для Enum – первое значение. И это произойдет автоматически при ModelBinding, а дальше снова вступает в дело атрибут [Required], а все обязательные поля есть и значения имеются. Запрос принимается контроллером, летит куда-то дальше в хэндлер, и, в конечном итоге, некорректное значение падает в базу. И все что нужно, чтобы запрос отклонил эндпоинт, помнить правило: если нам нужно использовать [Required], для значимых типов, всегда делайте их nullable
то есть должно быть
public class CreateModel
{
public UserStatus? Status { get; set; } //enum
public bool? IsActive { get; set; }
public int? Percent { get; set; }
}
Тогда они не будут инициироваться значениями по умолчанию, а атрибут сработает как мы и ожидали.
Итак, есть стандартный контроллер с необходимым атрибутом [ApiController], унаследован от базового ControllerBase – все как нужно.
На вход в контроллер придет модель
public class CreateModel
{
[Required]
public string Name { get; set; }
[Required]
public UserStatus Status { get; set; } //enum
[Required]
public bool IsActive { get; set; }
[Required]
public int Percent { get; set; }
}
С первого взгляда все сделано правильно – необходимым поля помечены атрибутом [Required], все поля не могут быть пустыми / nullable, мы теперь ожидаем что эндпоинт будет отклонять запрос, если одно из полей будет пустым. Запускаем приложение, открываем свагер, убираем из модели поле Name, получаем ожидаемый ответ 400 и отправляем МР со спокойной душой.
Но, как вы понимаете, здесь спрятана коварная ошибка, которая в лучшем случае всплывет на тестах. Из-за того что string – ссылочный тип, у него допускается значение null при инициализации. Если ничего не передать с фронта, то при обработке запроса и привязке к моделе, ASP .NET оставит это поле незаполненным, т. е. как есть, а дальше уже вступает в дело атрибут [Required], Name отсутствует и запрос отклоняется. А вот с остальными полями enum UserStatus, bool IsActive и int Percent ситуация интереснее. Они все значимые типы, а у значимых типов не может быть значения null по умолчанию. Поэтому парсер ASP .NET Core читает входящий JSON, видит отсутствие поля UsingService или IsActive и инициализирует эти свойства в вашей модели их значениями по умолчанию. А для int это 0, для bool = false, для Enum – первое значение. И это произойдет автоматически при ModelBinding, а дальше снова вступает в дело атрибут [Required], а все обязательные поля есть и значения имеются. Запрос принимается контроллером, летит куда-то дальше в хэндлер, и, в конечном итоге, некорректное значение падает в базу. И все что нужно, чтобы запрос отклонил эндпоинт, помнить правило: если нам нужно использовать [Required], для значимых типов, всегда делайте их nullable
то есть должно быть
public class CreateModel
{
public UserStatus? Status { get; set; } //enum
public bool? IsActive { get; set; }
public int? Percent { get; set; }
}
Тогда они не будут инициироваться значениями по умолчанию, а атрибут сработает как мы и ожидали.