Структурирование проекта

Lino структурирует решения .NET так, чтобы архитектурные решения были явными уже с первого commit. Сгенерированный проект сразу содержит оркестрацию Aspire, общие блоки, тесты, настройку качества кода и понятное место для каждого сервиса, модуля, API-контракта, аспекта persistence и интеграционной границы.


В этом разделе объясняется, как lino project new, lino service new и lino module new формируют решение. Цель не только в создании папок, но и в определении границ runtime, database ownership, изоляции модулей и пути развития от простого сервиса к модульному монолиту или распределенной системе.

Создание фундамента решения

Команда lino project new создает технический фундамент нового решения .NET. Запускайте ее в пустом каталоге после установки и аутентификации CLI.

lino project new --name <ProjectName>

Аргумент <ProjectName> представляет реальное имя решения. Это имя становится частью namespaces, assemblies, путей, конфигураций, артефактов и ссылок между компонентами; выбирайте короткое, стабильное и репрезентативное имя.

Интерактивный мастер запрашивает решения, которые влияют на структуру и поведение runtime всего решения:

  • Namespace проекта: корневая техническая идентичность, используемая сгенерированными проектами.
  • Отображаемое имя: понятное имя, используемое в сгенерированных metadata и видимых пользователю местах.
  • Язык и stack: сейчас C# с .NET 10 и Aspire.
  • Анализаторы кода: включают общие packages и rules, чтобы поддерживать согласованность и качество с момента bootstrap.
  • CQRS: подготавливает слой application к разделению commands и queries с оркестрацией через выбранную mediator library.
  • Базовые классы в решении: управляет тем, будут ли общие abstractions генерироваться локально внутри решения.
  • Распределенный cache: определяет, будет ли Microsoft.Extensions.Caching.Hybrid использовать только локальную память instance или также распределенный слой с Redis, настроенный Aspire.
  • Асинхронная коммуникация: включает RabbitMQ с MassTransit и блоки messaging/outbox, используемые интеграционными событиями.
  • Язык данных: язык, используемый для описания доменных metadata во время генерации.
  • Поддерживаемые приложением культуры: ресурсы локализации, генерируемые для UI-текста, validations, errors и API responses.
  • Культура по умолчанию: основной язык, используемый, когда приложению нужен fallback.

После подтверждения Lino генерирует решение, организованное для роста. Минимальный проект начинается с Aspire, общих слоев и тестов для области Shared:

<ProjectName>/
├── <ProjectName>.slnx
├── Directory.Build.props
├── Directory.Packages.props
├── src/
│   ├── Aspire/
│   │   ├── AppHost/
│   │   │   └── <ProjectName>.AppHost.csproj
│   │   └── ServiceDefaults/
│   │       └── <ProjectName>.ServiceDefaults.csproj
│   └── Services/
│       └── Shared/
│           ├── Api/
│           │   └── <ProjectName>.Shared.Api.csproj
│           ├── Application/
│           │   └── <ProjectName>.Shared.Application.csproj
│           ├── Domain/
│           │   └── <ProjectName>.Shared.Domain.csproj
│           ├── Infrastructure/
│           │   └── <ProjectName>.Shared.Infrastructure.csproj
│           ├── Infrastructure.Persistence/
│           │   └── <ProjectName>.Shared.Infrastructure.Persistence.csproj
│           └── Integration.Events/          (если есть messaging)
│               └── <ProjectName>.Shared.Integration.Events.csproj
└── tests/
    └── Services/
        └── Shared/
            └── UnitTests/
                ├── Domain/
                │   └── <ProjectName>.Shared.Domain.UnitTests.csproj
                └── Application/
                    └── <ProjectName>.Shared.Application.UnitTests.csproj

Роль проектов Shared

Shared содержит платформенный код, общий для решения. Это не бизнес-сервис. Используйте это пространство для cross-cutting abstractions, общих ошибок, инфраструктуры локализации, базовых application contracts, persistence helpers, API extensions, host integration, observability и переиспользуемых технических utilities.

Не помещайте бизнес-правила в Shared. Если правило принадлежит конкретной capability приложения, оно должно находиться в сервисе или модуле, которому принадлежит эта ответственность. Цель Shared - уменьшать техническое повторение, а не становиться коротким путем для связывания разных доменов.

Aspire и инфраструктурные решения

