*еще в разработке
Пример микросервиса аутентификации, покрытого тестами и реализованного с применением идей DDD, CQRS, SOLID, GRASP, Clean Architecture, Design Patterns.
Требования
- .NET SDK 8.0
- Docker
- GNU Make (необязательно)
- EF Core tools (необязательно)
Внешние зависимости
- PostgreSQL
- Redis
Разработка
В решении есть makefile
с командами (запускать из корневой директории), которые упрощают процесс разработки,
например, команды для работы с миграциями:
- Сгенерировать новую миграцию:
make add-migration {Name}
- Удалить последнюю миграцию:
make remove-migration
- Применить миграции к dev db:
make migrate-dev-db
Аутентификация:
- Вход/Получение токенов по email и паролю
- Вход/Получение токенов с помощью google аккаунта
- Регистрация нового пользователя по email и паролю
- Регистрация нового пользователя с помощью google аккаунта
- Сброс пароля (запрос + подтверждение)
- Refresh token
- Выход/инвалидация refresh token
Управление аккаунтом:
- Подтверждение email (запрос + подтверждение)
- Изменить email (запрос + подтверждение)
- Изменить пароль
- Изменить роль
- Привязать/отвязать google аккаунт
- Деактивировать аккаунт (запрос + подтверждение)
Структура решения выполнена в стиле Clean Architecture и разбита на уровни:
Доменный слой реализован изолированным и с подходом DDD. Вся бизнес-логики содержится в доменном слое, а именно в агрегатах, сущностях, объектах-значениях и т.д. Используется т.н. подход Богатой модели (Rich model), а также паттерн Information expert из GRASP - объекты, которые содержат данные сами занимаются обработкой и валидацией этих данных.
В доменной модели этого микросервиса содержится один агрегат User
. Он включает в себя сущность ExternalAccount
и объекты значения:
UserId
, Email
, FullName
, Password
, ConfirmToken
. Агрегат User
создаёт множество различных событий, связанных с действиями юзера,
например UserChangedEmail
, UserRegistered
, UsedDeactivated
и т.п. Кроме этого, агрегат User
включает в себя множество бизнес-правил,
которые реализованы в виде отдельных классов: UserEmailMustBeUnique
, PasswordMustBeStrong
и т.п.
- В слое присутствует чёткое разделение на команды и запросы (CQRS). Внутри команд используются шаблоны DDD чтобы повысить качество кода, который изменяет состояние системы. А внутри запросов наоборот - шаблоны DDD не используются с целью повышения производительности за счёт использования microORM, хранимых процедур, представлений и т.д. для запросов.
- Слой не привязан к конкретному способу аутентификации - можно подключать любые (cookies, JWT и т.д.) и использовать слой совместно с различными фреймворками.
Слой разбит на 3 проекта: Data
, JWT
, Auth
.
- В проекте
Data
содержится реализация репозиториев и паттерна UnitOfWork с помощью EF. - В проекте
JWT
содержится реализация компонентов:JwtGenerator
,JwtIdentityService
,JwtSignInManager
,RedisRefreshTokenRepository
. - В проекте
Auth
содержится реализация компонентов:PasswordHasher
,PasswordValidator
,SecureTokenizer
,GoogleAuthProvider
.
В решении содержатся функциональные, интеграционные и юнит тесты (всего 200+ тестов). Все тесты реализованы на выполнение в параллельном режиме.
- В интеграционных и функциональных тестах используются TestContainers для PostgreSQL и Redis, поэтому требуется Docker.
Функциональные тесты проверяют работу приложения по функциональным требованиям и как приложение работает с точки зрения конечного пользователя/клиента. Такие тесты имитируют действия/запросы пользователя/клиента - регистрация, вход и т.д. Такие тесты называются функциональными, потому что они проверяют, что приложение корректно выполняет все функции, которые ожидаются от него. В проекте содержатся функциональные тесты, которые покрывают логику и публичный контракт API endpoints.
Интеграционные тесты проверяют взаимодействие между различными компонентами, а также используются для тестирования инфраструктуры приложения. Такие тесты называются интеграционными, потому что они проверяют, как приложение работает в интеграции с разными компонентами, такими как базы данных, файловые системы, внешние процессы и т.д. В проекте содержатся интеграционные тесты, которые проверяют работоспособность репозиториев и EF конфигураций (EntityConfigs) в интеграции с PostgreSQL.
В решении в большинстве случаев соблюдаются принципы SOLID:
-
Single Responsibility Principle. Все обработчики (handlers) команд и запросов в решении соблюдают принцип SRP. Например, класс
RegisterByEmailAndPasswordCommandHandler
занимается только регистрацией нового юзера и сохранением его в БД, но не отправкой почты или еще чем-либо. Отправка письма, связанного с регистрацией, происходит в другом обработчикеUserRegisteredDomainEventHandler
. Таким образом класс имеет лишь одну причину для изменения. -
Open-Closed Principle. Например, метод
UserPassword.Create
в качестве параметров принимает абстракцииIPasswordStrengthValidator
,IPasswordHasher
и может менять поведение в зависимости от переданных реализаций. Таким образом класс открыт для расширения и закрыт для модификации. Т.е. можно изменять функционал за счёт добавления новых реализаций для абстракций без модификации существующего классаUserPassword
. -
Liskov Substitution Principle. В решении никакие подклассы не изменяют и не замещают контракт базового класса. Например, класс
EfUserRepository
никак не изменяет контракт своего родителяEfBaseRepository
. -
Interface Segregation Principle. Например, метод
IsEmailUnique
для проверки уникальности email юзера с помощью запроса к БД используется только в классеUserEmailMustBeUniqueRule
. Если расположить этот метод в интерфейсеIUserRepository
, то это будет считаться нарушением принципа ISP, т.к. клиенты, использующиеIUserRepository
будут зависеть от методаIsEmailUnique
, который им не нужен. А также классUserEmailMustBeUniqueRule
будет зависеть от всех остальных методов репозитория, которые ему не нужны. Поэтому методIsEmailUnique
вынесен в отдельный интерфейсIUserEmailUniquenessChecker
. Таким образом, классUserEmailMustBeUniqueRule
зависит только от методаIsEmailUnique
, а все остальные классы, использующиеIUserRepository
не зависят от этого метода. -
Dependency Inversion Principle. В решении используется подход Clean Architecture и принцип DIP для организации направленности зависимостей между модулями. Так, модули верхнего уровня
Domain
иApplication
являются ядром приложения и содержат лишь интерфейсы/контракты компонентов, которые реализуются модулями нижнего уровняInfrastructure
иAPI
. При этом модули верхнего уровня и их контракты никак не зависят от модулей нижнего уровня - наоборот - только нижние зависят от верхних. Таким образом модули верхнего уровня не зависят от конкретной инфраструктуры и легко покрываются тестами.
В решении используются некоторые популярные паттерны проектирования:
-
Mediator + Facade Patterns. В решении используется подход CQRS в связке с паттерном Mediator. Это позволяет контроллерам и другим компонентам не зависеть от конкретных обработчиков. Кроме этого, контроллеры выполнены в "тонком" стиле и занимаются только делегированием работы медиатору, поэтому такие тонкие контроллеры являются примером паттерна Facade.
-
Strategy Pattern. Например, метод
UserPassword.Create
принимает абстрактные стратегииIPasswordStrengthValidator
,IPasswordHasher
и может менять поведение в зависимости от переданных реализаций стратегий. Т.е. можно использовать разные стратегии валидации и хеширования паролей для различных ситуаций. -
Builder Pattern. В проектах тестов содержится класс
UserBuilder
, который упрощает создание объектовUser
в различных сценариях тестирования. -
Static Factory Method Pattern. В классе
User
есть статические фабричные методыRegisterByEmailAndPassword
иRegisterByGoogleAccount
, которые упрощают создание новых экземпляров классаUser
и являются единственным способом сделать это. -
Repository + UnitOfWork Patterns. В проекте
Infrastructure.Data
содержатся реализации паттернов Repository и UnitOfWork.