- Добавление новой модели
- Функциональный подход: Option, Result, монады
- Деплой через Docker Compose (API + Nginx)
Проект построен на чистой архитектуре с использованием .NET 10 и .NET Aspire для оркестрации сервисов.
MarketMate/
├── MarketMate.AppHost # Aspire — оркестрация всех сервисов
├── MarketMate.ServiceDefaults # Общие настройки: телеметрия, хелсчеки, resilience
│
├── MarketMate.Domain # Доменные модели (без зависимостей)
├── MarketMate.Application # Интерфейсы сервисов и репозиториев, бизнес-логика
│
├── MarketMate.Infrastructure.Postgre # EF Core + PostgreSQL: DbContext, репозитории, миграции
├── MarketMate.Infrastructure.Kafka # Реализация IEventBus через Kafka
├── MarketMate.Infrastructure.DependencyInjection # Точка сборки всех зависимостей
│
├── MarketMate.Llm # Интеграция с LLM (Yandex через SemanticKernel)
│
├── MarketMate.Api # HTTP API (ASP.NET Core)
├── MarketMate.Worker # Фоновый сервис
└── MarktetMate.Talker # Отдельный сервис
Api / Worker
↓
Infrastructure.DependencyInjection ← Infrastructure.Postgre
↓ ← Infrastructure.Kafka
Application ← Llm
↓
Domain
- Domain — чистые модели, никаких зависимостей
- Application — только интерфейсы (
IUnitOfWorkFactory,IEventBus,ILlmService, сервисы). Зависит только от Domain - Infrastructure — реализации интерфейсов из Application. Знает про EF, Kafka, LLM
- DependencyInjection — единственное место где всё собирается вместе, регистрирует все зависимости
- Api/Worker — вызывают
builder.AddInfrastructure()и получают всё готовое
1. Направление зависимостей — всегда внутрь
Внешние слои знают о внутренних, но не наоборот. Domain не знает про EF, API, Kafka — ничего. Application знает только про Domain. Инфраструктура знает про Application и реализует его интерфейсы.
Если хочется сделать using MarketMate.Infrastructure.* в Domain или Application — это красный флаг, что-то идёт не так.
2. Application оперирует интерфейсами, не реализациями
Application никогда не создаёт new ChatMessageRepository() или new KafkaEventBus(). Всё приходит через DI. Это позволяет легко подменить реализацию — например, заменить Kafka на RabbitMQ без изменений в бизнес-логике.
3. Контроллер — тонкая обёртка над Application
Контроллер не содержит бизнес-логики. Его единственная задача:
- принять HTTP запрос
- передать данные в сервис из Application
- вернуть результат
// Правильно — контроллер делегирует сервису
[HttpPost]
public async Task<IActionResult> Create([FromBody] CreateChatMessageRequest request, CancellationToken ct)
{
var message = await chatMessageService.CreateAsync(request.Content, request.Role, ct);
return Ok(message);
}
// Неправильно — логика в контроллере
[HttpPost]
public async Task<IActionResult> Create([FromBody] CreateChatMessageRequest request)
{
var message = new ChatMessage { Id = Guid.NewGuid(), ... }; // не сюда
await dbContext.SaveChangesAsync(); // не сюда
return Ok(message);
}4. Инфраструктура реализует, Application объявляет
Все контракты объявляются в Application (IUnitOfWorkFactory, IEventBus, ILlmService, репозитории). Инфраструктурные проекты их реализуют. Такой подход называется Dependency Inversion — зависимость от абстракции, а не от конкретики.
5. DependencyInjection — единственное место где всё знает про всё
Только Infrastructure.DependencyInjection знает одновременно про Application и все инфраструктурные проекты. Он связывает интерфейсы с реализациями. Больше нигде в проекте не должно быть new ConcreteImplementation() для сервисов.
UnitOfWork + репозитории
Все операции с БД идут через IUnitOfWorkFactory. Фабрика сама управляет транзакцией — открывает, коммитит или роллбачит:
await uowFactory.ExecuteAsync(async uow =>
{
await uow.ChatMessages.AddAsync(message);
});
var message = await uowFactory.ExecuteAsync(async uow =>
await uow.ChatMessages.GetByIdAsync(id));Каждый вызов ExecuteAsync — отдельный DbContext и транзакция.
Event Bus
Публикация событий через IEventBus. Реализация — Kafka (заглушка, готова к реализации):
await eventBus.PublishAsync(new SomeEvent(...));LLM интеграция
В Application доступен ILlmService. В инфраструктуре он адаптирован из IChatService (Yandex через SemanticKernel).
Проект работает в broker-only режиме. Для команды добавлен шаблон .env.example в корне репозитория.
Минимум для запуска LLM:
YANDEX_IAM_BROKER_BASE_URL=https://functions.yandexcloud.net/<your-broker-function-id>
YANDEX_IAM_BROKER_API_KEY=<your-broker-api-key>Для Windows можно задать значения через скрипт:
powershell -ExecutionPolicy Bypass -File .\ops\set-yandex-broker-env.ps1 -BaseUrl "https://functions.yandexcloud.net/<your-broker-function-id>" -ApiKey "<your-broker-api-key>"Миграции
Миграции лежат в MarketMate.Infrastructure.Postgre/Migrations. Применяются автоматически при старте API и Worker.
Создать новую миграцию:
cd MarketMate.Infrastructure.Postgre
dotnet ef migrations add <MigrationName>Для запуска нужен Docker (Aspire поднимает PostgreSQL в контейнере).
Aspire Dashboard откроется на http://localhost:15888 — там видны все сервисы, логи и трейсы.
Worker и Talker по умолчанию запускаются вручную через Dashboard (WithExplicitStart).
PostgreSQL контейнер персистентный — не пересоздаётся при каждом запуске (ContainerLifetime.Persistent).
Актуальный деплой лежит в MarketMate/deploy/docker-compose.yml.
Быстрый запуск:
Set-Location "C:\Users\Vladislav\Desktop\marketmate\MarketMate\deploy"
powershell -ExecutionPolicy Bypass -File .\start.ps1Ручной запуск:
Set-Location "C:\Users\Vladislav\Desktop\marketmate\MarketMate\deploy"
Copy-Item .env.example .env
# заполнить .env
docker compose up -d --buildОстановка:
Set-Location "C:\Users\Vladislav\Desktop\marketmate\MarketMate\deploy"
docker compose down