AppHost компонует решение и его runtime resources. Когда добавляются Redis, RabbitMQ, SQL Server, PostgreSQL, Redis Insight, сервисы, WebApps или workers, Aspire становится локальной точкой оркестрации. Это упрощает выполнение, service discovery, logs, metrics, traces и визуализацию ресурсов во время разработки.

ServiceDefaults централизует hosting patterns, такие как service discovery, health checks, resilience, logging, metrics, tracing и интеграция OpenTelemetry. Вместо того чтобы каждый сервис настраивал это изолированно, решение начинается с общей точки композиции.

Анализаторы кода

Статические анализаторы кода проверяют код во время разработки и делают проблемы видимыми до выполнения: несогласованность стиля, хрупкие patterns, возможности для рефакторинга, возможные bugs и security alerts.

Когда вы включаете анализаторы в lino project new, решение сразу получает packages вроде StyleCop.Analyzers, SonarAnalyzer.CSharp и Roslynator.Analyzers, настроенные централизованно. Это не дает каждому проекту самостоятельно определять собственный baseline качества.

  • Улучшение качества: сохраняет код читаемым, согласованным и выровненным со стандартами решения.
  • Предотвращение ошибок: указывает на проблемы рано, до того как они попадут в ручное тестирование или production.
  • Стандартизация: уменьшает различия стиля между сервисами, модулями и командами.
  • Поддержка рефакторинга: подсвечивает возможные упрощения и возможности модернизации C#-кода.

Distributed cache, hybrid cache и Redis

Lino подготавливает решение к использованию Microsoft.Extensions.Caching.Hybrid - библиотеки Microsoft, которая централизует операции cache через HybridCache. Эта abstraction позволяет handlers, application services и infrastructure components хранить результаты queries, permissions, settings или вспомогательные данные, не размазывая детали реализации cache по коду.

Когда вы не включаете distributed cache при создании проекта, HybridCache остается доступным, но работает только с локальной памятью запущенного instance. Этот режим прост и достаточен для локальных сценариев, небольших окружений или приложений с одной replica, но каждый процесс хранит собственный cache, и данные не разделяются между instances.

Когда вы включаете distributed cache, Lino добавляет Redis в ресурсы Aspire и настраивает инфраструктуру на использование общего слоя cache. С Redis несколько instances одного сервиса могут обращаться к одному и тому же слою, уменьшая повторные чтения из баз данных, внутренних APIs или интеграций и подготавливая проект к горизонтальному масштабированию.

  • Производительность: уменьшает время ответа при повторных чтениях и вспомогательных операциях.
  • Масштабируемость: позволяет разным instances разделять кэшированные данные.
  • Доступность: снимает часть нагрузки чтения с основной базы данных.
  • Операционная стоимость: уменьшает повторяющуюся обработку в high-volume queries и интеграциях.

Это решение принимается в lino project new, потому что оно меняет фундамент окружения: ресурсы AppHost, локальные secrets, packages, конфигурацию инфраструктуры и топологию выполнения, показанную в dashboard Aspire.

Асинхронная коммуникация

Асинхронная коммуникация позволяет сервисам, модулям и компонентам реагировать на факты системы, не блокируя основной поток операции. Она особенно полезна, когда producer не должен зависеть от немедленной доступности consumer или когда действие должно запускать последующие эффекты, например notifications, projections, внешние интеграции или синхронизацию между contexts.

Когда вы включаете асинхронную коммуникацию, Lino добавляет RabbitMQ в Aspire и настраивает MassTransit вместе с блоками messaging и outbox, используемыми интеграционными событиями. События, опубликованные сервисами или модулями, представлены явными контрактами в Integration.Events и могут обрабатываться более устойчиво.

  • Производительность: позволяет основному use case продолжаться без ожидания завершения всех consumers.
  • Масштабируемость: распределяет обработку по consumers и queues, поглощая пики с большим контролем.
  • Устойчивость: включает reprocessing и уменьшает потерю сообщений в сочетании с outbox.
  • Decoupling: избегает прямой зависимости между producer и consumer, когда немедленный ответ не требуется.

Используйте интеграционные события, когда сбой consumer не должен откатывать транзакцию producer. Когда consumer должен ответить немедленно, чтобы завершить use case, предпочитайте явную синхронную интеграцию и рассматривайте availability, timeout и fallback как часть контракта.

