Functional C#: Primitive obsession (одержимость примитивами)

    Это вторая статья из миницикла статей про функциональный C#.


    Что такое одержимость примитивами (Primitive obsession)?


    Если коротко, то это когда для моделирования домена приложения используются в основном примитивные типы (string, int и т.п.). К примеру, вот как класс Customer может выглядеть в типичном приложении:

    public class Customer
    {
        public string Name { get; private set; }
        public string Email { get; private set; }
     
        public Customer(string name, string email)
        {
            Name = name;
            Email = email;
        }
    }
    

    Проблема здесь в том, что если вам необходимо обеспечить соблюдение каких-то бизнес-правил, вам приходится дублировать логику валидации по всему коду класса:

    public class Customer
    {
        public string Name { get; private set; }
        public string Email { get; private set; }
     
        public Customer(string name, string email)
        {
            // Validate name
            if (string.IsNullOrWhiteSpace(name) || name.Length > 50)
                throw new ArgumentException(“Name is invalid”);
     
            // Validate e-mail
            if (string.IsNullOrWhiteSpace(email) || email.Length > 100)
                throw new ArgumentException(“E-mail is invalid”);
            if (!Regex.IsMatch(email, @”^([\w\.\-]+)@([\w\-]+)((\.(\w){2,3})+)$”))
                throw new ArgumentException(“E-mail is invalid”);
     
            Name = name;
            Email = email;
        }
     
        public void ChangeName(string name)
        {
            // Validate name
            if (string.IsNullOrWhiteSpace(name) || name.Length > 50)
                throw new ArgumentException(“Name is invalid”);
     
            Name = name;
        }
     
        public void ChangeEmail(string email)
        {
            // Validate e-mail
            if (string.IsNullOrWhiteSpace(email) || email.Length > 100)
                throw new ArgumentException(“E-mail is invalid”);
            if (!Regex.IsMatch(email, @”^([\w\.\-]+)@([\w\-]+)((\.(\w){2,3})+)$”))
                throw new ArgumentException(“E-mail is invalid”);
     
            Email = email;
        }
    }
    

    Более того, точно такой же код имеет тенденцию попадать в application слой:

    [HttpPost]
    public ActionResult CreateCustomer(CustomerInfo customerInfo)
    {
        if (!ModelState.IsValid)
            return View(customerInfo);
     
        Customer customer = new Customer(customerInfo.Name, customerInfo.Email);
        // Rest of the method
    }
    
    public class CustomerInfo
    {
        [Required(ErrorMessage = “Name is required”)]
        [StringLength(50, ErrorMessage = “Name is too long”)]
        public string Name { get; set; }
     
        [Required(ErrorMessage = “E-mail is required”)]
        [RegularExpression(@”^([\w\.\-]+)@([\w\-]+)((\.(\w){2,3})+)$”, ErrorMessage = “Invalid e-mail address”)]
        [StringLength(100, ErrorMessage = “E-mail is too long”)]
        public string Email { get; set; }
    }
    

    Очевидно, такой подход нарушает принцип DRY. Этот принцип говорит нам о том, что каждая часть информации о домене должна иметь единственный авторитетный источник в коде нашего приложения. В примере выше мы имеем 3 таких источника.

    Как избавиться от одержимости примитивами?


    Чтобы избавиться от одержимости примитивами, мы должны добавить два новых типа, которые бы агрегировали в себе логику валидации. Таким образом мы сможем избавиться от дублирования:

    public class Email
    {
        private readonly string _value;
     
        private Email(string value)
        {
            _value = value;
        }
     
        public static Result<Email> Create(string email)
        {
            if (string.IsNullOrWhiteSpace(email))
                return Result.Fail<Email>(“E-mail can’t be empty”);
     
            if (email.Length > 100)
                return Result.Fail<Email>(“E-mail is too long”);
     
            if (!Regex.IsMatch(email, @”^([\w\.\-]+)@([\w\-]+)((\.(\w){2,3})+)$”))
                return Result.Fail<Email>(“E-mail is invalid”);
     
            return Result.Ok(new Email(email));
        }
     
        public static implicit operator string(Email email)
        {
            return email._value;
        }
     
        public override bool Equals(object obj)
        {
            Email email = obj as Email;
     
            if (ReferenceEquals(email, null))
                return false;
     
            return _value == email._value;
        }
     
        public override int GetHashCode()
        {
            return _value.GetHashCode();
        }
    }
    
    public class CustomerName
    {
        public static Result<CustomerName> Create(string name)
        {
            if (string.IsNullOrWhiteSpace(name))
                return Result.Fail<CustomerName>(“Name can’t be empty”);
     
            if (name.Length > 50)
                return Result.Fail<CustomerName>(“Name is too long”);
     
            return Result.Ok(new CustomerName(name));
        }
     
        // Остальная часть класса такая же, как Email
    }
    

    Достоинство этого подхода в том, что в случае изменения логики валидации, нам достаточно отразить это изменение только единожды.

    Обратите вниманите, что конструктор класса Email закрыт, так что единственный способ создать его экземпляр — использовать статический метод Create, который проводит всю необходимую валидацию. Этот подход позволяет нам быть уверенными в том, что все экземпляры класса Email находятся в валидном состоянии на протяжении всей их жизни.

    Вот как контроллер может использовать эти классы:

    [HttpPost]
    public ActionResult CreateCustomer(CustomerInfo customerInfo)
    {
        Result<Email> emailResult = Email.Create(customerInfo.Email);
        Result<CustomerName> nameResult = CustomerName.Create(customerInfo.Name);
     
        if (emailResult.Failure)
            ModelState.AddModelError(“Email”, emailResult.Error);
        if (nameResult.Failure)
            ModelState.AddModelError(“Name”, nameResult.Error);
     
        if (!ModelState.IsValid)
            return View(customerInfo);
     
        Customer customer = new Customer(nameResult.Value, emailResult.Value);
        // Rest of the method
    }
    

    Экземпляры Result<Email> и Result<CustomerName> явным образом говорят нам о том, что метод Create может потерпеть неудачу, и если это так, то мы сможем узнать причину прочитав свойство Error.

    Вот как класс Customer выглядит после рефакторинга:

    public class Customer
    {
        public CustomerName Name { get; private set; }
        public Email Email { get; private set; }
     
        public Customer(CustomerName name, Email email)
        {
            if (name == null)
                throw new ArgumentNullException(“name”);
            if (email == null)
                throw new ArgumentNullException(“email”);
     
            Name = name;
            Email = email;
        }
     
        public void ChangeName(CustomerName name)
        {
            if (name == null)
                throw new ArgumentNullException(“name”);
     
            Name = name;
        }
     
        public void ChangeEmail(Email email)
        {
            if (email == null)
                throw new ArgumentNullException(“email”);
     
            Email = email;
        }
    }
    

    Почти все проверки переехали в Email и CustomerName. Единственная оставшаяся валидация — это проверка на null. Мы посмотрим как избавиться и от нее в следующей статье.

    Итак, какие преимущества дает нам избавление от одержимости примитивами?

    • Мы создаем единственный авторитетный источник знаний для каждой проблемы, решаемой нашим кодом. Никаких дублирований, только чистый и «сухой» (dry) код.
    • Более строгая система типов. Компилятор работает на нас с удвоенной силой: теперь невозможно ошибочно присвоить свойству типа Email объект типа CustomerName, такой код не будет скомпилирован.
    • Нет необходимости в проверке входящих значений. Если мы получаем объект класса Email или CustomerName, мы можем быть на 100% уверены, что он находится в корректном состоянии.

    Небольшое замечание. Некоторые разработчики имеют тенденцию «оборачивать» и «разворачивать» примитивные типы по нескольку раз в течение единственной операции:

    public void Process(string oldEmail, string newEmail)
    {
        Result<Email> oldEmailResult = Email.Create(oldEmail);
        Result<Email> newEmailResult = Email.Create(newEmail);
     
        if (oldEmailResult.Failure || newEmailResult.Failure)
            return;
     
        string oldEmailValue = oldEmailResult.Value;
        Customer customer = GetCustomerByEmail(oldEmailValue);
        customer.Email = newEmailResult.Value;
    }
    

    Лучше всего использовать кастомные типы во всем приложении, разворачивая их в примитивы только когда они выходят за границы домена, к примеру сохраняются в базу или рендерятся в HTML. В ваших доменный классах старайтесь всегда использовать кастомные типы, код в таком случае будет более простым и читаемым:

    public void Process(Email oldEmail, Email newEmail)
    {
        Customer customer = GetCustomerByEmail(oldEmail);
        customer.Email = newEmail;
    }
    

    Ограничения


    К сожалению, создание типов-оберток в C# — процесс не настолько простой как в к примеру в F#. Это возможно изменится в C# 7 если будет реализован pattern matching и record types на уровне языка. До того момента, нам приходится иметь дело с неуклюжестью этого подхода.

    Из-за этого некоторые примитивные типы не стоят того, чтобы быть обернутыми. К примеру, тип «money amount» с единственным инвариантом, говорящим о том, что количество денег не может быть отрицательным, может быть представлен как обычный decimal. Это приведет к некоторому дублированию логики валидации, но даже не смотря на это, такой подход будет более простым решением, даже в долгосрочной перспективе.

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

    Заключение


    С неизменяемыми и непримитивными типами мы подходим ближе к проектированию приложений на C# в более функциональном стиле. В следующей статье мы обсудим как облегчить «ошибку на миллиард долларов» (mitigate the billion dollar mistake).

    Исходники



    Остальные статьи в цикле



    Английская версия статьи: Functional C#: Primitive obsession
    Поделиться публикацией

    Комментарии 49

      +4
      Не совсем корректный пример с логикой проверки на уровне ViewModel MVC, там логика атрибутов пролезла потому, что по ней строится клиентская валидация автоматизированно умеет. Я действую наоборот, на границе, перед входом в домен, проверяю и валидирую все, атрибутами и дополнительными валидаторами, по необходимости, а дальше, в домене, уже оперирую чистыми моделями, считая, что они валидацию прошли и корректны. Допустим, если я получаю по ключу значение из какого-то хранилища, где он должен быть, то я делаю First(), а не FirstOrDefault(). Т.е. другими словами гарантированно генерирую исключение. Но подход с логикой валидации в одном месте исключительно правильный, надо к нему стремиться. Но я его вижу на базе атрибутов + обработчика этих правил (универсального) без реализации одного и того же в виде списка if — ов и так далее.
        +1
        Вдогонку, не успел отредактировать, прекрасный цикл статей намечается, продолжайте, пожалуйста!
          +1
          А не получается ли, что представление должно знать о том, что для домена нужно валидировать — ведь это не его обязанность, по хорошему? Кроме того, в командной разработке вполне может сложиться ситуация, что кто-то где-то когда-то не вспомнит о какой-то из валидаций при использовании домена (т.е. ответственность за стабильность кода перекладывается на использующего). Вам так не кажется?
            +1
            У меня нет личных проектов, вся разработка от 2 до 15 человек в команде. Но я с вами полностью согласен, бывает — забывают, но пишутся UT, где быстро вспоминают, все же редкость какая-то сложная логика валидации, в основном это наличие, промежуток, даже регулярка редкость. Да, представление действительно должно знать о том, какие правила валидации есть в домене, по одной простой причине, с представлением работает человек и его не интересует моя внутренняя логика, архитектура, ему интересно как можно скорее заполнить формы и видеть свои нарушения сразу, а не после отправки данных. Приходится искать консенсус.
              0
              Можно по идее попробовать сделать ajax-хелпер, который пинает логику валидации конкретного типа. Тогда мы себя не ограничиваем тем, что позволяют сделать атрибуты.
                0
                Хорошее покрытие логики тестами действиетльно должно помогать, да. А по поводу второй части — не лучше ли, чтобы представление знало о наличии валидации, но сама валидация была скрыта внутри домена? Опять же, валидация при каждом обращении к домену это явное нарушение DRY (если каждый объект не вызывается ровно один раз).
                Хотя, возможно, у вас хватает и такого способа, раз валидация не очень сложная.
            +7
            Пример с Customer и методами-сеттерами, имхо, высосан из пальца. Почему бы просто не разместить валидацию в коде сеттера свойства?
              –1
              Мне кажется плохим наделять Setter такими свойствами валидации. Какая-то двуответственность (и более) получается. Но я не автор, может он что-то иное скажет. Но мне подсознательно видится это плохим решением.
                +4
                Отнюдь, как раз для этого и придуманы сеттеры:
                Properties have many uses: they can validate data before allowing a change; they can transparently expose data on a class where that data is actually retrieved from some other source, such as a database; they can take an action when data is changed, such as raising an event, or changing the value of other fields.
                  0
                  Да никто не против, что их так можно использовать, что MSDN вам и пишет. Вопрос в целесообразности использования такого подхода при каких-то цепочках валидации, зависимости одной проперти от другой и так далее. По мне сеттер должен быть сеттером.
                    +4
                    Вопрос в целесообразности использования такого подхода при каких-то цепочках валидации, зависимости одной проперти от другой и так далее.

                    В таких случаях надо не метод ChangeSmth делать, а разносить присвоение и валидацию. А если валидация свойства атомарна (не зависит от других свойств), то ее целесообразно делать в сеттере.
                      +1
                      > По мне сеттер должен быть сеттером.

                      Сеттер — это банальный метод, который запускается при попытке присвоить параметру значение. То есть, методы из примера — ChangeEmail и ChangeName — это лишние сущности, которые должны быть запрятаны в set, заодно избавив от двух проверок в конструкторе и приведя решение к DRY.

                      Также мне лично непонятно разночтение между приватными сеттерами параметров, но публичными ChangeEmail и ChangeName. Это ставит в ступор, как минимум.
                  0
                  Отдельные методы-сеттеры сделаны для большей выразительности. Всю ту же логику можно написать и в сеттере свойства, безусловно, разницы в коде при этом не будет.
                    0
                    Еще раз посмотрел на код. Да, вы правы.
                    Старался сделать примеры как можно проще, не хотел накручивать какую-то логику поверх обычного изменения имейла и нэйма.
                    0
                    Валидацию не всегда можно уместить в сеттер. Более-менее сложные модели часто имеют правила, распространяющиеся на несколько свойств.

                    Ситуацию с проверками в сеттере можно довести до абсурда:
                    class Range
                    {
                        private int _min;
                        private int _max;
                    
                        public int Min
                        {
                            get { return _min; }
                            set { if(value > Max) throw new InvalidOperationException(); _min = value; }
                        }
                    
                        public int Max
                        {
                            get { return _max; }
                            set { if (value < Min) throw new InvalidOperationException(); _max = value; }
                        }
                    }
                    

                    Получаем класс, свойства которого надо задавать в определённом порядке. Мне подобный код как-то попадался, работать с ним было очень тяжело.
                      +1
                      Если валидация захватывает несколько свойств, ее нужно выносить в отдельный метод, который вызывается после присвоения всех свойств (либо, если возможно — делать метод, который меняет несколько свойств разом).
                        0
                        У вашего примера есть принципиальное отличие — вы допускаете, что объект может в какой-то момент быть инициализирован лишь частично, и следовательно быть непригодным к использованию. Тогда, как сказал lair выше, делается отдельный метод валидации, или атомарный сеттер для связанных свойств.
                      +5
                      Обратите вниманите, что конструктор класса Email закрыт, так что единственный способ создать его экземпляр — использовать статический метод Create, который проводит всю необходимую валидацию.

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

                      [HttpPost]
                      public ActionResult CreateCustomer(CustomerInfo customerInfo)
                      {
                          Result<Email> emailResult = Email.Create(customerInfo.Email);
                          Result<CustomerName> nameResult = CustomerName.Create(customerInfo.Name);
                       
                          if (emailResult.Failure)
                              ModelState.AddModelError(“Email”, emailResult.Error);
                          if (nameResult.Failure)
                              ModelState.AddModelError(“Name”, nameResult.Error);
                          ...
                      }
                      


                      Теперь вместо биндинга «из коробки» нужно совершить кучу ручных действий. А если свойств десятки?

                      Более строгая система типов. Компилятор работает на нас с удвоенной силой: теперь невозможно ошибочно присвоить свойству типа Email объект типа CustomerName, такой код не будет скомпилирован.

                      Но при этом для других свойств (скажем, City и Country) такой проверки нет, и теперь программисту нужно помнить про два разных механизма. Не ужас, но неудобно.

                      Ну и на самом деле, есть более фундаментальный вопрос к этому подходу. Он опирается на ту идею, что бизнес-сущность не может иметь в себе данные, не удовлетворяющие правилам валидации. Но в долгоживущих системах правила валидации меняются и эволюционируют, и в какой-то момент может так случиться, что данные, выдаваемые из БД, нынешним правилам валидации не соответствуют, но отображать их все еще надо (сохранять обратно при этом нельзя, но это второй вопрос). Описанный в статье подход этого просто не позволит.
                        0
                        По поводу биндинга — можно и универсальное решение написать, будет почти из коробки (почти — потому что в коробку-то надо самому положить).
                          0
                          Можно, вопрос усилий. Для классов со статическими фабриками это, скажем так, требует времени.
                            +1
                            По ходу, для продвижения идеи в массы нужно сделать небольшой фреймворк для вкручивания поддержки этого счастья в ASP.NET.
                            0
                            Good points.

                            1) По поводу ручного биндинга — вы правы, здесь теряется часть встроенного функционала, который есть в ASP.NET. Это вопрос взвешивания «за» и «против». Для меня плюсы более выразительной доменной модели перевешивают минусы необходимости писать подобный код вручную. Для более простых проектов вполне можно отказаться от этого подхода и делать по старинке.

                            Но при этом для других свойств (скажем, City и Country) такой проверки нет, и теперь программисту нужно помнить про два разных механизма. Не ужас, но неудобно.

                            Если у City и Country есть какие-то более-менее сложные инварианты, то их тоже стоит обернуть в классы-обертки.

                            2) По поводу невалидных данных в БД — отличный point. Есть два распространенных подхода к проблеме. Первый — вместе с ужесточением инвариантов писать скрипты для миграции данных в БД, чтобы они соответствовали новым инвариантам. Второй — создавать отдельный класс, к примеру EmailStrong для хранения имейлов, инварианты в которых были ужесточены и не давать присваивать кастомерам объекты старого Email, только нового. Со временем, когда БД придет в соответствие с новыми требованиями, старый Email удаляется, новый EmailStrong переименовывется в Email. Второй вариант сложнее, я как правило пользуюсь первым.
                              0
                              Если у City и Country есть какие-то более-менее сложные инварианты, то их тоже стоит обернуть в классы-обертки.

                              Нет у них инвариантов, поэтому не будет классов-оберток, поэтому нельзя выработать привычку к type safety net.

                              Первый — вместе с ужесточением инвариантов писать скрипты для миграции данных в БД, чтобы они соответствовали новым инвариантам.

                              К сожалению, это возможно далеко не всегда.

                              Второй — создавать отдельный класс, к примеру EmailStrong для хранения имейлов, инварианты в которых были ужесточены и не давать присваивать кастомерам объекты старого Email, только нового.

                              Я боюсь, что это не «сложнее», а «сильно сложнее», особенно учитывая, что где-то присваивать можно (мало ли, мы данные из системы в систему гоним). Опять-таки, в систему типов этот запрет присвоения уже не обернешь, снова боль.
                                0
                                особенно учитывая, что где-то присваивать можно (мало ли, мы данные из системы в систему гоним). Опять-таки, в систему типов этот запрет присвоения уже не обернешь, снова боль.

                                Обернуть можно. Где-то метод принимает старый Email, а где-то — только новый EmailStrong, который является наследником старого Email.
                                  0
                                  … и как понять, какой метод использовать в какой момент? А учитывая, что лучше все-таки использовать сеттеры, а не методы — так и вовсе весело.
                                    0
                                    Понять — по сигнатуре метода. Т.е. какие-то методы будут работать только с новыми имейлами, для других — мы оставляем как было.

                                    лучше все-таки использовать сеттеры, а не методы

                                    Здесь как раз пригодится сделать небольшие методы-сеттеры для обозначения новых инвариантов класса.
                                      0
                                      Т.е. какие-то методы будут работать только с новыми имейлами, для других — мы оставляем как было.

                                      А что мешает использовать методы со старыми сигнатурами?

                                      Здесь как раз пригодится сделать небольшие методы-сеттеры для обозначения новых инвариантов класса.

                                      … и теперь сломался ранее работавший маппинг.
                                        0
                                        А что мешает использовать методы со старыми сигнатурами?

                                        Ну мы же хотим чтобы кастомерам можно было присваивать только имейлы с новыми инвариантами? Если так, то можно на уровне компилятора обозначить это изменение.

                                        … и теперь сломался ранее работавший маппинг.

                                        Маппинг куда? Если речь про ORM, то они умеют памить на protected setter-ы. В EF это посложнее сделать, в NH — попроще.
                                          0
                                          Ну мы же хотим чтобы кастомерам можно было присваивать только имейлы с новыми инвариантами? Если так, то можно на уровне компилятора обозначить это изменение.

                                          Хотим. Но если мы для обратной совместимости оставили возможность присвоить «старые» емейлы, как мы запретим программисту использовать эти механизмы?

                                          Маппинг куда?

                                          Куда угодно. На вью-модели, на DTO.
                                            +1
                                            Запретить не получится. Но я бы поспорил с посылкой этого утверждения. Программисты-коллеги — друзья, а не враги, цель нового класса EmailStrong — не запретить им что-то делать, а подсказать, направить на правильный путь. Ну и плюс общение между коллегами должны быть intensive, чтобы все были в курсе нововведений.
                                              0
                                              Ну и плюс общение между коллегами должны быть intensive, чтобы все были в курсе нововведений.

                                              К сожалению, чем больше проект (и коллектив), тем это сложнее.
                                              0
                                              как мы запретим программисту использовать эти механизмы?
                                              Запретить не запретим, но если добавить запашку — Deprecated?
                                                0
                                                Тогда эти же примечания будут показываться в других местах, и как следствие, перестанут восприниматься.
                              +3
                              Customer — это объект из бизнес логики приложения.
                              CustomerInfo — это ваша view model.
                              Это абсолютно разные части приложения и они могут иметь разную валидационную логику. Они же в принципе могут сильно различаться (просто в вашем примере они одинаковые).
                              Поэтому вы принцип DRY не нарушаете.

                              CustomerInfo — это данные от пользователя, их нуно проверить на корректность (длинна, формат, обязательные/не обязательные поля и т.д.)

                              Customer — скорее всего я бы не позволял в контроллере делать new Customer… Я бы сделал CustomerService.Create
                              И сдесь может быть довольно много бизнес логики, помимо валидации. Например при создании Customer можно проверить что он уникален. Или предположим, что у нас еще есть сущьность Business, у каждого Business может быть много Customer, и Customer должен быть уникален в пределах Business.
                                0
                                Result<Email>
                                Не является ли это анти-паттерном в C#? Такой тип идеален в Rust, там нет исключений, но в C# используются две модели — исключения и bool TryXXX(..., out yyy).
                                  +2
                                  Для задачи валидации данных — нет, не является. Исключения слишком дорогие чтобы вызывать их постоянно — а bool TryXXX(..., out yyy) возвращает слишком мало информации в случае ошибки.
                                    0
                                    Исключения — да, понятно. Меня смутил пример, уж больно Result похож на собрата из Rust.
                                    +1
                                    Использовать исключения для валидации — плохая практика. Тут более подробно на эту тему: habrahabr.ru/post/263685
                                    0
                                    Что-то я не понял: первую статью пишете про immutability, а вторая статья показывает mutable класс Customer, как до так и после рефакторинга… Зачем?
                                      +1
                                      Потому что вторая статья — не про неизменяемость…
                                    • НЛО прилетело и опубликовало эту надпись здесь
                                        +2
                                        Никакого паттерна вы не нарушаете. Более того, сеттеры свойств были созданы в том числе для таких вот проверок.

                                        Но есть несколько проблем.
                                        1. Задача «собрать все ошибки валидации» при таком подходе превращается в кошмар.
                                        2. Если проверка валидности не раскладывается на отдельные проверки независимых полей — то все становится очень весело (выше был пример с границами диапазона).
                                        3. Если значение свойства по умолчанию невалидно, то подход просто не работает.
                                        0
                                        Насколько мне известно, эволюция программирования шла от использования кодов возвратов к использованию исключений. Печально, что статья описывает как раз такой «шажище» назад.
                                          +1
                                          Вы совершенно зря считаете, что развитие программирования — это линейный эволюционный процесс. Многие вещи, которые были предложены раньше, сейчас снова становятся актуальны.

                                          Впрочем, предлагаемая в статье монада Try — это не код возврата, это пламенный привет от функционального програмирования и явных контрактов.
                                            0
                                            Код возврата — это никому не понятное число… Result.Fail же принимает в качестве параметра строку на человеческом языке. Это не шаг назад, это шаг в сторону.
                                            0
                                                [Required(ErrorMessage = “E-mail is required”)]
                                                [RegularExpression(@”^([\w\.\-]+)@([\w\-]+)((\.(\w){2,3})+)$”, ErrorMessage = “Invalid e-mail address”)]
                                                [StringLength(100, ErrorMessage = “E-mail is too long”)]
                                                public string Email { get; set; }

                                            Этот код работал с ORM «из коробки». Можете привести пример как вы рекомендуете мапить обертки?
                                              +1
                                              Здесь пример того как я обычно это делаю: github.com/vkhorikov/FuntionalPrinciplesCsharp/blob/master/New/CustomerManagement.Logic/Model/Email.cs
                                                0
                                                А если Customer'у потребуется менять email, то добавляем такое?
                                                public void UpdatePrimaryEmail(Email email)
                                                {
                                                    _primaryEmail = email;   
                                                }
                                                
                                                public void UpdateSecondaryEmail(Email email)
                                                {
                                                    _seconaryEmail = email;   
                                                }
                                                
                                                public void ClearSeconaryEmail()
                                                {
                                                    _seconadryEmail = null;
                                                }
                                                  +1
                                                  Да, но не обязательно отдельными методами, property setters тоже подойдут.

                                            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                            Самое читаемое