redb 4.0: XML-маршруты, ленивые ссылки, уникальные ключи и WS-Trust в одном мажоре

Много работы было сделано ночами при свечах. Обратная связь от людей давала пищу для размышлений и фиксов: комментарии к статьям, обсуждения на GitHub, отчёты с живых стендов. И вот, отработав по-стахановски, выпускаю 4.0.0.

Это мажор всей экосистемы разом. Хранилище redb.Core, интеграционный движок redb.Route, рантайм redb.Tsak и OpenID-сервер redb.Identity выходят одним номером: 76 пакетов на NuGet (у 3.7 было 66), семь образов в GHCR, архивы под Windows и Linux. Pro-издание по-прежнему бесплатно и не требует лицензионного ключа на всей линии 4.x.

Изменений столько, что подробный разбор любого из продуктов тянет на отдельную статью, и такие статьи будут. Здесь коротко, почти списком: что появилось, что закрыто по безопасности и что нужно знать перед обновлением. Полные списки изменений лежат на redb.ru/releases.

redb.Route: маршруты в XML, тест-кит и форматы данных

Десять новых пакетов и заметная перестройка самого движка.

Общий словарь: книга EIP

Wire Tap, Content-Based Router, Splitter, Aggregator, Content Enricher, Claim Check, Idempotent Receiver: ни одно из этих названий не придумано для redb.Route. Все они из книги Грегора Хопе и Бобби Вулфа «Enterprise Integration Patterns: Designing, Building, and Deploying Messaging Solutions» (Addison-Wesley, 2003), каталога из 65 шаблонов обмена сообщениями. На этом каталоге построены Apache Camel, WSO2 Micro Integrator и redb.Route, поэтому он работает как общий словарь интеграции: узнав шаблон по названию, вы узнаете его в любом из этих инструментов. Глаголы DSL в redb.Route следуют написанию Camel: WireTap, Choice, Split, Aggregate, Enrich, ClaimCheck, IdempotentConsumer.

Весь каталог бесплатно доступен на сайте авторов, у каждого шаблона есть схема и короткое объяснение: enterpriseintegrationpatterns.com. Начинать стоит с глав о маршрутизации (Message Routing) и преобразовании сообщений (Message Transformation): форматы разных партнёров, трансляторы между ними и единая точка приёма разобраны там шаблон за шаблоном.

Новые пакеты

  • redb.Route.Xml. Маршрут описывается декларативно в .route.xml и загружается в тот же fluent DSL, что пишется на C#. Документ разбирается целиком при загрузке, и все ошибки приходят одним списком: неизвестный элемент с подсказкой, битое выражение, незарегистрированная схема эндпоинта. XSD генерируется из реестра элементов, context.xml описывает компоненты и бины контекста, генератор печатает XML обратно в C#. Хранилище redb доступно прямо из разметки через <redbGet> и <redbQuery>.
  • redb.Route.TestKit. Тест маршрута без брокеров и без правок в самом маршруте: AdviceRoute с ReplaceFrom, MockEndpoints("kafka://*"), MockEndpoint с ожиданиями и сценарными ответами, NotifyBuilder. От тестового фреймворка не зависит.
  • redb.Route.Templates. Тело сообщения из шаблона Scriban или Liquid. Тип результата задаёт экранирование подставленных значений, шаблон компилируется при Start(), песочница не даёт вызывать методы.
  • redb.Route.DataFormats.Csv, .Protobuf, .Avro, .Yaml, плюс Base64, GZip и Zip в самом redb.Route. У Protobuf и Avro есть обрамление Confluent.
  • redb.Route.JsonTransform. Преобразование JSON в JSON на JSONata с нативным .NET-движком.
  • redb.Route.Cache. Кэш как EIP: скоуп Cache(key, ttl)…EndCache() и компонент cache:, в памяти процесса или через любой IDistributedCache.
  • redb.Route.XPath2. XPath 2.0 для того, чего нет в 1.0: регулярные выражения, последовательности, if, for, some и every.