Локализация и культуры

Когда выбраны культуры, Lino генерирует ресурсы, чтобы messages, validations, labels, errors и UI-текст можно было локализовать с самого начала. Это позволяет не относиться к интернационализации как к поздней заплатке, когда strings уже разбросаны по endpoints, handlers и components.

Проверьте фундамент и переходите к сервисам

После генерации восстановите зависимости, соберите решение и запустите Aspire host:

dotnet restore <ProjectName>.slnx
dotnet build <ProjectName>.slnx
dotnet run --project src/Aspire/AppHost/<ProjectName>.AppHost.csproj

На этом этапе решение все еще является фундаментом. Откройте проект в редакторе, проверьте ресурсы, созданные Aspire, подтвердите решения по cache, messaging, локализации и качеству кода, а затем переходите к сервисам, которые будут представлять бизнес-возможности приложения.

Создание и управление сервисами

Сервис - это граница runtime и ownership. В Lino сервисы могут представлять независимые APIs в распределенной системе или более крупные бизнес-области внутри решения, которое может развиваться постепенно.

После создания фундамента проекта добавляйте сервисы с помощью:

lino service new

Мастер сервиса запрашивает:

  • Namespace сервиса: техническое имя, используемое в папках, проектах и namespaces.
  • Отображаемое имя: понятное имя сервиса.
  • Тип сервиса: выберите между простым и модульным.
  • Database provider: выберите SQL Server или PostgreSQL для базы данных сервиса.
  • Архитектурный стиль: сейчас Clean Architecture для простых сервисов.
  • Strongly Typed IDs: в простых сервисах определяет, будут ли идентификаторы генерироваться как выделенные типы.

Типы сервисов

Простой сервис: более прямая структура для бизнес-возможности с ясной границей. Это хороший выбор для сфокусированного API, микросервиса или достаточно небольшой области, которая может развиваться как единое целое.

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

Независимо от типа, каждый сервис владеет собственной базой данных. В простых сервисах решение по Strongly Typed IDs принимается на уровне сервиса; в модульных сервисах - отдельно для каждого модуля.

Простой сервис

Простой сервис размещает свои слои непосредственно в src/Services/<ServiceName>. Используйте эту структуру, когда бизнес-возможность имеет ясную границу и не нуждается в нескольких изолированных модулях внутри одного runtime. Она хорошо работает для сфокусированного API, микросервиса или достаточно небольшой доменной области, которая может развиваться как единое целое.

src/Services/<ServiceName>/
├── Domain/
│   └── <ProjectName>.<ServiceName>.Domain.csproj
├── Application/
│   └── <ProjectName>.<ServiceName>.Application.csproj
├── Infrastructure.Persistence/
│   └── <ProjectName>.<ServiceName>.Infrastructure.Persistence.csproj
├── Infrastructure/
│   └── <ProjectName>.<ServiceName>.Infrastructure.csproj
├── Api/
│   └── <ProjectName>.<ServiceName>.Api.csproj
├── Integration.Events/              (если есть messaging)
│   └── <ProjectName>.<ServiceName>.Integration.Events.csproj
├── Api.Contracts/                   (если есть типизированное HTTP-потребление)
│   └── <ProjectName>.<ServiceName>.Api.Contracts.csproj
└── Api.Client/                      (если есть типизированное HTTP-потребление)
    └── <ProjectName>.<ServiceName>.Api.Client.csproj
tests/Services/<ServiceName>/
├── UnitTests/
│   ├── Domain/
│   │   └── <ProjectName>.<ServiceName>.Domain.UnitTests.csproj
│   └── Application/
│       └── <ProjectName>.<ServiceName>.Application.UnitTests.csproj
└── IntegrationTests/
    └── <ProjectName>.<ServiceName>.IntegrationTests.csproj

У слоев разные ответственности: Domain защищает бизнес-правила и инварианты; Application оркестрирует use cases; Infrastructure.Persistence содержит Entity Framework Core, репозитории, Unit of Work и migrations; Infrastructure содержит технические интеграции; а Api адаптирует HTTP к use cases.

