redb.Route.RabbitMQ
redb.Route.RabbitMQ

Про Kafka в этой серии уже было. Теперь — RabbitMQ, и с упором на то, как им пользоваться. Коннектор redb.Route.RabbitMQ — поверх официального RabbitMQ.Client 7.x, но писать вам придётся не «клиент», а маршруты, где весь брокер задаётся одной строкой-URI:

From("rabbitmq://orders?exchange=shop&exchangeType=topic&routingKey=order.*&concurrentConsumers=4")
    .Log("Order in: ${body}")
    .To("direct://process");

Прочитали — и уже знаете, что происходит: слушаем очередь orders, привязанную к topic-обменнику shop по маске order.*, в четыре потока, логируем и отдаём дальше. Ни «створи ченнел», ни «забинди», ни «поставь QoS» — всё это коннектор.

Про «уходим от MassTransit» сразу оговорюсь честно: дело не в том, что MassTransit чего-то не умеет — умеет, и RPC (request/response через IRequestClient<T>) в том числе. Разница в модели. MassTransit — это шина с контрактами сообщений, консьюмерами и конфигурацией автобуса: покупаешь всю его картину мира целиком. redb.Route — это явные маршруты в духе Apache Camel: endpoint — строка-URI, паттерны интеграции — шаги маршрута, транспорт — абстракция под ними. Меньше церемоний с контрактами и регистрациями, больше явного «откуда → куда → что по дороге». Что удобнее — вопрос вкуса и задачи; эта статья — про то, как RabbitMQ выглядит во второй модели.

Дальше — как этим пользоваться: анатомия URI, все параметры справочником (что и зачем), заголовки, которые ставит консьюмер, конкурирующие консьюмеры (многопоточность), широковещание по нодам кластера (реальный пример из прода), RPC (у RabbitMQ он получается особенно изящным) и два EIP-паттерна, которые на брокер ложатся идеально — Filter (на компилируемых предикатах) и Dead Letter Channel.

Весь код — на английском, весь текст — по-русски. Примеры не выдуманные — это redb.Route/demos/redb.Route.Demo/Routes, тот же демо-проект, что гоняется в CI.

Цикл про redb и redb.Route. Это продолжение серии, свежие статьи — сверху:

Полный список — в профиле. Исходники: github.com/redbase-app. Про саму БД: redb.ru.


Анатомия URI

Endpoint — это строка:

rabbitmq://queue-name?host=localhost&exchange=my-exchange&exchangeType=topic&routingKey=order.*

Схема rabbitmq://, хост URI — имя очереди, дальше query-параметры (имена — в таблице ниже). Одна и та же строка работает и консьюмером (в From(...)), и продюсером (в .To(...)) — роль определяется тем, где endpoint стоит в маршруте. Подключение можно не дублировать: заведите фабрику подключения в реестре и сошлитесь через connectionFactory=имя — host/креды/SSL лягут в одно место, соединение переиспользуется.

Важная деталь: значения параметров — это шаблоныroutingKey=order.new — константа, а routingKey=order.${header.type} — динамика: ключ вычисляется на каждое сообщение из его заголовка (тот же механизм, что и ключ партиционирования у Kafka). Так что «строка» здесь не значит «только статика» — ${...} внутри значения превращает его в выражение.

Про флюент. У коннектора есть и билдер Rabbit.Queue("orders")…, но он expression-first: строковые методы принимают выражение, чтобы значение продюсера можно было вычислять на каждое сообщение (.RoutingKey(Header("order.type")) — динамический ключ, как у Kafka). Для константы там оборачивают (.Host(Constant("localhost"))). В этой статье я пишу через URI — для статичной конфигурации это короче и нагляднее; про динамику через выражения — отдельно.


Все параметры

Главная причина сохранить статью в закладки. Имена — как в URI, дефолты — из коробки.

Подключение

Параметр

Дефолт

Зачем

host

localhost

Хост брокера

port

5672

Порт AMQP

username / password

guest / guest

Креды

virtualHost

/

Виртуальный хост — изоляция ресурсов внутри брокера

connectionFactory

Имя фабрики в реестре: настройки в одном месте, соединение переиспользуется

clientName

redb.Route

Имя соединения — видно в management UI

Обменник

Параметр

Дефолт

Зачем

exchange

(пусто)

Имя обменника; пусто = default exchange (маршрут по имени очереди)