Движок

  • Один язык выражений. Условия, значения и ${...}-шаблоны идут через одну грамматику и один компилятор вместо трёх движков. Битое условие роняет Start(), а не превращается в константу на живом трафике. В языке появились stats(target, metric) со статистикой эндпоинтов, format(value, pattern, culture), uuid() и оператор %.
  • Компонент bean:. Пользовательский код как обычный эндпоинт: объект берётся из реестра контекста или создаётся с DI через конструктор, параметры URI биндятся на его свойства.
  • Перехваты и завершение в форме Apache Camel: Intercept, InterceptFrom, InterceptSendToEndpoint, OnCompletion.
  • Агрегация без лямбд. Библиотека готовых AggregationStrategies (сбор тел в список, конкатенация, сумма, минимум и максимум по выражению, слияние в заголовок или свойство), Enrich без стратегии, throttle с лимитом из сообщения. Aggregate, RecipientList, DynamicRouter, IdempotentConsumer, Enrich и PollEnrich принимают выражение, а не только делегат.
  • REST DSL в redb.Route.Http. Rest("/api/orders").Get("/{id}") с биндингом JSON, Consumes/Produces и OpenAPI 3.0.3 по адресу {basePath}/openapi.json.
  • Метрики изнутри маршрута. IExchange.Context и UseMetricsSnapshot(): слой OpenTelemetry читается прямо в процессе.
  • Без Newtonsoft.Json. jpath переехал на JsonPath.Net (RFC 9535) поверх System.Text.Json, и в зависимостях redb.Route осталась одна JSON-модель.

Нагрузка и хостинг

  • Лимиты приёма на эндпоинт. HTTP, SOAP, AS2 и gRPC получили maxConcurrentRequests и очередь ожидания: лишний запрос получает 429 с Retry-After до запуска конвейера, а соседний маршрут на том же порту свой бюджет не теряет. Отказы идут в отдельный счётчик Rejected, так что сброс нагрузки не путается с ошибками.
  • concurrentConsumers=auto у RabbitMQ, AMQP 1.0, IBM MQ, SQS и MQTT: число потребителей по числу процессоров, но не меньше двух. По умолчанию по-прежнему один, чтобы сохранялся порядок сообщений.
  • WebSocket и SignalR на общем Kestrel рядом с HTTP, gRPC, SOAP и AS2: один порт, аутентификация рукопожатия от хоста, шов для backplane при масштабировании.
  • Доверенные прокси на общем хосте. X-Forwarded-For проходится справа налево через все перечисленные прокси, X-Forwarded-Proto восстанавливает схему клиента, и каждый транспорт на хосте видит реальный адрес. Эту доработку подсказал комментарий к статье про 3.7.

Коннекторы

  • Kafka: коммит при отзыве партиции больше не теряет прочитанную, но не обработанную запись; seekTo отрабатывает по каждой партиции.
  • S3: консюмер больше не теряет объекты, настройки подключения доходят до AWS SDK.
  • Firebase: FCM multicast и управление топиками, копирование и подписанные URL в Storage, потоковое скачивание, databaseId для проектов Firestore с несколькими базами.
  • Llm: кэширование системного промпта у Anthropic со счётчиками токенов из кэша, фиксированная преамбула перед историей диалога, HTTP/2 keep-alive для длинных ответов через VPN и NAT, распознавание речи через stt:// и скачивание файлов из Telegram.
  • SOAP: консюмер обслуживает TLS, маршрут знает, кто его вызвал, и сам выбирает код fault.
  • TLS без сертификата больше нигде не открывает незашифрованный порт: WebSocket, SignalR и TCP в такой конфигурации отказываются стартовать.

redb.Core(.Pro): ленивые ссылки и уникальные ключи

Всё ниже работает одинаково на PostgreSQL, MSSQL и SQLite, во Free и в Pro.

  • Ленивые ссылки. Ссылка на границе глубины приходит заглушкой с id, схемой и хешем, и первое обращение к Props загружает ровно этот объект. Ссылку, которая должна быть ленивой на любой глубине, помечают virtual и включают EnableLazyReferences. Коллекция заглушек дозагружается одним запросом через LoadReferencesAsync, без N+1. Сохранение родителя, расчёт хеша и сериализация заглушки не будят.
  • Ключ объекта ValueUnique. Читаемая строка (номер заказа, артикул, внешний идентификатор), уникальная в рамках схемы, и upsert по ней через SaveByUniqueAsync.
  • [RedbUnique] на полях Props. Уникальность силами индекса базы для скаляров любого типа, включая decimal, даты и byte[]; для полей вложенных классов; для вложенного класса, массива или словаря целиком, где ключом становится содержимое поддерева; для элементов коллекции внутри объекта или по всей схеме. Поиск GetByUniqueAsync занимает одну пробу индекса, нарушение на трёх СУБД поднимается одним RedbUniqueViolationException. Дубли в уже накопленных данных попадают в отчёт, старт не падает.
  • Обновление схемы базы по контракту. Модуль схемы применяется при старте приложения. Если у роли нет прав менять схему, старт останавливается типизированным RedbSchemaOutdatedException, а скрипт для DBA отдают GetUpgradeScript() и redb schema --upgrade.
  • CancellationToken во всём асинхронном API. Отменённый SaveAsync откатывается полностью, а после коммита токен больше не читается: «отменено» никогда не означает «но всё-таки сохранено».
  • Интерцепторы записи IRedbSaveInterceptor в духе EF: SavingAsync может поправить объект или отменить сохранение, SavedAsync видит итог, то же самое для удаления. Под ChangeTracking интерцептор получает применённый дифф с путями свойств, из которого собирается собственный журнал аудита.
  • Уровень изоляции транзакции по запросу через BeginTransactionAsync(IsolationLevel) и DbErrorClassifier.IsSerializationFailure, чтобы повторять сериализуемые транзакции.
  • Maintenance: AnalyzeAsync обновляет статистику планировщика одним вызовом на любой СУБД, GetIndexStatsAsync читает состояние индексов.
  • LoadJsonAsync(id, depth) отдаёт объект сырым JSON без CLR-типа, а ToString() у RedbObject возвращает тот же канонический JSON.
  • byte[] хранится одним BLOB, а не строкой на каждый байт; старая раскладка конвертируется сама. Схема помнит namespace своего типа, и два Order из разных проектов больше не усыновляют схему друг друга.
  • Производительность: индексы на FK-колонках _values, ускорение ChangeTracking, хеши в SQLite как BLOB(16), индекс на _value_string теперь и на MSSQL.
  • Исправления, которые касались 3.x: сохранение родителя со ссылкой, заданной только по id, затирало объект за ссылкой; DateOnly на существующих базах читался как 0001-01-01; MSSQL обрезал вложенные ссылки в результатах LINQ на глубине 1, тогда как PostgreSQL и SQLite брали 10.

