Тестовое задание: ASP.NET Core Minimal API на .NET 9, PostgreSQL / EF Core и RabbitMQ.
Структура основана на dotnet-backend-template; зависимости от PANiXiDA.Core не используются.
Это серверное API без отдельного сайта с кнопками. Выполните шаги ниже по порядку: Docker запустит приложение, базу данных и RabbitMQ, а готовые команды помогут проверить работу с задачами. Инструкция рассчитана на Windows; устанавливать .NET и разбираться в коде не нужно.
- Установите Docker Desktop и запустите его. На Windows используйте Linux containers; если установщик попросит настроить WSL или перезагрузиться, выполните его инструкции. Docker Compose входит в Docker Desktop.
- Дождитесь, когда Docker Desktop сообщит, что Docker Engine запущен. Для первой сборки нужен интернет: Docker скачает необходимые компоненты.
- Войдите в GitHub под учётной записью, которой выдан доступ к приватному репозиторию.
- Откройте ветку с реализацией и нажмите Code → Download ZIP. Пока PR №1 не слит, используйте именно эту ветку: в
developmentприложения ещё нет. - Распакуйте скачанные исходники через Извлечь всё. Откройте внутри папку, в которой лежат
README.mdиcompose.yaml. - В Проводнике Windows нажмите на адресную строку, введите
powershellи нажмите Enter. Все команды ниже выполняйте в открывшемся окне.
Проверьте, что Docker доступен:
docker compose versionДолжна появиться строка с версией Docker Compose. Устанавливать .NET, PostgreSQL, RabbitMQ, Git или Postman отдельно не нужно.
В том же окне PowerShell выполните:
docker compose up --build -dДождитесь завершения команды: первая сборка может занять несколько минут. Она поднимает всё необходимое: API + PostgreSQL + RabbitMQ с веб-панелью управления. Подключения уже настроены, таблицы создаются автоматически миграциями.
Проверьте состояние контейнеров:
docker compose psУ API ожидается состояние Up, у PostgreSQL и RabbitMQ — Up (healthy).
Откройте в браузере http://localhost:8080/health. Ожидаемый ответ:
{"status":"ok"}Если страница не открылась сразу, подождите несколько секунд после запуска и обновите её. Список задач доступен по адресу http://localhost:8080/tasks; в новой БД он пустой: [].
Панель запускается автоматически той же командой docker compose up --build -d. Отдельно включать плагины не нужно.
| Параметр | Значение |
|---|---|
| Адрес панели | http://localhost:15672 |
| Порт панели | 15672 |
| Логин | tasks |
| Пароль | local-development-only |
Откройте адрес в браузере и введите эти логин и пароль. Если панель ещё не отвечает сразу после запуска, подождите несколько секунд и обновите страницу.
Учётные данные уже указаны в compose.yaml, заказчику ничего дописывать не нужно: RABBITMQ_DEFAULT_USER и RABBITMQ_DEFAULT_PASS создают пользователя брокера, а RabbitMq__UserName и RabbitMq__Password в сервисе api задают подключение приложения. Если меняете логин или пароль, меняйте их в обеих секциях одинаково. Переменные RABBITMQ_DEFAULT_* применяются при первоначальном создании данных брокера; пароль существующего пользователя меняется через панель управления.
Порт 5672 используется приложением для AMQP внутри Docker и не открывается на компьютере. Порт 15672 предназначен для браузера и доступен только с этого компьютера. Автотесты используют отдельные временные контейнеры без веб-панели и подключаются к брокеру напрямую.
Копируйте блоки по порядку в одно и то же окно PowerShell. ID задачи сохраняется автоматически: ничего подставлять вместо <id> не нужно.
Создайте задачу:
$baseUrl = "http://localhost:8080"
$task = Invoke-RestMethod -Method Post -Uri "$baseUrl/tasks" -ContentType "application/json" -Body '{"title":"Buy milk","priority":"High"}'
$task | ConvertTo-JsonВ ответе должны быть title: "Buy milk", priority: "High", isCompleted: false и completedAt: null.
Завершите её:
Invoke-RestMethod -Method Put -Uri "$baseUrl/tasks/$($task.id)/complete" | ConvertTo-JsonТеперь isCompleted равен true, а completedAt содержит дату и время завершения. Если выполнить эту команду повторно, ожидается ошибка 409 Conflict: повторное завершение запрещено.
Посмотрите список задач:
Invoke-RestMethod -Uri "$baseUrl/tasks" | ConvertTo-JsonСозданная задача должна присутствовать в списке и быть завершённой. Удалите её и снова откройте список:
Invoke-RestMethod -Method Delete -Uri "$baseUrl/tasks/$($task.id)"
Invoke-RestMethod -Uri "$baseUrl/tasks" | ConvertTo-JsonУспешное удаление не выводит текст. Задача исчезнет из списка; если других задач нет, список пустой (PowerShell может не вывести ничего).
Получение события из RabbitMQ проверяют автотесты в CI. В обычном запуске доступна веб-панель брокера, но очередь получателя автоматически не создаётся; тест создаёт свою очередь сам. Чтобы увидеть сообщение вручную в панели, до завершения задачи создайте очередь в virtual host / и привяжите её к exchange task.events с ключом task.completed. Если exchange ещё нет, сначала создайте его с типом topic и параметром Durable = true. Успешное ручное завершение задачи само по себе не подтверждает доставку события.
docker compose downСозданные вручную и не удалённые задачи сохраняются в PostgreSQL и будут доступны после следующего запуска. Для повторного запуска откройте PowerShell в той же папке и выполните docker compose up --build -d.
| Что видно | Что сделать |
|---|---|
docker не найден |
Установите Docker Desktop и заново откройте PowerShell. |
Cannot connect / error during connect к Docker |
Запустите Docker Desktop и дождитесь запуска Engine. |
no configuration file provided |
Откройте PowerShell в папке, где лежит compose.yaml. |
| Ошибка скачивания образов или пакетов | Проверьте интернет и доступ Docker к сети, затем повторите команду. |
port is already allocated / занят 8080 |
Выполните $env:TASK_API_PORT = "8081", затем повторите docker compose up --build -d. В адресах браузера и $baseUrl используйте порт 8081. |
| Занят порт панели RabbitMQ 15672 | Выполните $env:RABBITMQ_UI_PORT = "15673", затем повторите docker compose up --build -d. Панель будет доступна на http://localhost:15673. |
| API не отвечает после запуска | Выполните docker compose ps и docker compose logs --tail=100 api, передайте вывод разработчику. |
| При скачивании с GitHub ошибка 404 | Войдите в учётную запись с доступом к репозиторию. Если доступа нет, попросите разработчика выдать его. |
Команды Docker работают и на macOS/Linux с установленным Docker Compose; блоки Invoke-RestMethod выше предназначены для PowerShell.
Пароли в Compose — демонстрационные значения только для локальных контейнеров. Порты PostgreSQL и AMQP RabbitMQ не публикуются. API и веб-панель RabbitMQ доступны только через loopback.
| Метод | Путь | Результат |
|---|---|---|
| POST | /tasks |
201, созданная задача и Location: /tasks/{id}; неверный JSON, Title или Priority → 400 |
| GET | /tasks |
200, все задачи в порядке CreatedAt / Id |
| PUT | /tasks/{id}/complete |
200, завершённая задача; отсутствует → 404; уже завершена или конкурентно изменена → 409 |
| DELETE | /tasks/{id} |
204, физическое удаление; отсутствует → 404 |
Некорректный GUID возвращает 400. Пустой GUID корректен синтаксически, но такой задачи нет: 404.
Ошибки оформляются как Problem Details. Пагинации и аутентификации нет, согласно объёму задания.
Создание:
curl -i http://localhost:8080/tasks \
-H 'Content-Type: application/json' \
-d '{"title":"Buy milk","priority":"High"}'title обязателен, содержит хотя бы один непробельный символ, максимум 200 символов; исходный текст сохраняется.
priority по умолчанию Medium; принимаются Low, Medium, High или соответствующие числа 0, 1, 2.
Ответы и события содержат строковое значение приоритета. IsCompleted=false, CompletedAt=null при создании.
Location идентифицирует созданную задачу; отдельный GET /tasks/{id} не предусмотрен, для чтения используется список GET /tasks.
Временные метки записываются в UTC с точностью PostgreSQL до микросекунды, одинаковой для API, БД и события. Повторное завершение не изменяет CompletedAt и не публикует событие.
Проверки пробельного названия, допустимого приоритета и состояния завершения выполняются приложением. CHECK-ограничений этих правил в БД нет; ограничения NOT NULL и длины полей сохранены. Миграция RemoveTaskCheckConstraints удаляет прежние CHECK-ограничения при следующем запуске обновлённого приложения, сохраняя существующие задачи.
curl -i -X PUT http://localhost:8080/tasks/<id>/complete
curl http://localhost:8080/tasks
curl -i -X DELETE http://localhost:8080/tasks/<id>В Windows используйте curl.exe или Invoke-RestMethod.
src/
PANiXiDA.TaskApi.Host/ # startup, конфигурация, Dockerfile
Tasks/
PANiXiDA.TaskApi.Tasks.Domain/ # TaskItem, Priority, инварианты
PANiXiDA.TaskApi.Tasks.Application/ # сценарии, контракты хранилища и событий
PANiXiDA.TaskApi.Tasks.Infrastructure/ # EF Core, миграции, RabbitMQ
PANiXiDA.TaskApi.Tasks.Presentation/ # Minimal API, DTO, HTTP-валидация
tests/
PANiXiDA.TaskApi.Testing/ # общие Testcontainers для БД и брокера
Tasks/
PANiXiDA.TaskApi.Tasks.UnitTests/ # домен, сценарии, publisher с подменами
PANiXiDA.TaskApi.Tasks.IntegrationTests/ # репозиторий, миграции, RabbitMQ transport
PANiXiDA.TaskApi.Tasks.FunctionalTests/ # HTTP API через WebApplicationFactory
Host связывает слои через встроенный DI. Application зависит только от Domain; Presentation не обращается к EF или RabbitMQ. Версии NuGet централизованы в Directory.Packages.props.
Сценарии сгруппированы по фичам, как в Tactical Heroes API:
Application/Tasks/
Abstractions/ # ITasksRepository, ITaskEventPublisher
Common/ # единая точность временных меток
Create/ # CreateTaskCommand, CreateTaskHandler
GetList/ # GetTasksQuery, GetTasksHandler
Complete/ # CompleteTaskCommand, CompleteTaskHandler, результат и событие
Delete/ # DeleteTaskCommand, DeleteTaskHandler
Presentation/Features/Tasks/
TasksEndpoints.cs # только регистрация группы /tasks
Create/ # endpoint, request, response, mapper
GetList/ # endpoint, response, mapper
Complete/ # endpoint, response, mapper
Delete/ # endpoint, mapper
Каждый endpoint вызывает свой обработчик Application через встроенный DI. Mapper переводит входные данные в команду/запрос, а результат — в DTO ответа своей фичи. Unit-тесты обработчиков размещены в соответствующих папках Application/Tasks/Create и Application/Tasks/Complete.
TaskItem.RowVersion имеет тип uint и настроен через IsRowVersion(): Npgsql использует системную колонку PostgreSQL xmin. EF добавляет исходную версию в условие UPDATE; DbUpdateConcurrencyException преобразуется в 409. Для двух запросов, прочитавших одну версию строки, сохраняется только один результат. Подробнее: Npgsql concurrency tokens.
После успешного SaveChangesAsync неизменяемое событие добавляется в ограниченную очередь в памяти. HTTP-запрос не ждёт RabbitMQ. Один BackgroundService последовательно публикует события через асинхронный клиент, поэтому канал не используется параллельно.
- Exchange:
task.events, типtopic, durable. - Routing key:
task.completed. - JSON:
taskId,title,completedAt,priority. - Сообщение persistent,
Content-Type=application/json,MessageId=taskId. - Попытка публикации ограничена 2 секундами; ошибка логируется вместе с JSON события. Следующее событие может заново открыть соединение.
- При переполнении очереди или остановке publisher событие также записывается в лог.
- При SIGTERM/Ctrl+C очередь закрывается для новых событий. На завершение текущих и ожидающих публикаций отводится 4 секунды, затем до 1 секунды на отмену и закрытие канала/соединения.
Очередь получателя и binding создаёт получатель; приложение только объявляет exchange и публикует. Для проверки тест создаёт временную очередь с binding task.completed и читает реальное сообщение.
Это best-effort доставка, как в ТЗ: нет outbox, повторной доставки и publisher confirms. При падении процесса между сохранением и публикацией или недоступном брокере событие может потеряться. Если у exchange нет подходящей очереди, RabbitMQ удалит сообщение. Завершение задачи при этом сохраняется.
Результаты автотестов можно посмотреть без запуска команд: откройте PR №1 → Checks или GitHub Actions и выберите запуск для последнего коммита ветки с реализацией. Нужен доступ к приватному репозиторию. Зелёные проверки означают, что тесты и сборка прошли; красные — что есть ошибка, жёлтые — что проверка ещё выполняется. В отчёте Tests названия проверок указаны на русском.
Тесты запускаются обычной командой из корня репозитория:
dotnet testДля самостоятельного запуска разработчику нужны SDK из global.json (.NET 10), ASP.NET Core Runtime 9 и работающий Docker. Приложение и все тестовые проекты имеют target framework net9.0; SDK 10 нужен для команды dotnet test с Microsoft.Testing.Platform, которую использует общий CI-шаблон. Контейнер приложения по-прежнему работает на runtime .NET 9.
Предварительно запускать docker compose up не нужно. Testcontainers создаёт отдельные PostgreSQL и RabbitMQ со случайными портами и удаляет их после проверки. Строки подключения и тестовые учётные данные передаются автоматически; обычная база сервиса не используется.
| Проект | Что проверяет |
|---|---|
UnitTests |
Инварианты TaskItem, время и порядок сохранения/публикации в обработчиках фич, конфликты, ошибки и отмена, очередь publisher и остановка. Внешние сервисы не нужны. |
IntegrationTests |
Репозиторий и миграции на настоящем PostgreSQL: сортировка, ограничения, xmin, удаление. Контракт события и повторное соединение транспорта с настоящим RabbitMQ. |
FunctionalTests |
HTTP API через WebApplicationFactory: статусы, валидация, значения по умолчанию, CRUD, реальное событие, недоступный брокер и конкурентное завершение. |
Запуск только unit-тестов без Docker:
dotnet test --project tests/Tasks/PANiXiDA.TaskApi.Tasks.UnitTests/PANiXiDA.TaskApi.Tasks.UnitTests.csprojВсе [Fact] и [Theory] имеют русские DisplayName. Для интеграционных тестов общий fixture создаёт окружение, а перед каждой проверкой очищаются тестовые задачи. Гонка HTTP-запросов воспроизводится барьером перед UPDATE, чтобы оба запроса гарантированно прочитали одну версию строки.
CI использует общий dotnet-tests.yml: обнаруживает три тестовых проекта, запускает dotnet test на GitHub runner, публикует TRX и отчёт покрытия. Переменная репозитория PROJECT_FOLDER равна .. Приватные NuGet-пакеты и токены к ним не нужны. После успешных тестов отдельная job собирает рабочий Docker-образ API.
| Переменная | Назначение |
|---|---|
ConnectionStrings__Tasks |
Строка подключения PostgreSQL, обязательна |
RabbitMq__HostName |
Адрес RabbitMQ |
RabbitMq__Port |
AMQP-порт, по умолчанию 5672 |
RabbitMq__UserName, RabbitMq__Password |
Учётные данные, обязательны |
RabbitMq__VirtualHost |
Virtual host, по умолчанию / |
RabbitMq__QueueCapacity |
Размер очереди публикаций, по умолчанию 1024 |
В production секреты передаются через окружение/секрет-хранилище, а миграции обычно выносятся в отдельный шаг развёртывания. /health показывает работоспособность HTTP-процесса, а не доступность внешних зависимостей.