exchangeType

direct

direct / topic / fanout / headers

exchangeDurable

true

Переживает рестарт брокера

exchangeAutoDelete

false

Удалить, когда не используется

declare

false

Создать обменник+очередь на старте, если их нет

Очередь

Параметр

Дефолт

Зачем

queue

(из хоста URI)

Имя; пусто = авто-генерируемая эксклюзивная

durable

true

Переживает рестарт

autoDelete

false

Удалить, когда ушёл последний консьюмер

exclusive

false

Допустим только один консьюмер

Маршрутизация и сообщение

Параметр

Дефолт

Зачем

routingKey

(пусто)

Ключ для publish и bind

contentType

application/json

Content-type сообщений

messageTtl

0

TTL сообщения, мс (x-message-ttl)

expires

0

Очередь удаляется после N мс без консьюмеров (x-expires)

Консьюмер

Параметр

Дефолт

Зачем

concurrentConsumers

1

Единственный рычаг параллелизма — до N сообщений обрабатываются одновременно

prefetchCount

10

Префетч на консьюмера; держите ≥ concurrentConsumers

autoAck

false

true = at-most-once (без requeue при ошибке); false = at-least-once

transacted

false

Транзакционный AMQP-канал (несовместим с autoAck)

mandatory

(авто)

Вернуть недоставленное сообщение; по умолчанию true для direct/headers

RPC

Параметр

Дефолт

Зачем

replyTo

false

Включить request/reply: публикуем запрос и ждём коррелированный ответ

timeout

60

Таймаут ожидания ответа, сек

maxOutstandingConfirms

2048

Лимит неподтверждённых publish — чтобы под нагрузкой не спотыкаться о rate limiter клиента

Ограничения очереди и dead-letter

Параметр

Дефолт

Зачем

maxLength / maxLengthBytes

0

Максимум очереди в сообщениях / байтах (x-max-length[-bytes])

overflow

Переполнение: drop-head / reject-publish / reject-publish-dlx

deadLetterExchange

DLX — куда уходят «мёртвые» сообщения (x-dead-letter-exchange)

deadLetterRoutingKey

Ключ при перекладывании в DLX

queueType

classic / quorum (реплицированная очередь для кластера)

maxPriority

0

Максимальный приоритет для priority-очередей

Устойчивость и TLS

Параметр

Дефолт

Зачем

automaticRecovery

true

Авто-восстановление соединения

topologyRecoveryEnabled

true

Переобъявить обменники/очереди/binds после восстановления

recoveryInterval

5

Интервал попыток восстановления, сек

heartbeat

60

Интервал heartbeat («пульс»), сек — брокер и клиент пингуют друг друга, чтобы вовремя заметить мёртвое соединение

connectionTimeout

60

Не висеть на недоступном узле

ssl

false

Включить TLS

sslServerName / sslCertPath / sslCertPassphrase

SNI и клиентский сертификат для mTLS

Валидатор на старте не даст выстрелить в ногу: prefetch/concurrentConsumers/timeout должны быть > 0, а autoAck + transacted вместе прямо запрещены.


Свойства AMQP ↔ заголовки: сквозной round-trip

Коннектор мапит свойства сообщения RabbitMQ на заголовки exchange в обе стороны — и на консьюмере, и на продюсере. Консьюмер, приняв доставку, раскладывает BasicProperties по заголовкам; продюсер на отправке читает их обратно и ставит на исходящее сообщение. Значит consume → produce переносит все AMQP-свойства насквозь, а любое из них можно задать/переопределить, просто выставив заголовок.

Именование в два яруса.

Метаданные доставки — стандартного AMQP-имени нет, поэтому префикс redbRmq.:

Заголовок

Что внутри

redbRmq.Exchange

обменник, через который пришло сообщение

redbRmq.RoutingKey

routing key доставки

redbRmq.Redelivered

флаг повторной доставки (после nack/requeue или падения консьюмера)

redbRmq.DeliveryTag

тег доставки (используется для ack)

redbRmq.ConsumerTag

тег консьюмера

Стандартные свойства сообщения — по общеизвестным именам, без префикса (их и так все знают, и round-trip несёт их как есть):

Заголовок

Свойство AMQP

CorrelationId

correlation-id

ReplyTo

reply-to — адрес очереди для ответа

MessageId

message-id

Priority

приоритет 0–9

Expiration