redb.Tsak: кластер на ключах базы и модули из XML

  • Кластер держится на уникальных ключах базы. Блокировки, узлы, группы и назначения модулей опираются на частичные уникальные индексы redb.Core. Два узла, стартующие на пустой базе, больше не создают два корня кластера: проигравший гонку принимает победителя. Запись назначения защищена номером эпохи лидера, и устаревший лидер не может посадить один модуль на два узла. Существующая база дозаполняет ключи сама при первом старте.
  • Модули из XML. .tpkg может нести только .route.xml и context.xml без единой сборки, и такой пакет перезагружается на лету так же, как модуль на C#. XML-маршрут в воркере видит все элементы разметки из shared-слоя: <cache>, <rest>, <redbGet>.
  • Дашборд различает сброс нагрузки и тишину. Панель Shedding Routes показывает маршруты, упёршиеся в лимит приёма, у эндпоинтов появились Rejected и Bytes Out. Автообновление страниц переживает разовую ошибку, графики мониторинга не замирают под курсором.
  • Страница About: версия дашборда и узла, аптайм, а для администратора список реально загруженных сборок redb.
  • Доверенные прокси: Tsak:Http:TrustedProxies принимает адреса и CIDR-сети, после чего троттлинг API-ключей и модульные маршруты видят реального клиента за цепочкой прокси.
  • Страницы Audit и Dead-letter работают на PostgreSQL. Вместе с ними на PostgreSQL впервые заработала ежедневная очистка tsak_audit_log, поэтому на узле, давно живущем на Postgres, перед первым прогоном стоит посмотреть размер этой таблицы.
  • Протокол кластера: heartbeat больше не откатывает одновременный cordon, узел в cordon не получает новых модулей, кластерный маршрут после остановки через API снова стартует.
  • Поставка под кластер: на общей базе Quartz в конфигурации по умолчанию работает в кластерном режиме (на SQLite он выключается сам, с предупреждением в логе), а ApiEndpoint узла определяется автоматически вместо общего для всех localhost:9090.

redb.Identity: WS-Trust, audience в токенах и консоль из страниц

  • WS-Trust фасад redb.Identity.Soap. Третий транспорт после HTTP и gRPC: Issue, Validate, Cancel и Renew поверх SOAP для систем, которые строят клиента по WSDL. Издатель, реестр клиентов и хранилище токенов общие, так что клиент, зарегистрированный по HTTP, получает токен по SOAP. Без TLS фасад не стартует, есть mTLS с закреплением отпечатка сертификата.
  • Audience по RFC 9068 в access-токенах. Ресурсы выданных scope и новое поле приложения AccessTokenAudiences попадают в aud, а management API принимает только токены со своим audience.
  • Контракты отдельным пакетом-носителем. Горячая перезагрузка одного модуля больше не расщепляет общие DTO на две копии, из-за которых типизированное тело терялось по дороге через direct-vm://.
  • Уникальность на примитивах redb.Core. На MSSQL ключи ClientId, ScopeName и ReferenceId впервые защищены индексом, а внешняя учётная запись больше не может гонкой привязаться к двум локальным пользователям.
  • Консоль администратора из страниц вместо диалогов. У провайдеров федерации появилась полноценная страница с маппингом claims, у маппера claims выбирается владелец правила, пользователь создаётся мастером, добавились каталог ресурсов API и дашборд с KPI и аудитом по категориям.
  • DPoP за прокси, терминирующим TLS, и правильный client_id в аудите для клиентов, которые передают учётные данные в теле формы.

