Итак, после двухлетнего ожидания и многих обещаний мы можем пощупать 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)


  1. TerekhinSergey
    15.11.2023 10:23
    +3

    required - это не только и не столько про dto (по моему личному мнению все эти атрибуты валидации от бедности, fluentvalidation тот же гораздо лучше), а про контракты в широком смысле. Это слово требует задания значения свойства в явном виде, что является в аналогом и заменой конструкторов с параметрами, но выглядит более читаемо


    1. Kiel Автор
      15.11.2023 10:23

      У FluentValidator'а слишком высокий порог входа, к нему тяжело привыкнуть, он часто устаревает и для новичков это ужас. Атрибуты валидации лежат в том же месте что и класс, их подхватывает сваггер (и redoc если уж на то пошло) и это не размазывает логику валидиции по всему проекту ) Задумайтесь ;)

      2 characters это как раз автоматом прилетело из за атрибутов
      2 characters это как раз автоматом прилетело из за атрибутов


      1. Heggi
        15.11.2023 10:23

        Не всё можно провалидировать атрибутами.

        Например атрибутами не получится провалидировать связанные параметры.

        А если сервис gRpc, то там атрибуты просто некуда писать (все контракты кодогенерируемые) и тут без FluentValidator'a или аналога никуда.


        1. Kiel Автор
          15.11.2023 10:23

          Например атрибутами не получится провалидировать связанные параметры.

          Конечно, сложные кейсы уже не получится провалидировать нормально, это не значит что совсем уж нельзя (на самом деле можно), но я согласен, что оно того не стоит. Так что определенная логика использования FluentValidator'а присутствует. Но, опять же, за весь огромный проект международного маркетплейса я использовал его только в 1 месте для 1 класса


        1. nronnie
          15.11.2023 10:23

          Можно ведь писать кастомные аттрибуты валидации.


          1. OwDafuq
            15.11.2023 10:23

            Можно и EF/Dapper/etc. не использовать. Не понимаю за что хейтят FluentValidation, замечательная библиотека, порог входа не высокий уж точно, что там в ней сложного? Наследоваться от AbstractValidator<T>?)

            Постоянно использую её, замечательно встает в pipeline запроса (middleware или cqrs).


            1. nronnie
              15.11.2023 10:23

              Тут, просто, еще надо втыкать - в каком-то конкретном случае валидация это свойство самой сущности ("в телефоне должны быть только цифры") или требования бизнеса ("шлите нах* все звонки из ***")


            1. Kiel Автор
              15.11.2023 10:23

              Минусы FluentValidator'а

              • Он очень любит устаревать. Постоянно "эта версия устарела, используйте обязательно вот эту" и так же с правилами

              • Rules не выглядят логичными, я уже не помню подробностей, но каждый раз специфически влияет на другой и приходится сидеть бесконечно тестировать их, чтобы понимать что и как

              • Через какое то время модели классы валидации превращаются в лапшу с огромными конфигурациями и любой открывающий их боится даже строчку поменять

              • Не все валидации и правда нужны. 400 статус нужен только для токсичных данных, а FluentValidator очень часто начинает лезть в бизнес проверки и таким образом бизнес логика начинает течь

              • Документацию для сваггера нужно отдельно готовить, так что от атрибутов далеко не убежать )

              EF/Dapper/etc нужно использовать, правда Dapper с 8 .net устарел в случае, если вы используете EF. Каждый инструмент нужен для своей конкретной ситуации, нельзя взять один и сказать, что он теперь для всего и всегда и он самый лучший )


      1. TerekhinSergey
        15.11.2023 10:23

        Мне лично нравится подход, когда модели не облодены кучей атрибутов. Это касается и валидации, и генерации схемы бд. Единственное исключение тут - имена свойств для json'a, но и то только потому, что альтернатива - кастомные конвертеры. Отдельные валидаторы позволяют их как переиспользовать там, где они нужны, так и прозрачно обложить тестами. Насчёт сложности и порога входа - не заметил, если честно. Есть гораздо более сложные с точки зрения вхождения библиотеки


      1. nronnie
        15.11.2023 10:23

        Лично я не люблю FluentValidator. По идеологическим соображениям. Он просто валидирует, императивно. А ValidationAttribute это декларативное описание того, что собой данное свойство представляет - т.е. "домен" этого свойства.


  1. dyadyaSerezha
    15.11.2023 10:23
    +2

    Проблема в том, что создав класс Money с 15 rub ...

    Здесь и далее везде - не класс, а экземпляр класса. Прям каждый раз глаза резало.


    1. Kiel Автор
      15.11.2023 10:23

      Не говорю в жизни слово "экземпляр" или "class instance", вот и не заметил ошибку ) Теперь сижу и думаю - а можно ли это считать ошибкой в таком случае..


      1. zodchiy
        15.11.2023 10:23

        Да. Т.к. многие (проценты конечно никто не даст), когда говорят класс, то подразумевают контракт, а не его экземпляр.


        1. Kiel Автор
          15.11.2023 10:23

          Поменял на "объект класса" )


          1. dyadyaSerezha
            15.11.2023 10:23

            Объект тоже пойдёт)


        1. dyadyaSerezha
          15.11.2023 10:23

          Не контракт, а именно класс. Реальный, у которого можно вызвать конструктор.


      1. olivera507224
        15.11.2023 10:23
        +1

        В Руби, например, буквально можно создавать классы как объекты (экземпляры), а потом изменять их, поэтому соглашусь с комментатором выше, формулировка

        Теперь будет невозможно менять наш класс после его создания его объекта.

        ... режет глаз. Понятно в целом, что речь об экземпляре и его членах, но мы тут за формализм :)


        1. Kiel Автор
          15.11.2023 10:23

          Штош ) Меня больше расстраивает, что не смотря на мои попытки сделать статью проще - она всё еще слишком сложна для восприятия, вот что с этим делать -_-


          1. olivera507224
            15.11.2023 10:23

            Нет, статья для восприятия не сложна, просто имеется путаница в понятийном аппарате. Мне ввиду опыта вполне понятно что имелось ввиду, но тот же новобранец, который на вопрос "Что такое ООП?" ответит "Ну, это там классы, интерфейсы, наследование", может запутаться и в дальнейшем довольно долго пребывать в заблуждениях.

            В целом же я хоть в настоящий момент за развитием диеза и не слежу, так как не работаю на нём, но инфа была полезна. Теперь, когда и если я снова окунусь в диез, я буду знать, что те проблемы, которые я раньше решал через конструкторы, я смогу решить через модификаторы, что мне кажется удобнее. Просто поправь вот эти мелочи, вводящие в заблуждение, и всё тут будет норм :)


  1. nronnie
    15.11.2023 10:23

    FluentValidatinos это хороший, развитый фреймворк. Тут вопрос в декларативности. Я очень DDD, и за то что бы свойства сущностей описывались декларативно (а аттрибуты это и есть).


  1. Ratenti
    15.11.2023 10:23
    -1

    Ты даже не объяснил что такое record.


    1. Kiel Автор
      15.11.2023 10:23

      Так я и что такое свойства не объяснил Оо

      Ты даже не даже! (С) бебея напомнило


    1. Ratenti
      15.11.2023 10:23

      record в C# 9 добавили Что нового в C# 9.0