Проекты Api.Contracts и Api.Client появляются, когда есть типизированное HTTP-потребление, особенно в решениях с Blazor Web App. Первый концентрирует requests, responses, DTOs, публичные типы и общие client-интерфейсы; второй предоставляет HTTP-реализацию этих интерфейсов с использованием HttpClient, чтобы Blazor потреблял сгенерированные APIs без дублирования контрактов.

Integration.Events появляется, когда в решении включен messaging. Он содержит интеграционные события, публикуемые и потребляемые другими модулями, сервисами или системами, обычно вместе с инфраструктурой messaging и outbox.

Модульный сервис

Модульный сервис подходит для модульных монолитов или для runtime, который размещает несколько bounded contexts. Сам сервис содержит host, общую инфраструктуру и database provider; бизнес-правила живут внутри модулей.

src/Services/<ServiceName>/
├── Host/
│   └── <ProjectName>.<ServiceName>.Host.csproj
├── Infrastructure/
│   └── <ProjectName>.<ServiceName>.Infrastructure.csproj
└── Modules/
tests/Services/<ServiceName>/
└── Modules/

Host компонует модули, settings, endpoints и общую инфраструктуру этого сервиса. Он не должен содержать бизнес-правила. Проект Infrastructure на уровне сервиса предоставляет техническую поддержку композиции, общую для модулей.

Когда добавляется модуль, проекты Domain, Application, Infrastructure.Persistence, Infrastructure и Api появляются внутри Modules/<ModuleName>, сохраняя внутреннюю границу. Api.Contracts, Api.Client и Integration.Events следуют тем же условиям: типизированное HTTP-потребление для Blazor/API clients и включенный messaging для интеграционных событий.

Database ownership

Каждый сервис владеет собственной базой данных. В решении с несколькими сервисами один сервис может использовать PostgreSQL, а другой - SQL Server. Это решение может зависеть от потребности домена, performance, operation, зрелости команды или интеграции с существующей инфраструктурой.

В модульном сервисе база данных принадлежит сервису как runtime, но модули изолированы через schema, persistence projects и собственные migrations. Модуль не должен напрямую запрашивать таблицы другого модуля, даже когда таблицы находятся в одной физической базе данных.

Выбор между простым и модульным

Выбирайте минимальную структуру, которая защищает границу. Простого сервиса достаточно, когда есть одна доменная граница. Модульный сервис имеет смысл, когда нескольким capabilities нужно разделять runtime, deploy или transaction, но при этом они должны сохранять отдельные модели, persistence, APIs и тесты.

ВыборКогда использоватьПринятая стоимость
Простой сервисСфокусированный домен, мало внутренних границ, изолированный API или прямой микросервис.Меньше начальной структуры, но меньше внутренней изоляции, если домен сильно вырастет.
Модульный сервисНесколько поддоменов в одном runtime, модульный монолит, SaaS с независимыми областями или потребность в schemas по модулям.Больше проектов, больше дисциплины зависимостей и больше внимания к внутренним контрактам.

Архитектурный стиль

Сервисы, сгенерированные Lino, следуют Clean Architecture, чтобы отделять бизнес-правила от технических деталей. Доменному слою не нужно знать про HTTP, Entity Framework Core, messaging, UI или внешних providers; эти concerns остаются на краях приложения.

  • Decoupling: core rules не зависят от frameworks или delivery mechanisms.
  • Сопровождаемость: изменения инфраструктуры обычно остаются изолированными от бизнес-правил.
  • Тестируемость: use cases и доменный код можно тестировать с меньшим числом внешних зависимостей.
  • Эволюция: технические детали можно заменять с меньшим влиянием на ядро сервиса.

Clean Architecture и Strongly Typed IDs

Сгенерированные сервисы следуют Clean Architecture, чтобы бизнес-код не зависел от HTTP, EF Core, messaging, UI или деталей инфраструктуры. Такое разделение делает тесты более прямыми, снижает coupling и позволяет заменять технические детали без переписывания core rules.

Strongly Typed IDs повышают безопасность, предотвращая случайное смешивание идентификаторов. Вместо приема любого Guid, long или int домен может работать с отдельным типом для каждой сущности или aggregate, обычно в формате <EntityName>Id. Это предотвращает передачу идентификатора одной сущности туда, где ожидался другой, и делает сигнатуры выразительнее в commands, queries, entities, handlers и mappings.

  • Type safety: предотвращает случайное смешивание идентификаторов разных сущностей.
  • Ясность: делает сигнатуры и контракты выразительнее, чем свободные primitive types.
  • Рефакторинг: концентрирует изменения формата или сериализации в соответствующем ID type.
  • Снижение ошибок: делает некорректное использование видимым на этапе компиляции всегда, когда это возможно.