Безопасность

Часть пунктов пришла внешними отчётами, часть найдена собственным аудитом фасадов и дашборда. Механика каждого будет в разборах по продуктам. Если у вас в проде 3.x, этот список сам по себе причина обновиться.

  • redb.Identity: HTTP-заголовок позволял выдать себя за любого пользователя. Внутренние заголовки теперь вычищаются на входе всех трёх фасадов: HTTP, gRPC и SOAP.
  • redb.Identity: страница согласия брала имя приложения и список scope из адреса, а форму согласия можно было отправить за другого пользователя без его сессии. Теперь страница строится из подписанного сервером билета, пользователь берётся только из сессии, формы проверяют Origin и не встраиваются во фрейм.
  • redb.Identity: introspect отвечал любому аутентифицированному клиенту по любому токену. Теперь только тем, кто входит в audience токена.
  • redb.Tsak: страницы дашборда открывались без входа. Теперь закрыта каждая страница, а уровни доступа совпадают с API воркера: аудит и пользователи для Admin, dead-letter и логи для Operator.
  • redb.Tsak: подпись модуля проверяется на всех путях загрузки, включая старт процесса, и загружаются ровно проверенные байты. Пароли не попадают в журнал аудита, сессии перепроверяются каждые пять минут, троттлинг входа считает пару из логина и адреса.
  • redb.Core: хешер паролей по умолчанию теперь bcrypt. Старые хеши SHA256 проходят проверку как раньше и переходят на bcrypt при следующей смене пароля.

Что ломает мажор

Список короткий, но пройти его стоит до обновления.

  • Кластеры redb.Tsak и redb.Identity обновляются остановкой. Узлы одной группы не должны работать на разных версиях: остановить всю группу, развернуть 4.0, запустить. Первый старт дозаполнит ключи.
  • Большой базе нужно окно обслуживания. Апгрейд добавляет колонки и строит частичные индексы под эксклюзивными блокировками. Удобнее заранее применить GetUpgradeScript() с одного узла и стартовать приложение с AutoApplyDatabaseUpgrades = false.
  • redb.Core: удалена ленивая загрузка Props целиком (EnableLazyLoadingForProps, WithLazyLoading(), параметр lazyLoadProps). _objects._value_string ограничен 450 символами, и база с более длинными значениями откажет в апгрейде со списком объектов, которые нужно перенести в _note или Props. Обращение к Props ссылки за пределом глубины теперь идёт в базу; в Blazor WebAssembly для этого есть LazyReferenceAccess = Throw. Сборки, скомпилированные против 3.x, нужно пересобрать.
  • redb.Route: строковые LoopExpression, DelayExpression и ThrottleExpression стали Loop, Delay и Throttle, а SetBodyExpression("…") и соседи стали SetBody(Expr("…")). Newtonsoft.Json больше не приезжает транзитивно. ${...} форматирует числа и даты инвариантно, ${...} в адресе консюмера роняет Start(), а ноль, "0", "no" и "off" в условиях теперь ложь.
  • redb.Identity: backend, который интроспектирует токены другого клиента (классическая пара SPA и BFF), нужно добавить в AccessTokenAudiences этого клиента. Таблица аудита на SQLite сменила тип колонки времени, поэтому файлы аудита SQLite перед обновлением удаляются.

Как поставить

dotnet add package redb.Core
dotnet add package redb.Postgres        # или redb.MSSql / redb.SQLite
dotnet add package redb.Postgres.Pro    # Pro, бесплатно и без ключа

dotnet add package redb.Route
dotnet add package redb.Route.TestKit

Библиотеки таргетят net8.0, net9.0 и net10.0, хостовые приложения и образы собраны на .NET 10. Tsak и Identity поставляются архивами под Windows и Linux (redb-tsak, redb-identity) и образами в GHCR. Для первого знакомства с Tsak проще всего образ redb-tsak-stack, где воркер и дашборд живут в одном контейнере.

Пакеты: nuget.org/profiles/relikt. Исходники: github.com/redbase-app.

Что дальше

По каждому продукту выйдет отдельный разбор: redb.Core с кодом по ленивым ссылкам и ключам, redb.Route с тест-китом и XML-маршрутами, Tsak и Identity со своими историями. Для XML-маршрутов готовится расширение VS Code с текстовым режимом и графовым редактором.

Спасибо всем, кто писал комментарии, открывал обсуждения и присылал отчёты с продакшена: заметная часть этого релиза выросла именно оттуда.

Если было полезно, ⭐ на GitHub поможет другим это найти.

Другие мои статьи — redb.ru/articles, ещё — на Хабре.