TTL сообщения, мс

Type / AppId / UserId

тип / приложение / пользователь

ContentType / ContentEncoding

content-type / кодировка

Timestamp

метка времени

Persistent

persistent / transient

Особенно полезен redbRmq.Redelivered: по нему отдельно обработать «а это уже вторая попытка» — увести повторную доставку в другой маршрут вместо бесконечного requeue.

ReplyTo-заголовок ≠ параметр replyTo. Похожие имена — разные вещи. URI-параметр replyTo=true включает клиентский RPC-режим (см. ниже). А заголовок ReplyTo — это само AMQP-свойство reply-to: выставив его на обычной отправке, вы задаёте адрес reply-очереди сообщения напрямую, и продюсер прокинет заголовок в свойство. Всё это работает потому, что маппинг свойств двусторонний — не только консьюмер их читает, но и продюсер уважает.


Рецепт 1: pub/sub

Fanout-обменник рассылает копию всем подписчикам, ключ игнорируется. Продюсер публикует в обменник, каждый консьюмер объявляет свою очередь и биндит:

// издатель — публикует в fanout-обменник (копия всем подписчикам)
From("timer://tick?period=5000")
    .SetBody(_ => BuildEvent())
    .To("rabbitmq://events-pub?exchange=events&exchangeType=fanout");

// два независимых подписчика — каждый со своей очередью
From("rabbitmq://audit?exchange=events&exchangeType=fanout&declare=true").To("direct://audit");
From("rabbitmq://email?exchange=events&exchangeType=fanout&declare=true").To("direct://email");

