C#: [required] keyword, что это?

Итак, после двухлетнего ожидания и многих обещаний мы можем пощупать 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 просто не доходит до валидации по атрибутам. Я очень хочу, чтобы по прочтению данной статьи вы нашли где у вас в проекте может понадобиться столь чудный атрибут, а пока что, на этом всё

93b2d32cd9e644e6e906f95d672d25c8.png

© Habrahabr.ru