- О проекте
- Архитектура решения
- Сервисы
- Технологии
- Аутентификация и роли
- CQRS и MediatR
- Валидация и Pipeline Behaviors
- Kafka и асинхронные процессы
- Запуск проекта
- Наблюдаемость
- Swagger и API-документация
- Health Checks
- Логирование
- Миграции
- Тестирование
- API примеры
EventForge — это solution с несколькими .NET 10 микросервисами для управления пользователями, событиями и бронированиями.
Система построена вокруг асинхронного обмена сообщениями через Kafka и использует паттерн Outbox для надёжной публикации интеграционных событий.
EventForge.Users— регистрация, логин и JWT-аутентификацияEventForge.Events— управление событиями, доступными местами и обработка Kafka-событий бронированияEventForge.Booking— создание, обработка и отмена бронирований, публикация сообщений через outbox
Решение использует слои Domain, Application, Infrastructure, Presentation для каждого сервиса.
Схема зависимостей внутри каждого сервиса:
Presentation -> Application -> Domain
^
Infrastructure
Общие проекты:
EventForge.Shared/EventForge.Contract— контракты Kafka-сообщенийEventForge.Shared/EventForge.Entities— общие перечисления и shared entitiesEventForge.Shared/EventForge.ExceptionMiddleware— middleware для обработки ошибокEventForge.Shared/EventForge.LoggingDBInterceptor— DB interceptor и инфраструктурные расширенияEventForge.Shared/EventForge.Settings— общие настройки, включая JWTEventForge.Shared/EventForge.Behaviors— pipeline-поведения MediatR (валидация, логирование, метрики)EventForge.Shared/EventForge.CacheKeys— константы ключей кэша RedisEventForge.Shared/EventForge.Swagger— Swagger UI с API-версионированием и кастомизацией
AuthServicePasswordHasherJwtTokenGeneratorUserRepository
EventService— бизнес-логика событийEventRepository— доступ к БД событийProcessedMessageRepository— дедупликация Kafka-сообщенийBookingRequestedConsumer— первичная обработка запросов бронированияBookingRequestedRetryConsumer— повторная обработка из retry-топикаBookingRequestedMessageProcessor— ядро бизнес-обработки (переиспользуется primary/retry)BookingRequestedDbRetryPolicy— in-place retry с exponential backoffBookingCancelledConsumer— освобождение мест при отмене брониOutboxPublisherBackgroundService— публикация outbox-сообщений в KafkaKafkaEventPublisher— отправка сообщений в KafkaKafkaMetrics— RED-метрики producer/consumerRedisCacheService— кэширование событий
BookingService— бизнес-логика бронированияBookingRepository— доступ к БД бронированийOutboxRepository— управление outbox-сообщениямиProcessedMessageRepository— дедупликация входящих Kafka-событийOutboxPublisherBackgroundService— публикация outbox в KafkaKafkaBookingPublisher— отправка сообщений в KafkaKafkaMetrics— RED-метрики producer/consumerBookingConfirmedConsumer— подтверждение брониBookingRejectedConsumer— отклонение (событие не найдено)BookingNotApprovedConsumer— отклонение (нет мест / началось)
- .NET 10
- ASP.NET Core Web API
- Entity Framework Core 10
- PostgreSQL
- Confluent.Kafka
- MediatR 12.4 (CQRS pipeline)
- FluentValidation 12.1 (валидаторы команд/запросов)
- Serilog (структурное логирование)
- Swashbuckle / Swagger (API-документация)
- Asp.Versioning (API-версионирование)
- AspNetCore.HealthChecks (проверки PostgreSQL, Redis, Kafka)
- StackExchange.Redis (кэширование)
- OpenTelemetry (трассировка и метрики)
- xUnit 3
- FluentAssertions
- Moq
- Testcontainers for .NET
- Polly (resilience pipelines для in-place retry)
- NetArchTest.Rules (архитектурные тесты)
Управление версиями пакетов — централизованное через Directory.Packages.props.
Система использует JWT Bearer authentication с ролями User и Admin.
Userможет просматривать события, создавать и отменять свои бронированияAdminдополнительно может создавать, изменять и удалять события, а также отменять любые бронирования
JWT создаётся сервисом EventForge.Users.
JWT содержит как минимум:
sub— GUID пользователяrole—UserилиAdmin
В сервисах Booking, Events, Users применён CQRS-подход на уровне Application-слоя:
Commands— операции изменения состояния (create/update/cancel/register)Queries— операции чтенияHandlers— отдельные обработчики для каждого сценария- контроллеры делегируют выполнение через
ISender
Для dispatch-запросов используется библиотека MediatR 12.4:
IRequest<TResponse>— маркер команды/запроса (отMediatR)IRequestHandler<TRequest, TResponse>— обработчик (отMediatR)ISender— точка входа (MediatR.ISender)Mediator— резолвинг handler + pipeline (отMediatR)
Регистрация в DI — явная, без сборок (performance-oriented):
services.AddScoped<ISender, Mediator>();
services.AddScoped<IRequestHandler<CreateBookingCommand, BookingInfoDTO>, CreateBookingHandler>();
services.AddScoped(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
services.AddScoped(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>));
services.AddScoped(typeof(IPipelineBehavior<,>), typeof(MetricsBehavior<,>));Команды/запросы реализованы как record'ы:
public sealed record CreateBookingCommand(Guid EventId, Guid UserId) : IRequest<BookingInfoDTO>;Это позволяет:
- изолировать use-case'ы по файлам;
- проще тестировать бизнес-сценарии (unit-тесты на handlers);
- централизованно добавлять cross-cutting логику (валидация, аудит, логирование, pipeline-поведение).
Каждый Command/Query проходит через цепочку pipeline-поведений:
ValidationBehavior— вызывает все реализацииIValidator<TRequest>из FluentValidationLoggingBehavior— логирует start/success/failureMetricsBehavior— пишет метрикиcqrs_requests_totalиcqrs_request_duration_ms
Pipeline-поведения находятся в EventForge.Shared/EventForge.Behaviors/Behaviors/ и реализуют MediatR.IPipelineBehavior<TRequest, TResponse>.
Валидаторы наследуются от AbstractValidator<T> (FluentValidation):
RegisterUserCommandValidator— проверяет login (3–64 симв.), password (≥6 симв.), roleLoginUserQueryValidator— проверяет, что login/password не пустыеCreateEventCommandValidator— проверяет название, даты (StartAt > now, StartAt < EndAt)ChangeEventCommandValidator— проверяет EventId, nullable-поля (StartAt, EndAt, Title)CreateBookingCommandValidator— проверяет EventId ≠ Guid.Empty, UserId ≠ Guid.EmptyCancelBookingCommandValidator— проверяет BookingId ≠ Guid.Empty, UserId ≠ Guid.Empty
Kafka используется для межсервисного взаимодействия между Booking и Events.
- Клиент создаёт бронирование через
EventForge.Booking Bookingсохраняет бронь в статусеPendingBookingпубликуетBookingRequestedв Kafka через outboxEventsчитаетBookingRequestedEvents:- проверяет, что событие существует
- проверяет, что событие ещё не началось
- проверяет, что есть доступные места
- резервирует место через доменную модель
Event
- Если всё успешно — публикуется
BookingConfirmed - Если событие не найдено — публикуется
BookingRejected - Если событие уже началось или мест нет — публикуется
BookingNotApproved BookingчитаетBookingNotApprovedи переводит бронь вRejected
Для BookingRequestedConsumer используется схема отказоустойчивости:
- Чтение из
booking-requestedсEnableAutoCommit = false. - Невалидный JSON/пустой payload ->
booking-requested-dlq. - При
DbUpdateException:- in-place retry выполняется через
Polly(BookingRequestedDbRetryPolicy) с exponential backoff и jitter, - после исчерпания локальных попыток сообщение публикуется в
booking-requested-retry.
- in-place retry выполняется через
BookingRequestedRetryConsumerчитаетbooking-requested-retry:- ждет
NextAttemptAtUtc, - повторно обрабатывает через
BookingRequestedMessageProcessor.
- ждет
- Если
RetryAttempt >= RetryTopicMaxAttempts-> сообщение переводится вbooking-requested-dlq. - Commit offset выполняется только после принятого решения (success/retry/dlq), чтобы исключить потерю сообщений.
Pollyприменяется только для локальных (быстрых) повторов в рамках одного consumer-цикла. Отложенные повторы между сообщениями считаются черезNextAttemptAtUtcи обрабатываютсяBookingRequestedRetryConsumer.
Контракты Kafka лежат в EventForge.Shared/EventForge.Contract/Brokers.
Используемые типы сообщений:
BookingRequestedBookingConfirmedBookingRejectedBookingNotApprovedBookingCancelledBookingRequestedRetryEnvelopeBookingRequestedDlqMessage
Eventsхранит обработанные сообщения вProcessedMessagesBookingтакже хранит обработанные сообщения вProcessedMessagesдля защиты от повторной доставки Kafka-сообщений- повторная доставка Kafka-сообщения не приводит к повторной обработке
- публикация в Kafka выполняется через outbox
BookingRequestedMessageProcessorцентрализует бизнес-обработку и повторно используется из primary/retry consumer.- retry/dlq сообщения также отправляются через outbox, чтобы не терять события при временной недоступности Kafka.
- .NET 10 SDK
- Docker Desktop
Проект запускается через docker-compose.yml.
Для каждого микросервиса используется отдельный Dockerfile:
EventForge.Users/dockerfileEventForge.Events/dockerfileEventForge.Booking/dockerfile
docker-compose поднимает:
zookeeperkafkaakhqkafka-init-topicspostgresredisusers_apievents_apibooking_apipgadmin
Перед запуском нужно создать .env файл в корне проекта рядом с docker-compose.yml.
# Базовые секреты
DB_USER=postgres
DB_PASSWORD=postgres
DB_PORT=5432
# Настройки для Users
USERS_DB=eventforge_users_dev
USERS_HOST=postgres_users
# Настройки для Events
EVENTS_DB=eventforge_events_dev
EVENTS_HOST=postgres_events
# Настройки для Booking
BOOKING_DB=eventforge_booking_dev
BOOKING_HOST=postgres_booking
# Строка подключения в формате .NET Npgsql
USERS_CONNECTION_STRING=Host=${USERS_HOST};Port=${DB_PORT};Database=${USERS_DB};Username=${DB_USER};Password=${DB_PASSWORD}
EVENTS_CONNECTION_STRING=Host=${EVENTS_HOST};Port=${DB_PORT};Database=${EVENTS_DB};Username=${DB_USER};Password=${DB_PASSWORD}
BOOKING_CONNECTION_STRING=Host=${BOOKING_HOST};Port=${DB_PORT};Database=${BOOKING_DB};Username=${DB_USER};Password=${DB_PASSWORD}
EVENTS_REDIS_CONNECTION_STRING=redis:6379
# Настройки pgAdmin
PGADMIN_EMAIL=admin@admin.com
PGADMIN_PASSWORD=admin_password_987
DB_USER— пользователь PostgreSQL для всех контейнеров БДDB_PASSWORD— пароль PostgreSQLDB_PORT— порт к PostgeSQLUSERS_DB— имя БД сервисаUsersEVENTS_DB— имя БД сервисаEventsBOOKING_DB— имя БД сервисаBooking
Внутри docker-сети сами контейнеры PostgreSQL по-прежнему слушают стандартный порт 5432.
Прир разворачивании postgres в docker-compose базы данных будут созданы скриптом.
USERS_CONNECTION_STRING— строка подключения дляusers_apiEVENTS_CONNECTION_STRING— строка подключения дляevents_apiBOOKING_CONNECTION_STRING— строка подключения дляbooking_api
Важно:
- в compose используется имя контейнера БД как host
- поэтому внутри connection string нужно указывать:
postgres_userspostgres_eventspostgres_booking
- порт внутри сети Docker —
5432
Пример:
Host=postgres_users;Port=5432;Database=eventforge_users;Username=postgres;Password=postgres
PGADMIN_EMAIL— логин для входа в PgAdminPGADMIN_PASSWORD— пароль для входа в PgAdmin
EVENTS_REDIS_CONNECTION_STRING=redis:6379
По сути состоит из имени сервера и порта
Сборка и запуск всех сервисов в фоновом режиме:
docker-compose up -dЗапуск только Users API с зависимостями:
docker-compose up -d users_apiЗапуск только окружения для последующего запуска сервисов в debug режиме
docker-compose up -d zookeeper kafka akhq postgres pgadmin kafka-init-topics redis prometheus grafana jaegerСборка образов без запуска:
docker-compose buildОстановка и удаление контейнеров, тома при этом сохраняются:
docker-compose downПри запуске:
- каждый API-сервис собирается из своего
Dockerfile - сервисы запускаются в окружении
Docker - при старте автоматически применяются EF Core миграции
BookingиEventsподключаются к Kafka внутри docker-сети черезkafka:29092- Swagger доступен в окружении
DevelopmentиDocker Kafka-init-topicsавтоматически создаёт 7 топиков (1 партиция, replication-factor 1):booking-requested— запрос на бронирование от Booking -> Eventsbooking-confirmed— подтверждение бронирования от Events -> Bookingbooking-rejected— отказ (событие не найдено) от Events -> Bookingbooking-not-approved— отказ (нет мест / событие началось) от Events -> Bookingbooking-cancelled— отмена бронирования от Booking -> Eventsbooking-requested-retry— отложенные повторные попытки обработкиBookingRequestedbooking-requested-dlq— сообщения, требующие ручного разбора
После запуска будут доступны:
Users API—http://localhost:5008Events API—http://localhost:5009Booking API—http://localhost:5010AKHQ—http://localhost:8080PgAdmin—http://localhost:8020
Для локального запуска без compose можно использовать appsettings.Development.json, user secrets или переменные окружения.
Пример для Users:
dotnet user-secrets set "ConnectionStrings:DefaultConnection" "Host=localhost;Port=5433;Database=eventforge_users;Username=postgres;Password=postgres" --project EventForge.Users/EventForge.Users.PresentationАналогично настраиваются EventForge.Events.Presentation и EventForge.Booking.Presentation.
В appsettings*.json сервисов Booking и Events используется секция KafkaOptions.
Пример:
{
"KafkaOptions": {
"BootstrapServers": "localhost:9092",
"ConsumerGroup": "eventforge-events",
"InPlaceRetryCount": 3,
"InPlaceRetryBaseDelayMs": 200,
"RetryTopicMaxAttempts": 5,
"RetryTopicInitialDelaySeconds": 30,
"RetryTopicMaxDelaySeconds": 900
}
}В проект добавлен базовый observability-стек:
Prometheus— сбор метрик (prometheus.yml,metrics_path: /metrics)Grafana— визуализация метрик и дашбордыJaeger— трассировка (OpenTelemetry OTLP + UI)AKHQ— UI для Kafka (просмотр топиков/сообщений)
| Инструмент | URL | Порт |
|---|---|---|
| Grafana | http://localhost:3300 | 3300 |
| Prometheus | http://localhost:9090 | 9090 |
| Jaeger UI | http://localhost:16686 | 16686 |
| AKHQ | http://localhost:8080 | 8080 |
Для Kafka добавлены:
- клиентские метрики producer/consumer (publish/consume/process counters и latency histograms);
- сквозная трассировка через Kafka headers (
traceparent/tracestate); - перенос trace-context через Outbox (
TraceParent,TraceStateвOutboxMessages); - продолжение trace в consumer'ах с
ActivityKind.Consumer.
Класс KafkaMetrics регистрирует RED-метрики через MeterFactory:
eventforge.kafka.messages_published— счётчик отправленных сообщенийeventforge.kafka.publish_duration_seconds— гистограмма длительности отправкиeventforge.kafka.messages_consumed— счётчик полученных сообщенийeventforge.kafka.consume_duration_seconds— гистограмма длительности обработки
Метрики регистрируются в OpenTelemetry через AddMeter("EventForge.Booking.Kafka") / AddMeter("EventForge.Events.Kafka").
Результат:
- в
Jaegerвидна непрерывная цепочка:HTTP -> Command/Handler -> Outbox -> Kafka Producer -> Kafka Consumer -> DB; - в
Prometheus/Grafanaвидны технические метрики Kafka-клиентов по сервисам.
Если сервисы уже запущены, поднимите только мониторинг:
docker compose up -d prometheus grafana jaegerЕсли нужен полный локальный стенд:
docker compose up -dФайл дашборда:
grafana/provisioning/dashboards/files/EventForge dashboard 1_0.json
Панели в дашборде:
Процент загрузки процессора (CPU)ExceptionsОбъем ОЗУАктивные потокиLatency (p50, p95, p99)Текущее количество запросов в обработкеThroughput (RPS)
Все три сервиса предоставляют Swagger UI через общий проект EventForge.Shared/EventForge.Swagger.
- API-версионирование через
Asp.Versioning— версия читается из URL (/v1/Events) - JWT-аутентификация в Swagger UI — кнопка Authorize с поддержкой Bearer-токенов
- XML-комментарии — автоматический импорт документации из
<summary>тегов - Кастомный UI — встроенные JS/CSS для копирования токена, переключения API-версий и темы Material
- Swagger UI доступен в окружениях
DevelopmentиDocker
В Program.cs каждого сервиса:
using EventForge.Swagger;
// В Presentation/DependencyInjection.cs:
services.AddSharedSwagger("EventForge Booking API");
// В Program.cs:
if (app.Environment.IsDevelopment() || app.Environment.IsEnvironment("Docker"))
{
app.UseSwagger();
app.UseSharedSwaggerUI("Booking"); // "Users" или "Events" для других сервисов
}Каждый сервис предоставляет endpoint /health для проверки состояния инфраструктурных зависимостей.
| Сервис | PostgreSQL | Redis | Kafka |
|---|---|---|---|
| Users | ✅ | ✅ | ✅ |
| Events | ✅ | ✅ | ✅ |
| Booking | ✅ | ❌ | ✅ |
Проверки регистрируются в Infrastructure-слое через пакеты AspNetCore.HealthChecks.*:
services.AddHealthChecks()
.AddNpgSql(configuration.GetConnectionString("DefaultConnection")!)
.AddRedis(configuration.GetConnectionString("Redis")!)
.AddKafka(setup =>
{
setup.BootstrapServers = configuration["KafkaOptions:BootstrapServers"]!;
}, name: "kafka");В Program.cs маппится эндпоинт:
app.MapHealthChecks("/health");Для всех трёх сервисов используется Serilog как провайдер структурного логирования с выводом в консоль в формате Compact JSON.
В Program.cs каждого сервиса:
using Serilog;
using Serilog.Formatting.Compact;
builder.Logging.AddConsole();
builder.Host.UseSerilog((ctx, cfg) =>
cfg.ReadFrom.Configuration(ctx.Configuration)
.WriteTo.Console(new CompactJsonFormatter()));Serilog читает конфигурацию из appsettings.json (секция Serilog), что позволяет гибко настраивать уровни логирования и sinks без изменения кода.
LoggingBehavior (из EventForge.Behaviors) логирует каждый CQRS-запрос через ILogger<LoggingBehavior<TReq,TRes>>:
CQRS start {Name}— при входе в обработчикCQRS success {Name}— при успешном выполненииCQRS failed {Name}— при исключении (с деталями ошибки)
Миграции создаются отдельно для каждого сервиса.
dotnet ef migrations add <MigrationName> --project EventForge.Users/EventForge.Users.Infrastructure --startup-project EventForge.Users/EventForge.Users.Presentationdotnet ef migrations add <MigrationName> --project EventForge.Events/EventForge.Events.Infrastructure --startup-project EventForge.Events/EventForge.Events.Presentationdotnet ef migrations add <MigrationName> --project EventForge.Booking/EventForge.Booking.Infrastructure --startup-project EventForge.Booking/EventForge.Booking.PresentationПрименение миграций:
dotnet ef database update --project <InfrastructureProject> --startup-project <PresentationProject>В репозитории есть тестовые проекты для микросервисов в EventForge.*/Tests.
dotnet test EventForge.Users/Tests/EventForge.Users.UnitTests/EventForge.Users.UnitTests.csproj
dotnet test EventForge.Users/Tests/EventForge.Users.IntegrationTests/EventForge.Users.IntegrationTests.csproj
dotnet test EventForge.Users/Tests/EventForge.Users.e2eTests/EventForge.Users.e2eTests.csprojdotnet test EventForge.Events/Tests/EventForge.Events.UnitTests/EventForge.Events.UnitTests.csproj
dotnet test EventForge.Events/Tests/EventForge.Events.IntegrationTests/EventForge.Events.IntegrationTests.csproj
dotnet test EventForge.Events/Tests/EventForge.Events.e2eTests/EventForge.Events.e2eTests.csprojdotnet test EventForge.Booking/Tests/EventForge.Booking.UnitTests/EventForge.Booking.UnitTests.csproj
dotnet test EventForge.Booking/Tests/EventForge.Booking.IntegrationTests/EventForge.Booking.IntegrationTests.csproj
dotnet test EventForge.Booking/Tests/EventForge.Booking.e2eTests/EventForge.Booking.e2eTests.csprojIntegration-тесты используют Testcontainers.PostgreSql, поэтому для них нужен запущенный Docker Desktop.
Каждый сервис содержит проект ArchitectureTests на базе NetArchTest.Rules:
LayerDependencyTests— проверка соблюдения слоёной архитектуры (Presentation → Application → Domain)NamingConventionTests— проверка соглашений об именованииInterfaceImplementationTests— проверка, что все интерфейсы имеют реализации
Сервис EventForge.Events использует Redis для кэширования часто запрашиваемых данных. Цель — снизить нагрузку на БД при повторяющихся чтениях и ускорить ответ API.
| Данные | Ключ | TTL (по умолчанию) |
|---|---|---|
| Одно событие | event:{guid} |
5 минут |
| Топ-10 событий | events:top10 |
10 минут |
Оба ключа определены в EventForge.CacheKeys.KeysForEvents.
- Пагинированный поиск с фильтрами (
GET /Events?title=...&page=...) — слишком много уникальных комбинаций фильтров, попадание в кэш маловероятно, а инвалидация при любом изменении любого события была бы неоправданно сложной. - Операции записи (
POST,PUT,DELETE) — только читают/пишут БД и инвалидируют затронутые кэш-ключи.
Используется инвалидация при изменении (cache-aside + write-invalidate):
| Операция | Инвалидируемые ключи |
|---|---|
ChangeEventAsync |
event:{id} |
CancelEventAsync |
event:{id} |
ReleaseSeatAsync |
event:{id}, events:top10 |
BookingRequestedConsumer (бронь подтверждена) |
event:{id}, events:top10 |
Примечание: ключ events:top10 инвалидируется только при изменении заполненности мест (ReleaseSeatAsync, BookingRequestedConsumer). При удалении события (CancelEventAsync) этот агрегированный кэш принудительно не сбрасывается и обновляется по TTL. Это оправдано, так как удаления происходят редко и полная очистка топа для них неэффективна.
Кэширующие методы GetEventAsync и GetTop10EventsAsync используют double-check locking через ConcurrentDictionary<string, SemaphoreSlim>:
- Быстрая проверка кэша (без блокировки).
- Если промах — захват
SemaphoreSlimдля данного ключа. - Повторная проверка кэша (другой поток мог уже заполнить).
- Только один поток идёт в БД, остальные ждут и получают результат из кэша.
Это гарантирует, что при истечении TTL на горячий ключ в БД уйдёт ровно один запрос, а не N параллельных.
Интерфейс ICacheService абстрагирует работу с Redis. Если Redis недоступен:
GetStringAsyncвозвращаетnull→ сервис прозрачно идёт в БД.SetStringAsyncиRemoveAsyncsilently fail (ошибки логируются, но не пробрасываются наружу).
Таким образом, Redis является опциональным ускорителем, а не критической зависимостью.
Настройки Redis задаются в appsettings.json секцией RedisOptions:
{
"RedisOptions": {
"SingleEventExpirationMinutes": 5,
"TopEventsExpirationMinutes": 10
}
}Строка подключения к Redis задаётся в секции ConnectionStrings:
{
"ConnectionStrings": {
"Redis": "localhost:6379"
}
}Регистрация пользователя:
curl -X POST 'http://localhost:5008/auth/register' \
-H 'Content-Type: application/json' \
-d '{
"login": "admin-user",
"password": "SecurePassword123!",
"role": "Admin"
}'Логин:
curl -X POST 'http://localhost:5008/auth/login' \
-H 'Content-Type: application/json' \
-d '{
"login": "admin-user",
"password": "SecurePassword123!"
}'Создание события под Admin:
curl -X POST 'http://localhost:5009/Events' \
-H 'Authorization: Bearer <JWT>' \
-H 'Content-Type: application/json' \
-d '{
"title": "DotNet Meetup",
"description": "Community event",
"startAt": "2026-08-15T10:00:00Z",
"endAt": "2026-08-15T13:00:00Z",
"totalSeats": 50
}'Получение списка событий:
curl 'http://localhost:5009/Events?title=dotnet&page=1&pageSize=10'Создание бронирования:
curl -X POST 'http://localhost:5010/Bookings/<EVENT_ID>' \
-H 'Authorization: Bearer <JWT>'Получение бронирования:
curl 'http://localhost:5010/Bookings/<BOOKING_ID>' \
-H 'Authorization: Bearer <JWT>'Отмена бронирования:
curl -X DELETE 'http://localhost:5010/Bookings/<BOOKING_ID>' \
-H 'Authorization: Bearer <JWT>'- Для полного end-to-end сценария между сервисами, кроме PostgreSQL, потребуется Kafka broker