Интеграция между сервисами

Когда одному сервису нужно реагировать на другой, предпочитайте integration events, явные HTTP-интеграции или осознанно replicated data. Прямой доступ к базе данных другого сервиса создает структурный coupling, усложняет migrations и делает независимые deploys более рискованными.

Следующие шаги для модульных сервисов

После создания модульного сервиса следующий шаг - добавить модули с помощью lino module new. Каждый модуль должен представлять собственную бизнес-возможность, с domain, application, persistence, API, tests и integrations, сохраняющими внутреннюю границу сервиса.

Создание и управление модулями

Модули существуют только внутри модульных сервисов. Модуль представляет бизнес-границу в рамках одного runtime: у него есть собственная доменная модель, use cases, persistence, поверхность API, интеграционные события, тесты и схема базы данных.

Используйте модули, когда сервису нужно размещать больше одной бизнес-возможности, не смешивая сущности, правила, migrations и контракты. В модульном монолите все может выполняться в одном процессе, но внутренние границы остаются важными: реальная изоляция - это контроль зависимостей, а не просто разделение папок.

lino module new --service <ServiceName>

Во время создания мастер запрашивает:

  • Сервис: модульный сервис, который будет размещать модуль. Lino не позволяет создавать модули внутри простых сервисов, потому что у них нет структуры, необходимой для внутренней изоляции.
  • Namespace модуля: техническое имя, используемое в папках, namespaces, assemblies и проектах.
  • Отображаемое имя: понятное имя модуля, используемое в местах, рассчитанных на чтение человеком.
  • Strongly Typed IDs: определяет, будут ли идентификаторы, сгенерированные внутри модуля, использовать выделенные типы, обычно в формате <EntityName>Id.

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

src/Services/<ServiceName>/
├── Host/
├── Infrastructure/
└── Modules/
    └── <ModuleName>/
        ├── Domain/
        │   └── <ProjectName>.<ServiceName>.<ModuleName>.Domain.csproj
        ├── Application/
        │   └── <ProjectName>.<ServiceName>.<ModuleName>.Application.csproj
        ├── Infrastructure.Persistence/
        │   └── <ProjectName>.<ServiceName>.<ModuleName>.Infrastructure.Persistence.csproj
        ├── Infrastructure/
        │   └── <ProjectName>.<ServiceName>.<ModuleName>.Infrastructure.csproj
        ├── Api/
        │   └── <ProjectName>.<ServiceName>.<ModuleName>.Api.csproj
        ├── Integration.Events/              (если есть messaging)
        │   └── <ProjectName>.<ServiceName>.<ModuleName>.Integration.Events.csproj
        ├── Api.Contracts/                   (если есть типизированное HTTP-потребление)
        │   └── <ProjectName>.<ServiceName>.<ModuleName>.Api.Contracts.csproj
        └── Api.Client/                      (если есть типизированное HTTP-потребление)
            └── <ProjectName>.<ServiceName>.<ModuleName>.Api.Client.csproj
tests/Services/<ServiceName>/Modules/<ModuleName>/
├── UnitTests/
│   ├── Domain/
│   │   └── <ProjectName>.<ServiceName>.<ModuleName>.Domain.UnitTests.csproj
│   └── Application/
│       └── <ProjectName>.<ServiceName>.<ModuleName>.Application.UnitTests.csproj
└── IntegrationTests/
    └── <ProjectName>.<ServiceName>.<ModuleName>.IntegrationTests.csproj

Ответственность сгенерированных артефактов