Нужна маршрутизация похитрее — берёте exchangeType=topic и routingKey с масками (order.*#.error). Меняется только тип обменника и ключ.

Рецепт 2: конкурирующие консьюмеры (многопоточность)

Один параметр — concurrentConsumers:

From("rabbitmq://orders?concurrentConsumers=8&prefetchCount=16")   // до 8 сообщений параллельно
    .Process(HandleOrder);

Это единственный рычаг параллелизма консьюмера: он задаёт и AMQP dispatch concurrency у канала, и app-level семафор. Дефолт — 1 (строго серийно, порядок сохранён). Ставите N > 1 — порядок на очереди больше не гарантируется, обработчик должен быть потокобезопасным, а prefetchCount держите ≥ N.

Ремарка для тех, кто щупал коннектор раньше: до версии 3.2.2 concurrentConsumers из-за одной ловушки в RabbitMQ.Client 7.x молча не давал параллелизма (канал был зажат в серийную выдачу). С 3.2.2 это единственный и работающий рычаг — если вы ставили N > 1 и «оно как-то не ускорялось», обновитесь.

Подтверждения — тоже параметром. По умолчанию at-least-onceack после успешного прохода, nack+requeue при ошибке. Нужна семантика «получил и забыл» — autoAck=true переключает на at-most-once (брокер гасит доставку на выдаче, ошибка не возвращает сообщение).

А если параллелить надо не приём, а обработку внутри маршрута (источник серийный, но работа тяжёлая) — есть ортогональный EIP-шаг .Threads(N)concurrentConsumers масштабирует чтение с очереди, .Threads — обработку; комбинируются свободно.

Рецепт 3: широковещание по нодам кластера (инвалидация локального кеша)

Реальный паттерн из прода. Сервис работает кластером из нескольких нод, у каждой — свой локальный кеш. Пишем в БД на любой ноде — и хотим, чтобы кеш обновился на всех. Классика поверх RabbitMQ: продюсер аудита публикует доменное событие в durable topic-обменник, а каждая нода вешает на него свою временную очередь и слушает.

Продюсер (там, где идёт запись в БД) — по маршруту на тип события, все в один обменник lt.events, каждый со своим routing key. Заголовки проставляем сразу, чтобы подписчики роутили/фильтровали, не разбирая тело. Подключение — через общую фабрику:

// общий stamp: доменные заголовки для подписчиков — маршрутизация без парсинга тела
Action<IExchange> stamp = e =>
{
    var a = e.In.GetBody<AuditEvent>();
    e.In.Headers["CorrelationId"] = a.CorrelationId;
    e.In.Headers["EntityType"]    = a.EntityType;
    e.In.Headers["EventType"]     = a.EventType.ToString();
};

From("direct://audit.entity.created")
    .RouteId("audit-entity-created")
    .Process(stamp)
    .SetBody(e => JsonSerializer.Serialize(e.In.GetBody<AuditEvent>()))
    .To("rabbitmq://?connectionFactory=RabbitMQ&exchange=lt.events&exchangeType=topic"
        + "&routingKey=dal.events.created&exchangeDurable=true");

// updated / deleted / batch / error — тот же обменник, routingKey=dal.events.{updated|deleted|…}

Консьюмер на каждой ноде — вот тут вся соль. queue= пустой + autoDelete=true: каждая нода получает свою временную очередь (имя выдаёт сервер), привязанную к общему обменнику. Нода отключилась — очередь исчезла. Значит копию каждого события получает каждая нода:

// created и updated — два консюмера в одну общую логику
From("rabbitmq://?connectionFactory=RabbitMQ&exchange=lt.events&exchangeType=topic"
        + "&routingKey=dal.events.created&queue=&autoDelete=true&expires=300000"
        + "&concurrentConsumers=2&prefetchCount=10")
    .RouteId("cache-consumer-created")
    .To("direct://cache-update");

From("rabbitmq://?connectionFactory=RabbitMQ&exchange=lt.events&exchangeType=topic"
        + "&routingKey=dal.events.updated&queue=&autoDelete=true&expires=300000"
        + "&concurrentConsumers=2&prefetchCount=10")
    .RouteId("cache-consumer-updated")
    .To("direct://cache-update");

// один процессор на оба потока — обновляет локальный кеш этой ноды
From("direct://cache-update")
    .RouteId("cache-update")
    .Process(new CacheUpdateProcessor(/* … */));

Ключевое — контраст с Рецептом 2. Там competing consumers: одна общая durable-очередь, работа делится между консюмерами (каждое сообщение достаётся кому-то одному). Здесь наоборот: у каждой ноды своя ephemeral-очередь на общий обменник → широковещание, событие получают все. Один коннектор, противоположная топология — разница в одном параметре (queue=имя&durable=true против queue=&autoDelete=true). Запись в БД на любой ноде → одно событие в обменник → локальный кеш обновился на всех нодах, без опроса и без единой точки отказа.

Рецепт 4: RPC (request/reply)

Вот где особенно приятно. Классический RPC на RabbitMQ — это временная reply-очередь, correlationId, ожидание ответа. Здесь — один параметр на клиенте и ничего на сервере. Вот как это задано в демо (DemoEndpoints.cs):

// сервер — обычный консьюмер, declare создаёт очередь
public const string RabbitConsumer =
    "rabbitmq://demo-rpc-queue?host=localhost&port=5672&username=admin&password=admin&declare=true";

// клиент — replyTo=true включает RPC, timeout=15 — ждать ответ не дольше 15 с
public const string RabbitProducer =
    "rabbitmq://demo-rpc-queue?host=localhost&port=5672&username=admin&password=admin&replyTo=true&timeout=15";

Клиент — обычный .To(...), только он не «выстрелил и забыл», а дождался ответа и поехал дальше уже с ним (MainPipelineRoutes.cs):

.Log("[3-RABBIT] → Sending to RabbitMQ RPC...")
.To(RabbitProducer)                                      // публикуем запрос и ЖДЁМ ответ
.Log("[3-RABBIT] ← stamp.rabbit=${header.stamp.rabbit}") // ответ уже в exchange

Сервер — просто консьюмер, ему для RPC настраивать не надо ничего. Коннектор сам видит reply-to на входящем сообщении, делает маршрут InOut и отправляет то, что лежит в Out, обратно в reply-очередь с тем же correlationId. Весь «сервер» из демо:

From(RabbitConsumer)
    .RouteId("demo-rabbit-worker")
    .Log("[RABBIT-W] ▶ Received: ${body}")
    .SetHeader("stamp.rabbit", e => $"ok:{DateTime.UtcNow:HH:mm:ss.fff}")
    .Log("[RABBIT-W] ◀ Stamped, replying");         // reply уходит сам

Ни слова про reply-очередь и корреляцию — клиент отправил запрос и получил обратно заголовок stamp.rabbit, проставленный сервером. Чистый request/reply поверх брокера, без ручной возни с временными очередями.

Тут и виден контраст с MassTransit. Там request/response тоже есть — но это IRequestClient<TRequest>, типизированные контракты TRequest/TResponse и консьюмер, зарегистрированный в автобусе. Мощно, но это отдельный слой абстракций, в который надо войти. Здесь — replyTo=true в URI, а на сервере вообще ничего: обычный консьюмер, коннектор сам увидел reply-to и ответил. Разные подходы к одной задаче: контракты и шина против одного флага на endpoint.

Рецепт 5 (EIP): Filter — пропускаем только нужное

Message Filter — простой и постоянно нужный EIP: пропустить дальше только те сообщения, что удовлетворяют условию, остальные — молча отбросить. В redb.Route это .Filter(...) … .EndFilter() (или короче .End() — это алиас): блок между ними выполняется только для прошедших фильтр, для остальных маршрут на этом заканчивается. И вот тут разворачивается компилируемый предикатный движок — тот самый, что мы обсуждали в теме про выражения.

Условие задаётся тремя способами. Предикатом по заголовку (или телу):

From("rabbitmq://orders?concurrentConsumers=4")
    .Filter(Header("score").isGreaterThan(50))       // score > 50
        .To("rabbitmq://vip-orders")
    .EndFilter();

Набор предикатов — весь, что ожидаешь: .isEqualTo / .isNotEqualTo / .isGreaterThan / .isLessThan / .isGreaterThanOrEqualTo / .isLessThanOrEqualTo / .isBetween(a, b) / .contains(...). Слева — Header("...") или Body():

.Filter(Header("email").contains("@example"))   .To("direct://internal") .EndFilter();
.Filter(Header("grade").isEqualTo("B+"))         .To("direct://grade-b")  .EndFilter();

Строковым выражением — ${...} резолвится, результат приводится к boolean. Для truthy-проверки — просто значение; для сравнения оберните его в logical(...) (голое ${header.score > 50} внутри шаблона вернёт пустую строку — так писать нельзя):

.Filter("${header.status}")                  // 'active' → truthy → пропустит
    .Log("active only")
.EndFilter();

.Filter("${logical(header.score > 50)}")     // сравнение → "True"/"False"
    .To("rabbitmq://vip")
.EndFilter();

Или лямбдой, когда нужна произвольная логика на C#:

.Filter(ex => ex.In.Body is string s && s.Contains("urgent"))
    .To("rabbitmq://urgent")
.EndFilter();

Фильтровать удобно как раз по тем заголовкам, что консьюмер уже проставил, — например, .Filter(Header("redbRmq.RoutingKey").contains("order")), чтобы в блок попадали только сообщения с нужным ключом маршрутизации.

Как выражение превращается в bool

Под капотом Filter на каждое сообщение вычисляет выражение в object? и приводит к bool — по правилам ConvertToBoolean:

bool   → как есть
string → "true"/"false" парсятся; иначе непустая строка = true, пустая = false
null   → false
прочее (число/объект, не-null) → true

Отсюда — поведение трёх форм:

  • Предикат Header("amount").isGreaterThan(1000) — это IPredicate, у которого уже готовый Func<IExchange,bool>. Коэрция не нужна: настоящий boolean прямо от сравнения. Компилируется в делегат и кэшируется — не рефлексия на каждом сообщении, а быстрый вызов.

  • Строка-шаблон "${header.status}" — ${...} интерполируется в значение, дальше truthy-правила. status="active" → непустая строка → true (потому в демо и «active is truthy»); а "${header.enabled}" при "false" → bool.TryParse → false. Сравнение прямо в шаблоне ("${header.score > 50}") не считается — вернёт пустую строку (то есть false); для сравнения нужен logical(...), как показано выше.

  • Лямбда ex => … — что вернули, то и предикат.

То есть «булево выражение» — это либо честный bool от предиката/сравнения, либо truthy-приведение интерполированного значения. Пустая строка, null и "false" — отсекают; непустая строка, число, true — пропускают.

DSL открыт: свой шаг — это extension-метод

Обрати внимание на один момент. Строковой и предикатной форм Filter(string) / Filter(IPredicate) нет в интерфейсе IRouteDefinition — там только канонические Filter(Func<IExchange,bool>) и Filter(IExpression). Строковая форма — это extension-метод, тонкий фасад над каноническим:

// весь строковый overload целиком (redb.Route/…/RouteDefinitionCamelDslExtensions.cs):
public static FilterDefinition Filter(this IRouteDefinition self, string simpleTemplate)
    => self.Filter(new StringExpression(simpleTemplate));   // оборачиваем строку в шаблон-выражение

Отсюда важное следствие: DSL расширяемый, и свой шаг добавляется ровно так же — extension-методом с делегированием в канонический. Захотелось собственный «глагол» в маршруте — пишете:

// свой шаг: пропустить только "urgent" в теле
public static FilterDefinition OnlyUrgent(this IRouteDefinition self)
    => self.Filter(ex => ex.In.Body is string s && s.Contains("urgent"));

// и в маршруте он выглядит как родной:
From("rabbitmq://orders")
    .OnlyUrgent()
        .To("rabbitmq://urgent")
    .EndFilter();

Никакой магии и никаких правок в движке — просто метод расширения над IRouteDefinition. Ровно так же в самом фреймворке сделаны и Camel-совместимые формы, и строковые шаблоны, и предикатные overload'ы. То есть если вам не хватает какого-то шага — вы его дописываете сами, не форкая коннектор.

Здесь мы задели только край выражений — Header(...)Body()${...}logical(...). А вообще выражения в redb.Route — это отдельный компилируемый язык: доступ к телу и заголовкам, свойства exchange, индексаторы (items[0]), строковые функции, JSONPath и XPath. Всё это токенизируется, парсится в AST и компилируется в делегат (System.Linq.Expressions, не рефлексия), с кэшем. Тема большая — про сам движок будет своя статья, он того заслуживает.

Рецепт 6 (EIP): Dead Letter Channel

Из паттернов Хопе и Вульфа на RabbitMQ идеально ложится Dead Letter Channel — потому что у брокера это встроенная фича (x-dead-letter-exchange), а не эмуляция в коде. Идея: сообщение, которое не обработалось (или протухло по TTL, или не влезло в лимит очереди), не теряется и не крутится вечно в requeue, а уезжает в «морг» — отдельный dead-letter обменник, откуда его можно спокойно разобрать.

Настраивается прямо в URI рабочей очереди:

// рабочая очередь: при nack без requeue / протухании / переполнении — сообщение уходит в DLX
From("rabbitmq://orders?declare=true"
        + "&deadLetterExchange=orders.dlx"          // куда
        + "&deadLetterRoutingKey=orders.failed"     // с каким ключом
        + "&messageTtl=30000"                       // не обработали за 30 с — в DLX
        + "&overflow=reject-publish-dlx&maxLength=100000"   // и переполнение туда же
        + "&concurrentConsumers=4")
    .Process(HandleOrder);

// «морг»: отдельный маршрут слушает DLX — логирует и складывает в redb для разбора
From("rabbitmq://orders.dead?declare=true&exchange=orders.dlx&exchangeType=direct&routingKey=orders.failed")
    .Log("[DLQ] dead-lettered: ${body}")
    .ProcessWithRedb((redb, ex) => redb.SaveAsync(BuildDeadLetter(ex)));

Никакого самописного retry-цикла и «поспал-повторил» — надёжность отдаётся брокеру, а у вас остаётся чистый маршрут-разгребатель, который пишет проблемные сообщения в базу. И очередь не разбухает под наплывом: лишнее сразу уезжает в DLX, а не копится в памяти брокера.


Итог

RabbitMQ в redb.Route — это маршруты, где весь брокер задан одним URI: From("rabbitmq://…") / .To("rabbitmq://…"), а вся возня с каналами, QoS, биндами, reply-очередями и dead-letter спрятана за параметрами строки. Обменники всех типов, durable/quorum-очереди, приоритеты, TTL, DLX, авто-recovery для кластера, TLS/mTLS — набор полный, а на выходе — короткая и понятная строка. Конкурирующие консьюмеры включаются одним параметром, RPC — одним флагом на клиенте и нулём настроек на сервере, Filter отсекает лишнее компилируемыми предикатами, а Dead Letter Channel — парой параметров поверх нативного DLX.

Полный демо-проект — в redb.Route/demos/redb.Route.Demo, поднимается на локальном RabbitMQ (admin/admin) и гоняется как self-test. Ловится что-то в вашем сценарии — пишите в комментариях.

Исходники и релизы: github.com/redbase-app. Про БД redb: redb.ru. Прошлые статьи — в профиле.

If this was useful — a ⭐ on GitHub helps others find it.

Комментарии (4)


  1. lazarus_net
    13.07.2026 16:06

    Такого нейрослопа- давно не приходилось читать. Вы бы попросили его родного, стиль слегка поправить, а-то читать невозможно.

    Зашёл на сайт проекта, прочитал обещания- ау все будет работать без знания SQL и мигрироваться само собой.

    Тут на проекте как раз фанаты code-first активно кодили помощью EF-Core, а теперь разобраться что там в БД и как это в принципе работает/в основном не работает тот еще квест.

    Думаю в вашей системе будет еще веселее и быстрее.

    Особенно порадовало про Джуна который не знает про SQL но хочет писать фичи…. Начинаешь понимать адептов АИ подмена которые обещают всех джинов выгнать ….


    1. grelikt Автор
      13.07.2026 16:06

      Критика принята по части стиля — буду работать над этим. Однако мне либо код писать, либо стьатьи оформлять, я один и у меня нет за плечами корпораций и миллионов.
      НО, оно работает, и в компании один за другим переводят проекты на redb экосистему, потому что дешево. Бизнес не интересует как ты написал, бизнес интересует быстро кратко поддерживаемо расширяемо и много функционала.

      По существу: redb.Core не скрывает SQL, а убирает необходимость писать DDL вручную. Под капотом — типизированные колонки (_Long bigint, Numeric numeric(38,18), DateTimeOffset timestamptz), FK с CASCADE, индексы, CTE для деревьев. Всё это видно через ToSqlStringAsync() и get_object_json() прямо из psql.

      Два года в проде на реальной нагрузке (150k заказов/месяц, 3-нодовый кластер) — если интересно как это работает под капотом, серия "REDB изнутри" как раз об этом.

      А redb.Route позволяет писать интеграции — и весь бэк — значительно быстрее. redb.Tsak разворачивает приложение сразу на уровне enterprise: hot-reload, кластер, дашборд из коробки.

      Жду технической критики — именно её не хватает в комментариях. С тех. аргументами.


      Да, я не копирайтер и не техWрайтер — пытаюсь поделиться с сообществом тем что реально работает и позволяет достигать цели значительно быстрее.


      и оно всё открыто github
      и redb.Identity это в следующей статье


      я бы вообще рекомендовал скармливать readme с github нейронке, она сделает выжимку, суть. и попросить нейронку покапаться поглубже. дело 5 минут.


      1. grelikt Автор
        13.07.2026 16:06

        я не поленился и...

        Вот выжимка от нейронки:

        redb.Route — Apache Camel для .NET. Суть за 1 минуту.

        Любая интеграция = три строки:

        csharp

        From("kafka://orders")
            .Filter(Header("type").isEqualTo("new"))
            .To("rabbitmq://events");

        Что внутри:

        • 22 транспорта — Kafka, RabbitMQ, IBM MQ, AMQP, MQTT, HTTP, gRPC, SignalR, SFTP, S3, SQS, Telegram, LDAP и ещё

        • 30+ EIP паттернов — Choice, Splitter, Aggregator, WireTap, Saga, Circuit Breaker, Idempotent Consumer

        • Compiled expression engine — ${header.x++}, JSONPath, XPath → Func<IExchange, T> без интерпретатора

        • OpenTelemetry из коробки — трейсы и метрики на каждом шаге

        • Транзакции через .Transacted() — Kafka EOS, RabbitMQ confirms, IBM MQ

        Vs конкуренты:

        • MassTransit — 5 транспортов, нет EIP, message bus (не ESB)

        • NServiceBus — коммерческий после 2 эндпоинтов

        • Apache Camel — то же самое но на JVM

        Production runtime: redb.Tsak — hot-reload, кластер, Blazor dashboard, REST API

        Apache 2.0. Без per-endpoint лицензий.


  1. grelikt Автор
    13.07.2026 16:06

    @lazarus_net
    гоняю тесты с нейронкой, и я забыл вогнать поля, да и не было нужды в них, но тест дал варнинг, нейронка увидела и добавили и вот что пишет сама

    claude
    claude

    тесты от

    ____

    oidcc-basic-certification-test-plan

    redb.Identity Basic OP (manual)

    Variant:

    server_metadata=discovery, client_registration=static_client

    Alias:

    redb-final-0043

    Plan ID:

    hdV2kmVNvRuK1

    Plan Version:

    5.2.0

    Started:

    14.07.2026, 00:43:15

    Test Owner:

    developer

    Certification profile:

    Basic OP