инжектирование придётся настраивать в самих тестах, а по сути это будет имитация инжектирования ради самого процесса — контекст то один, и конфигурируется он в самом тесте
Именно это и есть самый настоящий юнит тест, в котором зависимости настроены на поведение, тестируемое в конкретном кейсе. Несколько вариаций поведения зависимостей (сущность найдена или не найдена) — пишем еще юнит тесты с измененной логикой зависимостей
Не совсем так
DbContext желательно передавать как зависимость, причем отдельный экземпляр желательно создавать на каждый LifetimeScope, говоря терминами IoC контейнеров. А еще лучше брать из пула подключений, но этим всем обычно занимается asp net core и разработчик не забивает себе этим голову. Причин несколько, главная из которых — нельзя шарить один контекст на несколько, скажем, api запросов, потому что контекст один, а реальное состояние могло поменяться. Плюс EntityFramework Core не допускает параллельного выполнения запросов в рамках одного DbContext (читай — подключения к БД).
Нужно понимать, что репозиторий (как паттерн), хоть программно и является обычным сервисом, но все таки имеет свои особенности, связанные именно с тем, что под капотом у него есть конкретное состояние и взаимодействие с «внешним миром» (БД), который может изменится в любой момент времени, вплоть до недоступности.
Исходя из этого, Persistence, как слой архитектуры, зачастую выносится отдельно и является подвидом инфраструктурного слоя.
По этому DbContext обычно создается на весь LifetimeScope.
К примеру, в рамках WebApi — это http запрос. Создается один DbContext, который инжектируется во все (требующие DbContext) зависимости этого запроса и используется на столько запросов к БД, сколько нужно. После завершения http запроса DbContext закрывается или передается обратно в пул.
А юнит тестах с InMemory провайдером — на каждый тест создается новый, чистый DbContext, который уже в тесте конфигурируется под потребности теста. Это очень быстро и вполне нормально
Если вы хотите автоматизировать тестирование БД, то тестировать нужно конкретно те методы, которые DAL предоставляет другим слоям. Причем порядок вызова тестов не должен иметь никакого значения. В каком порядке методы DAL будут вызывать другие слои — ни DAL, ни тесты совершенно не должно волновать. Если нужно вытащить юзера по емейлу — вот вам метод, а разбираться, возвращена ли Entity (если есть) или null (если нет юзера с таким емейлом) — это задача не DAL, а того слоя, который вызвал метод. И юнит тесты с моками с этим прекрасно справляются. Плюс, для тестирования DAL тащить SQL Server Express совершенно необязательно, когда есть SQLite, а в dotnet core давным давно появился InMemory провайдер специально для тестов.
Если вам нужна персистентность DAL в юнит тестах — у меня для вас плохие новости.
Юнит тесты вообще не должны зависеть от любого другого слоя, моки именно для этого и существуют.
Моки — не уродливы, не раздувают код, и, тем более, не являются дублированием кода или копи-пастой. Они имитируют зависимости так, как это нужно для конкретного теста. Если DAL может выдать разную реакцию на вызов его метода — пишем несколько тестов.
Мало того, если у вас успешность прохождения тестов зависит от порядка их вызова — это вообще ужас-ужас.
Интеграционные же тесты лучше проводить не на машинах девелоперов (хотя это не запрещено), а, например, дерганьем реальных апи в специально подготовленном для этого окружении с реальной БД, настройками, связями сервисов, DI и т.д.
А так получается раздутый юнит тест, натянутый на глобус и названный интеграционнным
SSD может стоять как в ПК, так и в лептопах.
Имелось в виду, что перезаписывание данных на SSD — совершенно бесполезная затея, так как организация хранения данных на физическом уровне в корне отличается от таковой для HDD.
Грубо говоря, если мы хотим программно перезаписать некие данные в ячейке А, то новые данные могут попасть в ячейку Б, а ячейка А будет помечена для очистки командой TRIM и физически данные будут удалены в случайный момент времени по команде ОС или контроллера.
PS Но если через api OS прочитать «перезаписанные» сектора — то мы получим новые данные, а не старые, так что минимальный смысл тут все таки есть. Но все равно, физически старые данные на SSD не удаляются по команде «перезаписать вот эти байты»
Как-то понадобилось мне в базе держать данные нескольких пользователей (допустим, заметки) и запрещать показывать чужие.
А это не задача FluentValidation. Это — задача слоя бизнес логики, в вашем примере — QueryHandler'а, который должен либо самостоятельно определить текущего юзера, либо получить эти данные из Query, а затем либо делегировать выборку репозиторию (читай — sql запросу), либо самостоятельно отфильтровать данные.
FluentValidation — это библиотека исключительно для Presentation Layer и предназначена только для первичной валидации данных, пришедших от пользователя. Например, что email — это email, а не случайная строка, или что возраст — больше нуля, но меньше 120 и, в случае ошибки — детально сообщить об этом юзеру. С чем и справляется блестяще.
Бизнес слою же вообще желательно работать только с ValueObject, которые должны быть реализованы так, чтобы их в принципе было невозможно создать невалидными, но это уже оффтопик.
И, к вашему примеру, добавлю еще, что RequestHandler не должен возвращать DTO. Опять же, потому что RequestHandler — это слой бизнес логики (модели), а DTO — это Presentation Layer. По этому меняем NoteDto на Note (Entity), в контроллер добавляем AutoMapper (или собственный маппер Note -> NoteDto) и радуемся тому что слои не залезают друг в дружку, а код — чистый, красивый и лаконичный.
тоже плохой пример. Зачем в описании теста название тестируемого сервиса и метода?
Название сервиса можно вынести и в родительский describe, а в этом написать примерно такое:
Я бы еще попросил составить список, для условных отцов маленьких детей :), из доступного к покупке клея — чтоб машинки и игрушки клеить и потом не отравить случайно.
Если в сервисе становиться слишком много зависимостей — очевидно, что сервис стал слишком сложным и пришло время его рефакторить, т.к. скорее всего нарушаются KISS и Single Responsibility
Да, действительно, вы правы. Это частный случай, когда экземпляр класса является статическим полем/свойством этого же класса.
Получается, что по приведенной выше ссылке Microsoft себе противоречит, но здесь описывает правильно: Static Classes and Static Class Members
Static members are initialized before the static member is accessed for the first time and before the static constructor, if there is one, is called.
Спасибо за видео, оно действительно весьма полезно, но я его уже смотрел, практически сразу после его появления. Тоже рекомендую всем к просмотру.
1. По поводу сигналов — это очень на любителя. Лично я не испытываю проблем от вызова методов в проектах +- средней сложности. Тут никто никого не ограничивает в выборе инструмента. Будет нужно — рассмотрю необходимость сигналов, но статья — не про это.
2. FluentValidation — штука прикольная, но, я уже это объяснял — правила создания доменного объекта должны задаваться на уровне домена, а не на уровне Presentation. FluentValidation тут разве что поможет несколько упростить код контроллеров, но брать на себя ответственность за логику валидации не должно. К тому же я не задавался целью преподнести прям «production-ready» код, поскольку тема — VO + EF Core. Соответственно и контроллер — максимально упрощен для понимания.
3. Статья — не про DDD в целом, а про Value Objects, которые являются частью DDD, EF Core, которая часто используется как ORM, в том числе и в DDD проектах, и как объединить вместе VO и EF Core
Перед тем, как написать коммент — я, естественно, попробовал. Все прекрасно сохранилось, потому что эти атрибуты — немножко не для предотвращения таких операций
Я бы уточнил, что тут речь идет не столько о вложенных циклах, сколько о многомерных массивах.
Иначе, при вложенных циклах, но по одномерным массивам в вызове относительно медленного .GetLength(0/1) нет никакого смысла, вполне достаточно arr1.Length и arr2.Length
1. Если свойство доступно для изменения, то это будет просто SomeProperty { get; set; } и доступно всем. Никаких методов для этого не нужно. Отдельный метод нужен только, если на изменение свойства завязана какая нибуть логика. Скорее всего это будет доменный сервис или метод в Entity (в простых случаях) и сеттер становится private или internal.
2. Ничего не мешает передать фильтры в Repository и конкретный запрос с фильтрами формировать уже там. С IQueryable это очень просто.
3. Логика изменения паспортных данных выносится в доменный сервис, которым пользуются только админы. Ограничить можно разными сборками (доменные сервисы не обязательно должны быть в одной сборке с Entity), но если это один VS Solution — программисты клиентской части все равно будут видеть этот сервис.
Code Review, кстати, никто не отменял.
4. Если я вас правильно понял, то AbcDataService — это PersonRepository. Репозиторий — это же обычный сервис, только находится в Persistence и отвечает за сохранение и выборку данных, за что и получил отдельное название.
5. Entity Framework — это уже раскрытие реализации. В проекте в дополнение может использоваться легковесный Dapper для быстрой выборки, могут быть чистые SQL запросы через ADO.Net, загрузка из csv файлов, что угодно — и все это в одном проекте. Persistence — это черный ящик, и что там внутри — остальные слои знать не должны.
6. Подразумевается, что параметр не может быть null. Если это произошло в домене — однозначно что то пошло не так и бизнес-процесс далее не может выполнятся, что исключение в том числе и делает. Опять же, если это произошло в домене — это нештатная ситуация, требующая разбора полетов. Presentation layer (как и другие слои) должен проверить все данные перед тем, как передавать их в доменные сервисы. Не проверил и пропустил null — это баг. А сам по себе if (...) в 100 раз не замедлит выполнение
Для каждого свойства каждой Entity будете создавать методы-сеттеры и копи-пастить правила валидации? Зачем? Чтоб написать кучу одинакового кода и потом искать во всему проекту, если Name со 100 увеличится до 150 символов? В Entity валидация данных — явно лишняя. Валидация логики — да, но не данных.
И другое — если среди кода я вижу Name, то я точно знаю, что это Name, а не PhoneNumber или EmailAddress, хотя формально все они — строки. А Age — это именно возраст, а не рост в сантиметрах.
В этом методе программист элементарно может перепутать местами параметры:
void SomeMethod(string name, string phoneNumber);
А в этом перепутать не получится при всем желании:
И насчет ValueObjects, я не знаю, откуда взято то, что они immutable.
Да, их не обязательно делать immutable. На приктике даже само название — Value Object — говорит о том, что это некое значение. Новое значение — новый Value Object.
Еще дополню, что мы можем передать, скажем, номер телефона, в доменный сервис и совершать с ним какие либо манипуляции без привлечения Entity. Если это string — как сервис может быть уверен, что значение корректно, а не какая то ерунда по типу «qwerty$%^»? С VO PhoneNumber такого в принципе не может случится
Если DDD не работает в ваших проектах — это не означает, что DDD не работает совсем.
У меня был не один проект с DDD, все работает отлично и продолжает развиваться. DDD — штука сложная и понимать это должна вся команда, а не один тимлид. Возможно, по этому у вас DDD и не работает.
Допустим, что имя не должно именяться, только фамилия
Person.cs
public class Person {
...
public PersonalName PersonalName { get; private set; }
public void UpdateLastName(Name lastName) {
if (lastName == null) {
throw new ArgumentNullException(nameof(lastName));
}
this.PersonalName = new PersonalName(
this.PersonalName.FirstName,
lastName);
}
}
возраст — вообще элементарно:
person.Age = new Age(newAge);
Логика фильтрации — в репозитории. В статье есть пример — IPersons.GetOlderThan(Age age).
Домен — это ядро всего приложения, все его возможности доступны всем слоям, которые с ним работают. Назовите хоть одну причину, почему подсистема А может иметь доступ к IPersons.GetOlderThan, а подсистема Б — не может. Это ведь одно приложение. Если уж совсем нужно — это можно сделать на уровне сборок, но никакого смысла я в этом не вижу.
Где каша? Почему мы не должны знать про Name и Age у Person? Это же публичный доменный объект.
Если вы имели в виду преобразование Person -> PersonModel, то тут я согласен — обычно это делает отдельный PersonModelBuilder (сервис уровня Presentation layer). В статье я это опустил, потому что статья не про правильное программирование с DDD в целом, а конкретно про Value Objects.
Я это упоминал в материале. Мы не должны раскрывать детали реализации Persistence. Использование DbContext и DbSet очевидно нарушает это правило. Если проект не использует DDD — конечно же никакие репозитории и UoW не нужны.
Мое мнение — красивые архитектуры очень даже работают, если прилагать для этого некоторые усилия, а не формошлепить. DDD вносит очень заметный оверхед в разработку, но оно того стоит на средних и больших проектах. Для сайтов-визиток это явно не нужно.
Name и Age (как VO) — это доменные объекты и могут быть (и наверняка будут) переиспользованы в других Entity. Для всех будете атрибуты писать и потом везде исправлять, если вдруг требования поменяются?
Value Objects — не обязательно должны быть только частью Entity. В домене они могут использоваться и сами по себе.
The System.ComponentModel.DataAnnotations namespace provides attribute classes that are used to define metadata for ASP.NET MVC and ASP.NET data controls.
Т.е. это Presentation layer, а Entity — это доменный объект, а не DTO.
VO же полностью исключают возможность передачи некорректных данных в домен.
Ну и, ваш Person, если убрать private set — это обычная анемичная модель, со всеми ее преимуществами и недостатками
«нюансы маппинга» — не совсем корректно написал, скорее «нюансы ограничений при создании объектов».
Presentation layer не должен брать на себя ответственность устанавливать правила валидации. Его задача — провалидировать данные, т.е. спросить домен, нравится ли ему такое имя/возраст или нет, и если нет — сообщить об ошибке. Потому что Name/Age — это доменные объекты и только домену решать, подходит ли ему Age 25 или Name «Alex»
Именно это и есть самый настоящий юнит тест, в котором зависимости настроены на поведение, тестируемое в конкретном кейсе. Несколько вариаций поведения зависимостей (сущность найдена или не найдена) — пишем еще юнит тесты с измененной логикой зависимостей
DbContext желательно передавать как зависимость, причем отдельный экземпляр желательно создавать на каждый LifetimeScope, говоря терминами IoC контейнеров. А еще лучше брать из пула подключений, но этим всем обычно занимается asp net core и разработчик не забивает себе этим голову. Причин несколько, главная из которых — нельзя шарить один контекст на несколько, скажем, api запросов, потому что контекст один, а реальное состояние могло поменяться. Плюс EntityFramework Core не допускает параллельного выполнения запросов в рамках одного DbContext (читай — подключения к БД).
Нужно понимать, что репозиторий (как паттерн), хоть программно и является обычным сервисом, но все таки имеет свои особенности, связанные именно с тем, что под капотом у него есть конкретное состояние и взаимодействие с «внешним миром» (БД), который может изменится в любой момент времени, вплоть до недоступности.
Исходя из этого, Persistence, как слой архитектуры, зачастую выносится отдельно и является подвидом инфраструктурного слоя.
По этому DbContext обычно создается на весь LifetimeScope.
К примеру, в рамках WebApi — это http запрос. Создается один DbContext, который инжектируется во все (требующие DbContext) зависимости этого запроса и используется на столько запросов к БД, сколько нужно. После завершения http запроса DbContext закрывается или передается обратно в пул.
А юнит тестах с InMemory провайдером — на каждый тест создается новый, чистый DbContext, который уже в тесте конфигурируется под потребности теста. Это очень быстро и вполне нормально
Если вы хотите автоматизировать тестирование БД, то тестировать нужно конкретно те методы, которые DAL предоставляет другим слоям. Причем порядок вызова тестов не должен иметь никакого значения. В каком порядке методы DAL будут вызывать другие слои — ни DAL, ни тесты совершенно не должно волновать. Если нужно вытащить юзера по емейлу — вот вам метод, а разбираться, возвращена ли Entity (если есть) или null (если нет юзера с таким емейлом) — это задача не DAL, а того слоя, который вызвал метод. И юнит тесты с моками с этим прекрасно справляются. Плюс, для тестирования DAL тащить SQL Server Express совершенно необязательно, когда есть SQLite, а в dotnet core давным давно появился InMemory провайдер специально для тестов.
Если вам нужна персистентность DAL в юнит тестах — у меня для вас плохие новости.
Юнит тесты вообще не должны зависеть от любого другого слоя, моки именно для этого и существуют.
Моки — не уродливы, не раздувают код, и, тем более, не являются дублированием кода или копи-пастой. Они имитируют зависимости так, как это нужно для конкретного теста. Если DAL может выдать разную реакцию на вызов его метода — пишем несколько тестов.
Мало того, если у вас успешность прохождения тестов зависит от порядка их вызова — это вообще ужас-ужас.
Интеграционные же тесты лучше проводить не на машинах девелоперов (хотя это не запрещено), а, например, дерганьем реальных апи в специально подготовленном для этого окружении с реальной БД, настройками, связями сервисов, DI и т.д.
А так получается раздутый юнит тест, натянутый на глобус и названный интеграционнным
Имелось в виду, что перезаписывание данных на SSD — совершенно бесполезная затея, так как организация хранения данных на физическом уровне в корне отличается от таковой для HDD.
Грубо говоря, если мы хотим программно перезаписать некие данные в ячейке А, то новые данные могут попасть в ячейку Б, а ячейка А будет помечена для очистки командой TRIM и физически данные будут удалены в случайный момент времени по команде ОС или контроллера.
PS Но если через api OS прочитать «перезаписанные» сектора — то мы получим новые данные, а не старые, так что минимальный смысл тут все таки есть. Но все равно, физически старые данные на SSD не удаляются по команде «перезаписать вот эти байты»
А это не задача FluentValidation. Это — задача слоя бизнес логики, в вашем примере — QueryHandler'а, который должен либо самостоятельно определить текущего юзера, либо получить эти данные из Query, а затем либо делегировать выборку репозиторию (читай — sql запросу), либо самостоятельно отфильтровать данные.
FluentValidation — это библиотека исключительно для Presentation Layer и предназначена только для первичной валидации данных, пришедших от пользователя. Например, что email — это email, а не случайная строка, или что возраст — больше нуля, но меньше 120 и, в случае ошибки — детально сообщить об этом юзеру. С чем и справляется блестяще.
Бизнес слою же вообще желательно работать только с ValueObject, которые должны быть реализованы так, чтобы их в принципе было невозможно создать невалидными, но это уже оффтопик.
И, к вашему примеру, добавлю еще, что RequestHandler не должен возвращать DTO. Опять же, потому что RequestHandler — это слой бизнес логики (модели), а DTO — это Presentation Layer. По этому меняем NoteDto на Note (Entity), в контроллер добавляем AutoMapper (или собственный маппер Note -> NoteDto) и радуемся тому что слои не залезают друг в дружку, а код — чистый, красивый и лаконичный.
Название сервиса можно вынести и в родительский describe, а в этом написать примерно такое:
следующий тест, на неуспешную загрузку:
Гораздо же понятнее, когда предназначение теста и его поведение описывается «людским» языком, а не просто «сервис — метод»
Получается, что по приведенной выше ссылке Microsoft себе противоречит, но здесь описывает правильно:
Static Classes and Static Class Members
Static Constructors
1. По поводу сигналов — это очень на любителя. Лично я не испытываю проблем от вызова методов в проектах +- средней сложности. Тут никто никого не ограничивает в выборе инструмента. Будет нужно — рассмотрю необходимость сигналов, но статья — не про это.
2. FluentValidation — штука прикольная, но, я уже это объяснял — правила создания доменного объекта должны задаваться на уровне домена, а не на уровне Presentation. FluentValidation тут разве что поможет несколько упростить код контроллеров, но брать на себя ответственность за логику валидации не должно. К тому же я не задавался целью преподнести прям «production-ready» код, поскольку тема — VO + EF Core. Соответственно и контроллер — максимально упрощен для понимания.
3. Статья — не про DDD в целом, а про Value Objects, которые являются частью DDD, EF Core, которая часто используется как ORM, в том числе и в DDD проектах, и как объединить вместе VO и EF Core
Иначе, при вложенных циклах, но по одномерным массивам в вызове относительно медленного .GetLength(0/1) нет никакого смысла, вполне достаточно arr1.Length и arr2.Length
2. Ничего не мешает передать фильтры в Repository и конкретный запрос с фильтрами формировать уже там. С IQueryable это очень просто.
3. Логика изменения паспортных данных выносится в доменный сервис, которым пользуются только админы. Ограничить можно разными сборками (доменные сервисы не обязательно должны быть в одной сборке с Entity), но если это один VS Solution — программисты клиентской части все равно будут видеть этот сервис.
Code Review, кстати, никто не отменял.
4. Если я вас правильно понял, то AbcDataService — это PersonRepository. Репозиторий — это же обычный сервис, только находится в Persistence и отвечает за сохранение и выборку данных, за что и получил отдельное название.
5. Entity Framework — это уже раскрытие реализации. В проекте в дополнение может использоваться легковесный Dapper для быстрой выборки, могут быть чистые SQL запросы через ADO.Net, загрузка из csv файлов, что угодно — и все это в одном проекте. Persistence — это черный ящик, и что там внутри — остальные слои знать не должны.
6. Подразумевается, что параметр не может быть null. Если это произошло в домене — однозначно что то пошло не так и бизнес-процесс далее не может выполнятся, что исключение в том числе и делает. Опять же, если это произошло в домене — это нештатная ситуация, требующая разбора полетов. Presentation layer (как и другие слои) должен проверить все данные перед тем, как передавать их в доменные сервисы. Не проверил и пропустил null — это баг. А сам по себе if (...) в 100 раз не замедлит выполнение
И другое — если среди кода я вижу Name, то я точно знаю, что это Name, а не PhoneNumber или EmailAddress, хотя формально все они — строки. А Age — это именно возраст, а не рост в сантиметрах.
В этом методе программист элементарно может перепутать местами параметры:
А в этом перепутать не получится при всем желании:
Да, их не обязательно делать immutable. На приктике даже само название — Value Object — говорит о том, что это некое значение. Новое значение — новый Value Object.
Еще дополню, что мы можем передать, скажем, номер телефона, в доменный сервис и совершать с ним какие либо манипуляции без привлечения Entity. Если это string — как сервис может быть уверен, что значение корректно, а не какая то ерунда по типу «qwerty$%^»? С VO PhoneNumber такого в принципе не может случится
У меня был не один проект с DDD, все работает отлично и продолжает развиваться. DDD — штука сложная и понимать это должна вся команда, а не один тимлид. Возможно, по этому у вас DDD и не работает.
Домен — это ядро всего приложения, все его возможности доступны всем слоям, которые с ним работают. Назовите хоть одну причину, почему подсистема А может иметь доступ к IPersons.GetOlderThan, а подсистема Б — не может. Это ведь одно приложение. Если уж совсем нужно — это можно сделать на уровне сборок, но никакого смысла я в этом не вижу.
Если вы имели в виду преобразование Person -> PersonModel, то тут я согласен — обычно это делает отдельный PersonModelBuilder (сервис уровня Presentation layer). В статье я это опустил, потому что статья не про правильное программирование с DDD в целом, а конкретно про Value Objects.
Мое мнение — красивые архитектуры очень даже работают, если прилагать для этого некоторые усилия, а не формошлепить. DDD вносит очень заметный оверхед в разработку, но оно того стоит на средних и больших проектах. Для сайтов-визиток это явно не нужно.
И успешно сохранить это в БД. А все потому, что это атрибуты валидации и должны использоваться в DTO (PersonModel в нашем случае).
System.ComponentModel.DataAnnotations Namespace
Т.е. это Presentation layer, а Entity — это доменный объект, а не DTO.
VO же полностью исключают возможность передачи некорректных данных в домен.
Ну и, ваш Person, если убрать private set — это обычная анемичная модель, со всеми ее преимуществами и недостатками
Presentation layer не должен брать на себя ответственность устанавливать правила валидации. Его задача — провалидировать данные, т.е. спросить домен, нравится ли ему такое имя/возраст или нет, и если нет — сообщить об ошибке. Потому что Name/Age — это доменные объекты и только домену решать, подходит ли ему Age 25 или Name «Alex»