АртефактОтветственность внутри модуля
DomainСущности, агрегаты, Value Objects, enumerations, доменные события, контракты репозиториев и инварианты модуля.
ApplicationUse Cases, commands, queries, handlers, validations, внутренние контракты ввода и вывода, а также оркестрация правил модуля.
Infrastructure.PersistenceDbContext, конфигурации Entity Framework Core, конкретные репозитории, Unit of Work и migrations модуля.
InfrastructureТехнические реализации, специфичные для модуля, адаптеры, providers и композиция зависимостей.
ApiHTTP endpoints, versioning, filters, authorization и адаптация между внешними запросами и use cases.
Api.ContractsГенерируется, когда есть типизированное HTTP-потребление, обычно со стороны Blazor Web Apps. Содержит requests, responses, DTOs, публичные типы и client-интерфейсы, общие для API и потребителя.
Api.ClientГенерируется вместе с контрактами, когда есть типизированное HTTP-потребление. Содержит HTTP-реализацию интерфейсов с использованием HttpClient, providers, options и helpers, чтобы проекты Blazor потребляли сгенерированные APIs согласованно и строго типизированно.
Integration.EventsГенерируется, когда в проекте есть messaging. Содержит интеграционные события, публикуемые и потребляемые другими модулями, сервисами или системами, сохраняя payload как явный контракт.

Структура базы данных

База данных остается привязанной к сервису, а не к каждому модулю отдельно. Внутри модульного сервиса каждый модуль представлен собственной schema в связанной базе данных, с собственным проектом persistence и собственными migrations. Это дает изоляцию и организацию без требования создавать отдельную физическую базу данных для каждого модуля.

Изоляция и независимость модулей

Модуль не должен напрямую обращаться к DbContext, сущностям, репозиториям или внутренним сервисам другого модуля. У каждого модуля есть собственная модель и собственная persistence. Когда другому модулю нужны данные, используйте явную интеграцию вместо пересечения границы через внутренние детали реализации.

Это предотвращает ситуацию, когда на первый взгляд локальное изменение ломает другую область системы. Если модулю-потребителю нужно запросить данные, принадлежащие другому модулю, он не должен зависеть от полной сущности исходного модуля. Он может хранить shadow entity с минимальными данными, необходимыми для собственного use case, пополняемыми через интеграцию или событие.

Преимущества такого decoupling:

  • Изоляция: каждый модуль может развивать правила, persistence и тесты, не пересекая внутренние детали другого модуля.
  • Организация: приложение уважает bounded contexts и делает ownership явным.
  • Гибкость: модули можно добавлять, удалять или рефакторить с меньшим влиянием на остальную часть сервиса.
  • Удобство тестирования: каждый модуль можно проверять более независимо, повышая уверенность в изменениях.

Коммуникация между модулями

Используйте явные контракты для коммуникации. Для вызовов внутри одного runtime внутренняя интеграция может раскрывать контракты в Integration.Contracts и in-process реализацию, когда она сгенерирована. Для вызовов между runtimes используйте HTTP-интеграцию. Для асинхронного распространения публикуйте интеграционные события в Integration.Events и используйте инфраструктуру messaging/outbox, когда публикация является частью транзакционной операции.

Schemas, migrations и база данных

В модульном сервисе модули совместно используют database provider сервиса, но каждый модуль представлен собственной schema и собственным проектом Infrastructure.Persistence. Migrations генерируются по модулю, сохраняя развитие базы данных согласованным с бизнес-границей.

Такое разделение позволяет каждому модулю развивать свои таблицы, seeds, indexes и foreign keys, не превращая базу данных сервиса в единую модель, общую для всех. Schema - это техническая граница, которая усиливает бизнес-границу, даже когда модули выполняются в одном процессе и используют одну физическую базу данных.

Хорошие границы модулей

Создавайте модули вокруг бизнес-возможностей, а не вокруг технических слоев. Хорошее имя модуля должно описывать ответственность, узнаваемую в домене, например область, процесс или capability с собственными правилами. Избегайте общих имен вроде Common, Core или Utilities для бизнес-правил, потому что они скрывают ownership и склонны превращаться в чрезмерно общие зависимости.

  • Связность: у модуля должны быть язык, правила и данные, которые изменяются вместе.
  • Автономность: должно быть возможно тестировать и развивать модуль без доступа к внутренним сущностям другого модуля.
  • Ясные контракты: данные, нужные вне модуля, должны раскрываться через API, интеграцию, событие или shadow entity, а не через прямой доступ к базе данных.
  • Низкая связанность: если двум модулям постоянно нужно менять одни и те же сущности, границу, вероятно, стоит пересмотреть.

После создания модулей следующие темы документации показывают, как моделировать сущности, Value Objects, enumerations, commands, queries, APIs, события, интеграции и migrations внутри этих границ.

Произошла необработанная ошибка. Обновить 🗙