Итак, после двухлетнего ожидания и многих обещаний мы можем пощупать LTS версию свежего .net 8, более менее спокойно мигрировать наши кодовые базы и потестировать от всей души свежую версию языка!
Формально, C# 11 доступен с .net 7, но, на деле, он там работал очень плохо. Я один раз попробовал потестить около года назад и получил море ошибок, так что решил подождать
История появления required keyword в языке
Возьмём какой-нибудь класс
public class Money
{
public decimal Amount { get; set; }
public string Currency { get; set; }
}
Теперь мы можем его создавать и использовать как захотим
var money = new Money
{
Amount = 15m,
Currency = "RUB",
}
Проблема в том, что создав объект класса Money с 15 rub мы передаем его в другие методы. И в какой-то момент кому-то может прийти в голову не создавать новый объект класса (например, в целях оптимизации), а просто задать новый Amount. Такого, конечно же, допускать нельзя! Для таких людей и целей у нас появилось слово init
public class Money
{
public decimal Amount { get; init; }
public string Currency { get; init; }
}
Теперь будет невозможно менять наш класс после его создания его объекта. Но вот проблема, можно забыть задать валюту!!!
var money = new Money
{
Amount = 15m,
}
В чем теперь наши деньги? Опытные разработчики скажут, что данная проблема решается конструктором
public class Money
{
public Money(string currency)
{
Currency = currency;
}
public decimal Amount { get; init; }
public string Currency { get; }
}
Но тогда у нас появляется этот самый конструктор и запись класса превращается в монстра Франкенштейна! Для решения данной проблемы ребята придумали record
public record Money(string Currency, decimal Amount);
И, в принципе, здесь можно было бы остановиться, потому что у рекордов есть ряд приятных преимуществ. Но вот что делать, есть нам нужны DTO? Для DTO нужно будет пометить всё атрибутами
public class MoneyDto
{
[Required]
[JsonPropertyName("amount")]
public decimal Amount { get; init; }
[Required]
[StringLength(maximumLength: 3, MinimumLength = 3)]
[JsonPropertyName("currency")]
public string Currency { get; init; }
}
Рекорд тоже можно обложить атрибутами, но там начинается просто ужас нечитаемый
И вот здесь уже появляется наш герой: ключевое слово required
Required keyword в действии
С данным словом наш класс становится намного элегантнее!
public class MoneyDto
{
[JsonPropertyName("amount")]
public required decimal Amount { get; init; }
[StringLength(maximumLength: 3, MinimumLength = 3)]
[JsonPropertyName("currency")]
public required string Currency { get; init; }
}
Теперь класс не только можно создать только с данными свойствами, но еще и запретить их менять в процессе. Ииии, тестируем!
Без required keyword
{
"Amount": [
"The Amount field is required."
],
"Currency": [
"The Currency field is required."
]
}
C required keyword
{
"$": [
"JSON deserialization for type 'NewNet.MoneyDto' was missing required properties, including the following: amount, currency"
],
"body": [
"The body field is required."
]
}
Для начала, мне очень нравится, что всё работает (Я просто люблю, когда всё работает). Но, как мы видим - первая наша проблема, это изменился контракт ошибки с красивого на непонятно что с "$". Вторая - если атрибут nullable, то его всё еще можно передавать через null. Например, вот так
public required decimal? Amount { get; init; }
{
"amount": null,
"currency": "RUB"
}
И всё отработает. Это надо держать в уме. Вообще, это очень логично, мы же сами разрешили null и пометили свойство required, но всё равно чуть-чуть остается ощущение будто обманули -_-
Заключение
Как всегда приятно видеть, что язык развивается и становится лучше, продуманнее, глубже. Но детали всё еще не проработаны, всё еще нужно учитывать достаточно много и писать кое какой boilerplate code или как то что то по определенному настраивать. В нашем случае очевидно, что проблема в валидаторе десерилизатора json и наша dto просто не доходит до валидации по атрибутам. Я очень хочу, чтобы по прочтению данной статьи вы нашли где у вас в проекте может понадобиться столь чудный атрибут, а пока что, на этом всё
Комментарии (23)
dyadyaSerezha
15.11.2023 10:23+2Проблема в том, что создав класс Money с 15 rub ...
Здесь и далее везде - не класс, а экземпляр класса. Прям каждый раз глаза резало.
Kiel Автор
15.11.2023 10:23Не говорю в жизни слово "экземпляр" или "class instance", вот и не заметил ошибку ) Теперь сижу и думаю - а можно ли это считать ошибкой в таком случае..
zodchiy
15.11.2023 10:23Да. Т.к. многие (проценты конечно никто не даст), когда говорят класс, то подразумевают контракт, а не его экземпляр.
dyadyaSerezha
15.11.2023 10:23Не контракт, а именно класс. Реальный, у которого можно вызвать конструктор.
olivera507224
15.11.2023 10:23+1В Руби, например, буквально можно создавать классы как объекты (экземпляры), а потом изменять их, поэтому соглашусь с комментатором выше, формулировка
Теперь будет невозможно менять наш класс после его создания его объекта.
... режет глаз. Понятно в целом, что речь об экземпляре и его членах, но мы тут за формализм :)
Kiel Автор
15.11.2023 10:23Штош ) Меня больше расстраивает, что не смотря на мои попытки сделать статью проще - она всё еще слишком сложна для восприятия, вот что с этим делать -_-
olivera507224
15.11.2023 10:23Нет, статья для восприятия не сложна, просто имеется путаница в понятийном аппарате. Мне ввиду опыта вполне понятно что имелось ввиду, но тот же новобранец, который на вопрос "Что такое ООП?" ответит "Ну, это там классы, интерфейсы, наследование", может запутаться и в дальнейшем довольно долго пребывать в заблуждениях.
В целом же я хоть в настоящий момент за развитием диеза и не слежу, так как не работаю на нём, но инфа была полезна. Теперь, когда и если я снова окунусь в диез, я буду знать, что те проблемы, которые я раньше решал через конструкторы, я смогу решить через модификаторы, что мне кажется удобнее. Просто поправь вот эти мелочи, вводящие в заблуждение, и всё тут будет норм :)
nronnie
15.11.2023 10:23FluentValidatinos это хороший, развитый фреймворк. Тут вопрос в декларативности. Я очень DDD, и за то что бы свойства сущностей описывались декларативно (а аттрибуты это и есть).
TerekhinSergey
required - это не только и не столько про dto (по моему личному мнению все эти атрибуты валидации от бедности, fluentvalidation тот же гораздо лучше), а про контракты в широком смысле. Это слово требует задания значения свойства в явном виде, что является в аналогом и заменой конструкторов с параметрами, но выглядит более читаемо
Kiel Автор
У FluentValidator'а слишком высокий порог входа, к нему тяжело привыкнуть, он часто устаревает и для новичков это ужас. Атрибуты валидации лежат в том же месте что и класс, их подхватывает сваггер (и redoc если уж на то пошло) и это не размазывает логику валидиции по всему проекту ) Задумайтесь ;)
Heggi
Не всё можно провалидировать атрибутами.
Например атрибутами не получится провалидировать связанные параметры.
А если сервис gRpc, то там атрибуты просто некуда писать (все контракты кодогенерируемые) и тут без FluentValidator'a или аналога никуда.
Kiel Автор
Конечно, сложные кейсы уже не получится провалидировать нормально, это не значит что совсем уж нельзя (на самом деле можно), но я согласен, что оно того не стоит. Так что определенная логика использования FluentValidator'а присутствует. Но, опять же, за весь огромный проект международного маркетплейса я использовал его только в 1 месте для 1 класса
nronnie
Можно ведь писать кастомные аттрибуты валидации.
OwDafuq
Можно и EF/Dapper/etc. не использовать. Не понимаю за что хейтят FluentValidation, замечательная библиотека, порог входа не высокий уж точно, что там в ней сложного? Наследоваться от AbstractValidator<T>?)
Постоянно использую её, замечательно встает в pipeline запроса (middleware или cqrs).
nronnie
Тут, просто, еще надо втыкать - в каком-то конкретном случае валидация это свойство самой сущности ("в телефоне должны быть только цифры") или требования бизнеса ("шлите нах* все звонки из ***")
Kiel Автор
Минусы FluentValidator'а
Он очень любит устаревать. Постоянно "эта версия устарела, используйте обязательно вот эту" и так же с правилами
Rules не выглядят логичными, я уже не помню подробностей, но каждый раз специфически влияет на другой и приходится сидеть бесконечно тестировать их, чтобы понимать что и как
Через какое то время модели классы валидации превращаются в лапшу с огромными конфигурациями и любой открывающий их боится даже строчку поменять
Не все валидации и правда нужны. 400 статус нужен только для токсичных данных, а FluentValidator очень часто начинает лезть в бизнес проверки и таким образом бизнес логика начинает течь
Документацию для сваггера нужно отдельно готовить, так что от атрибутов далеко не убежать )
EF/Dapper/etc нужно использовать, правда Dapper с 8 .net устарел в случае, если вы используете EF. Каждый инструмент нужен для своей конкретной ситуации, нельзя взять один и сказать, что он теперь для всего и всегда и он самый лучший )
TerekhinSergey
Мне лично нравится подход, когда модели не облодены кучей атрибутов. Это касается и валидации, и генерации схемы бд. Единственное исключение тут - имена свойств для json'a, но и то только потому, что альтернатива - кастомные конвертеры. Отдельные валидаторы позволяют их как переиспользовать там, где они нужны, так и прозрачно обложить тестами. Насчёт сложности и порога входа - не заметил, если честно. Есть гораздо более сложные с точки зрения вхождения библиотеки
nronnie
Лично я не люблю
FluentValidator
. По идеологическим соображениям. Он просто валидирует, императивно. АValidationAttribute
это декларативное описание того, что собой данное свойство представляет - т.е. "домен" этого свойства.