Под капотом

Не ORM. Не обёртка. Платформа данных с собственным компилятором выражений, протоколом массовой загрузки и движком структурного сравнения. На этой странице описано, как всё это работает, с реальными именами классов и архитектурой SQL.

Компилятор выражений

ExpressionToSqlCompiler обходит деревья выражений C# и генерирует нативный параметризованный SQL. Тернарный оператор → CASE WHEN, nullable-навигация → IS NOT NULL, StringComparisonILIKE. Покрывает крайние случаи, которые даже EF Core не обрабатывает полностью.

Diff Tree Save

Change Tracking строит два дерева ValueTreeNode (память vs БД), сравнивает их с пропуском по хешам — неизменённые поддеревья обрабатываются бесплатно, как в git. Генерируется только минимальный набор SQL-операций. Никакого delete-all/re-insert.

Структурированное хранилище, полный LINQ

Значения хранятся реляционно в типизированных колонках с FK-ограничениями — не как JSON-блобы. При этом полноценный LINQ работает: Where, OrderBy, GroupBy, Window, агрегация — всё компилируется в реальный SQL по реальным индексам. Привычный компромисс “гибкая схема = нет запросов” здесь не действует.

Базовая архитектура

IRedbService — Точка входа

Всё проходит через IRedbService. Он объединяет 10+ провайдерных интерфейсов в единый сервис. Конкретное поведение для БД обеспечивается подклассами RedbServiceBase, которые переопределяют абстрактные фабричные методы.

IRedbService
ISchemeSyncProvider Code-first синхронизация схемы. SyncSchemeAsync<T>(), автообнаружение через атрибут [RedbScheme].
IObjectStorageProvider CRUD объектов. CreateAsync, LoadAsync, SaveAsync, DeleteAsync. Стратегии: DeleteInsert, ChangeTracking.
ITreeProvider Древовидная иерархия. LoadTree, GetChildren, MoveObject, GetPathToRoot. Полиморфные (мульти-схемные) деревья.
IQueryableProvider LINQ-запросы. Query<T>()IRedbQueryable<T>. А также TreeQuery<T>() для запросов в контексте дерева.
IUserProvider CRUD пользователей и аутентификация. CreateUserAsync, ValidateUserAsync, ChangePasswordAsync, GetUsersAsync.
IRoleProvider Роли. CreateRoleAsync, AssignUserToRoleAsync, RemoveUserFromRoleAsync. Каскадное удаление при удалении роли.
IPermissionProvider Контроль доступа. CanUserEditObject, CanUserSelectObject, CanUserInsertScheme, CanUserDeleteObject.
IListProvider Упорядоченные списки. RedbListItem с ленивой загрузкой объектов. Кэширование с TTL (по умолчанию 5 мин).
IValidationProvider Правила валидации данных для каждой схемы.
IRedbContext Фасад прямого SQL. QueryAsync<T>, ExecuteAsync, BeginTransactionAsync, ExecuteAtomicAsync, пакетная генерация ID.

Инициализация — 9 шагов в InitializeAsync()

  1. Установить резолвер типов для SystemTextJsonRedbSerializer
  2. Авто-синхронизация схем — сканирование сборок на [RedbSchemeAttribute], вызов SyncSchemeAsync<T>() для каждого типа
  3. Синхронизация системной схемы UserConfigurationProps
  4. Инициализация конфигурации пользователя по умолчанию
  5. RedbObjectFactory.Initialize(this)
  6. RedbObject.SetSchemeSyncProvider(_schemeSync)
  7. WarmupMetadataCacheAsync() — вызывает SQL warmup_all_metadata_caches()
  8. InitializePropsCache() — создаёт MemoryRedbObjectCache
  9. _treeProvider.InitializeTypeRegistryAsync()

Паттерн фабрики провайдеров

RedbServiceBase определяет 10 абстрактных фабричных методов: CreateSchemeSyncProvider, CreateObjectStorageProvider, CreateTreeProvider, CreateQueryableProvider, CreateUserProvider, CreateRoleProvider, CreatePermissionProvider, CreateListProvider, CreateLazyPropsLoader, CreateValidationProvider. Каждый проект провайдера PostgreSQL/MSSQL переопределяет их для внедрения поведения, специфичного для БД. Ваш бизнес-код никогда не видит разницы.

Модель данных

RedbObject<T> — Основная сущность

Каждый объект в RedBase отображается на таблицу _objects. Системные поля хранятся в колонках. Бизнес-данные (Props) хранятся как структурированные значения со строгой типизацией и ссылочной целостностью на уровне базы данных.

_types Реестр типов (Long, String, Guid, Object, ListItem, Class, Array, Dictionary…)
← FK
_structures Определения полей. _id_type FK → _types — каждое поле имеет строгий тип, контролируемый БД
← FK
_schemes C#-класс → схема. Хеш структуры для обнаружения изменений. Соответствует атрибуту [RedbScheme]
_values Значения свойств в нативных типизированных колонках (не строках). FK на _structures и _objects. Ссылки на объекты (_Object FK → _objects) и ссылки на списки (_ListItem FK → _list_items) — «висячие» ссылки невозможны
← FK
_objects Экземпляры объектов. _id_scheme FK → _schemes. Древовидная иерархия через _id_parent (self FK, ON DELETE CASCADE). + колонки _value_* для RedbPrimitive<T>

Что это даёт

Типобезопасность: каждое поле в _structures имеет _id_type (FK → _types). Нельзя сохранить строку там, где ожидается число — схема базы данных это предотвращает.

Ссылочная целостность: _values._Object — это настоящий FK на _objects, _values._ListItem — настоящий FK на _list_items. БД предотвращает осиротевшие ссылки — никакого скрытого повреждения данных.

Нативное хранение: значения живут в типизированных колонках — NUMERIC(38,18) для денег, timestamptz для дат, uuid для GUID, bytea для бинарных данных. Индексы работают по реальным типам, а не по сериализованным строкам.

RedbObject<T> в C#

Системные поля: Id, ParentId, SchemeId, OwnerId, DateCreate, DateModify, Name, Note, Hash.

Props (generic TProps) — ваш C#-класс. Ленивая загрузка через ILazyPropsLoader.

20+ типов значений: int, long, short, byte, float, double, decimal, bool, string, DateTime, DateOnly, TimeOnly, TimeSpan, Guid, char, byte[], enum — плюс все Nullable<T>.

Контейнеры: T[], List<T>, Dictionary<string, T>, Dictionary<int, T>, Dictionary<(int, string), T> (ключи-кортежи), вложенные RedbObject<T> (одиночные и массивы), Dictionary<string, RedbObject<T>>, вложенные классы и массивы внутри значений словарей. RedbListItem — ссылка на _list_items (справочники на FK).

Древовидный вариант

TreeRedbObject<T> добавляет: Parent, Children, IsLeaf, Level, Ancestors, Descendants.

RedbPrimitive<T>

Когда Props — одиночное значение (не класс), оно хранится прямо в колонках _objects._value_* — строки в _values не нужны. Поддерживаются: все числовые типы, string, Guid, bool, DateTime, byte[].

Реляционное хранение коллекций

Массивы, словари и вложенные документы хранятся реляционно в _values — не как сериализованный JSON. Каждый элемент — это строка с типизированными колонками, FK-ограничениями и индексами.

_array_parent_id (self FK) связывает вложенные структуры. _array_index хранит позицию для массивов ('0','1','2') или строковые ключи для словарей. Это значит, что можно запрашивать отдельные элементы массива через SQLWHERE, JOIN, агрегация — без десериализации.

Трёхуровневая уникальность через частичные уникальные индексы гарантирует целостность данных на уровне БД:

  • Корневые поля: (structure, object) WHERE _array_index IS NULL AND _array_parent_id IS NULL
  • Базы вложенных массивов: (structure, object, parent) WHERE _array_index IS NULL AND _array_parent_id IS NOT NULL
  • Элементы массивов: (structure, object, parent, index) WHERE _array_index IS NOT NULL

Полиморфные типы схем

Схема — это не просто «таблица = класс». _schemes._type (FK → _types) может быть: Class, Array, Dictionary, JsonDocument, XDocument. Это означает, что RedbObject<T> может оборачивать одиночный класс, массив, словарь или полный JSON/XML-документ — всё хранится реляционно с той же FK- и индексной инфраструктурой.

Глобальная идентификация

Последовательность global_identity (начинается с 1 000 000) генерирует ID для всех таблиц — объектов, значений, структур, схем, пользователей, ролей, разрешений. Каждый ID глобально уникален во всей базе данных. Никаких коллизий между таблицами. Это обеспечивает безопасные кросс-табличные ссылки и упрощённый экспорт/импорт.

Семантика атрибутов и вычисляемые свойства

[RedbScheme("Name")] — привязывает класс к схеме и задаёт алиас (человекочитаемое имя). Атрибут также используется для автоматического обнаружения схем в сборке.

[RedbIgnore] — свойство исключается из хранения в БД (не сериализуется в _values). Используйте для вычисляемых свойств: [RedbIgnore] public double Margin => Price * Qty; Вычисляется при чтении — нулевая стоимость хранения. Работает внутри вложенных классов и массивов.

[JsonIgnore] — другая семантика: исключается из API-сериализации, но хранится в БД нормально. Полезно для внутренних полей, которые не должны утекать к клиентам.

Не-generic RedbObject: когда Props не нужен, значения хранятся прямо в колонках _objects: _value_string, _value_long, _value_bool, _value_double. Никаких строк в _values, никаких структур. Максимальная эффективность для простых пар ключ-значение.

Перегрузка операторов в Props: Props-классы могут определять operator + и другие для паттернов агрегации (например, суммирование метрик аналитики в C# после загрузки). RedBase сохраняет стандартную семантику C#.

Конвейер запросов

Два движка, один API

И Free, и Pro версии выполняют реальный SQL по тем же таблицам. Разница в том, как выражение C# превращается в SQL.

Free версия
C# Lambda
BaseFilterExpressionParser
FilterExpression
FacetFilterBuilder
JSON Facets
search_objects_with_facets()
Результаты
25+ операторов фильтрации. Поддержка nullable-полей, доступа к массивам, свойств классов. JSON-формат фасетов — стабильный контракт: можно вызывать из Python, Go, Java или сырого SQL.
Используемые SQL-функции: search_objects_with_facets, search_objects_with_facets_base, search_objects_with_projection_by_paths, search_objects_with_projection_by_ids, search_tree_objects_with_facets, search_tree_objects_with_facets_base, get_object_json, warmup_all_metadata_caches
Pro версия
C# Lambda
ProFilterExpressionParser без ограничений
ExpressionToSqlCompiler реализация для каждой БД
Нативный параметризованный SQL
ProSqlBuilder + SqlParameterCollector
Результаты
Прямая компиляция. Дерево выражений C# → нативный параметризованный SQL. Внутренний SqlExpressionVisitor рекурсивно обходит дерево. Реализация для каждой БД: PostgreSQL и MSSQL имеют собственные компиляторы.
Дополнительные возможности Pro: Sql.Function<T>("COALESCE", ...), вычисляемые выражения в WHERE, неограниченная вложенность фильтров, Distinct/DistinctBy, комбинированный GroupBy+Window, Pivot-запросы (PivotSqlGenerator — на основе CTE)
Тот же .Where(p => p.Name == "test"). Тот же IRedbQueryable<T>. Free работает. Pro устраняет два слоя интерпретации и даёт БД план запроса, который она реально может кэшировать. Разница — 3–10x на реальных нагрузках.

Почему Pro-запросы быстрее — интерпретация vs компиляция

Free — Интерпретация
C# Expression
FacetFilterBuilder
JSON {"Age":{"$gt":30}}
search_objects_with_facets()
plpgsql: разбор JSON → динамический SQL
Выполнение — новый план при каждом вызове
Два интерпретатора подряд. Нет переиспользования планов. Нет параметризации.
vs
Pro — Компиляция
C# Expression
ExpressionToSqlCompiler рекурсивный обход дерева
Параметризованный SQL WHERE pvt."Age" > @p1
Выполнение — кэшированный план
Прямая компиляция. Параметризация. План кэшируется PostgreSQL.

Free сериализует ваш C#-фильтр в JSON, передаёт его plpgsql-процедуре, которая разбирает этот JSON и строит динамический SQL конкатенацией строк — внутри базы данных, при каждом вызове. PostgreSQL видит каждый раз разный текст запроса. Нет кэширования планов. Нет параметризации на уровне БД.

Pro обходит дерево выражений в C# (SqlExpressionVisitor), генерирует стабильный SELECT … WHERE pvt."Field" = $1 с bind-переменными, управляемыми SqlParameterCollector. БД получает один и тот же текст SQL каждый раз — prepared statement, кэшированный план, нулевая интерпретация в рантайме. Предикаты на базовых полях (o._name, o._id) попадают в primary index напрямую; если в фильтре нет Props-полей, PVT CTE вообще не генерируется.

Фильтрация значений — однопроходный Pivot vs коррелированный EXISTS

Когда свойства хранятся в строках _values, фильтрация по нескольким полям требует стратегии. Выбор этой стратегии определяет потолок производительности.

Pro — PVT CTE Один проход
WITH pvt_cte AS (SELECT v._id_object,
(array_agg(_Long) FILTER (WHERE _id_structure = $1))[1] AS "Age",
(array_agg(_String) FILTER (WHERE _id_structure = $2))[1] AS "City"
FROM _values WHERE _id_structure = ANY($3)
GROUP BY v._id_object)
WHERE pvt."Age" > $4 AND pvt."City" = $5
SELECT o.* FROM _objects o JOIN pvt_cte ON ...
Один проход. Один GROUP BY. 5 полей или 50 — одинаковая цена. Все параметры безопасны.
vs
Free — Коррелированный EXISTS
WHERE o._id_scheme = $1
AND EXISTS (SELECT 1 FROM _values WHERE _id_object = o._id AND _id_structure = $2 AND _Long > 30)
AND EXISTS (SELECT 1 FROM _values WHERE _id_object = o._id AND _id_structure = $3 AND _String = 'NY')
AND EXISTS (…) × N полей
Один подзапрос на поле. Работает хорошо при 1–3 фильтрах. Замедляется с ростом числа полей.

Pro использует PvtSqlGenerator.GeneratePvtCte() для разворачивания всех нужных полей в плоские колонки CTE через один GROUP BY v._id_object. Конструкция FILTER (WHERE _id_structure = $N) разделяет значения по колонкам при агрегации — один проход по индексу (_id_structure, _id_object), одна агрегация, готово. Внешний WHERE применяет стандартные B-tree предикаты к уже плоским колонкам. Все значения параметризованы через SqlParameterCollector.

Free использует _build_exists_condition() — каждое поле фильтра становится коррелированным подзапросом EXISTS к _values. Для простых запросов с 1–2 полями это вполне эффективно — PostgreSQL хорошо оптимизирует коррелированный EXISTS при наличии индексов. С ростом числа полей фильтра однопроходный Pivot в Pro становится всё более выгодным.

Материализация — сложные объекты без сложности

Pro — Bulk + Parallel
1 BULK SELECT всех _values
1 BULK SELECT всех _list_items
Предзагрузка схем + типов (в памяти)
Parallel.ForEach — CPU-bound, нуль обращений к БД
O(1) индексы ILookup, прямое преобразование типов
RedbObject<T>
Без JSON. Без сериализации. Все ядра. O(1) доступ к полям.
vs
Free / Dapper / EF Core
get_object_json() для каждого объекта
PostgreSQL строит JSON в plpgsql
System.Text.Json.Deserialize<T>()
RedbObject<T>
Сериализация → передача → десериализация. Для каждого объекта. Последовательно.

Рассмотрим реальную сущность вроде EmployeeProps из примеров: 8 скалярных полей, 4 массива, 3 вложенных класса (каждый со своими массивами), 5 словарей (включая Dictionary<string, Address> и Dictionary<(int, string), string>), вложенные ссылки RedbObject, ссылки RedbListItem. В классическом ORM типа EF Core для этого нужно ~28 таблиц с FK-ограничениями, промежуточные таблицы для many-to-many, отдельные таблицы для каждого массива и словаря, и ~40–60 INSERT операций на один объект по всем этим таблицам в правильном FK-порядке. 100 сотрудников = 4 000–6 000 INSERTов.

Dapper эту задачу вовсе не решает — он маппит плоские строки на плоские объекты. Для графа из 28 таблиц вы пишете JOINы, FK-порядок и код сборки сами. В Dapper нет абстракции для вложенных документов, массивов или словарей.

EF Core может смоделировать это, но: DbContext делает снапшот каждой отслеживаемой сущности (полный клон в памяти для change detection), не является потокобезопасным (Parallel.ForEach структурно невозможен), время жизни кэша = время жизни DbContext (один HTTP-запрос), а порядок INSERT по 28 таблицам с каскадными FK — известная головная боль.

Pro хранит тот же EmployeeProps в 2 таблицах (_objects + _values). Загрузка: 2 bulk SELECT, затем ProLazyPropsLoader индексирует всё в ILookup и запускает Parallel.ForEach — чистый CPU, O(1) на поле. ProPropsMaterializer обрабатывает 20+ типов значений, вложенные объекты, массивы, словари, словари с кортежными ключами, ссылки ListItem — всё из плоских строк _values, без JSON-этапа. При повторных загрузках GlobalPropsCache возвращает объект из памяти с проверкой хеша — вообще без SQL. Сохранение: один BulkInsert через протокол PostgreSQL COPY. 100 сотрудников ≈ 3 000 строк, одна команда.

Проекция — загружайте только то, что выбираете

.Select(x => new { x.Props.Name, x.Props.City }) — проекция указывает движку, какие именно поля вам нужны. Обе версии анализируют дерево выражений C# в рантайме через ProjectionFieldExtractor. Дальше происходят принципиально разные вещи.

Free — SQL Projection Function
ProjectionFieldExtractor
Извлечение fieldPaths + structureIds
Фильтр → JSON → plpgsql-интерпретатор
search_objects_with_projection_by_paths
build_flat_projection() → частичный JSON
Десериализация JSON → частичный Props
compiledProjection(obj)TResult
Проекция читает только запрошенные строки _values через SQL-функцию. Фильтр проходит через JSON → plpgsql динамический SQL.
vs
Pro — Compiled SQL + Filtered _values
ProjectionFieldExtractor
Извлечение structureIds HashSet<long>
BuildQuerySqlAsync → скомпилированный WHERE + PVT CTE
SELECT из _objects → ID подходящих объектов
ProLazyPropsLoader: _values WHERE _id_structure = ANY($2)
Parallel.ForEach материализация без JSON
compiledProjection(obj)TResult
Без plpgsql. Без JSON-сериализации. Скомпилированный WHERE с кэшированным планом. _values фильтруются по structureIds → параллельная материализация.

Free: единая SQL-функция. search_objects_with_projection_by_paths делает всё в одном plpgsql-вызове — резолвит текстовые пути в structure_id через resolve_field_path(), фильтрует объекты, затем вызывает build_flat_projection() для построения минимального JSON по каждому объекту только с запрошенными полями. JSON десериализуется в частичный RedbObject<TProps> (непроецируемые поля остаются default), затем compiledProjection извлекает нужные значения в TResult. SkipPropsLoading = true гарантирует, что избыточная перезагрузка не перезапишет частичные Props.

Pro: двухфазный скомпилированный конвейер. Pro идёт совершенно другим путём. ProQueryProvider.ExecuteToListAsync никогда не вызывает search_objects_with_projection_by_paths. Вместо этого: BuildQuerySqlAsync компилирует .Where() в параметризованный SQL с PVT CTE (если фильтруются Props-поля), выполняет стандартный SELECT по _objects для получения ID подходящих объектов и базовых полей. Без JSON. Без plpgsql-интерпретатора.

Затем ProLazyPropsLoader.LoadPropsForManyAsync(objects, projectedStructureIds) загружает только проецируемые строки _values: SELECT * FROM _values WHERE _id_object = ANY($1) AND _id_structure = ANY($2). У объекта 30 полей — запрос читает 2. Результат проходит через конвейер параллельной материализации: ILookup-группировка, предзагрузка схем, Parallel.ForEach с ProPropsMaterializer. Без JSON-парсинга, без десериализации — плоские строки _values собираются напрямую в C#-свойства. Частичные Props не кэшируются (они неполные по замыслу).

Сравнение с ORM. Проекция EF Core (.Select(x => new { x.Name })) генерирует SELECT "Name" FROM "Employees" — проекция на уровне колонок плоской таблицы. Просто и быстро для плоских моделей. Но когда у сущности 28 связанных таблиц (вложенные объекты, коллекции, словари), EF Core требует цепочек .Include() или ручных DTO с JOINами — нет анализа дерева выражений для автоматического определения, какие связанные сущности реально нужны. RedBase делает это автоматически: одно выражение .Select(), и движок читает только подходящие строки _values.

Это не обёртка. Смотрите код.

Внутри нет DbContext. Нет зависимости от EF. Нет Dapper. Нет скрытого SqlCommand. Каждый слой, перечисленный ниже, написан с нуля и существует в репозитории:

  • Компилятор выраженийExpressionToSqlCompiler. Рекурсивный SqlExpressionVisitor обходит узлы Binary, Unary, MethodCall, Member, Conditional, Constant. Генерирует параметризованный SQL для каждого диалекта. Отдельные реализации для PostgreSQL и MS SQL.
  • Генератор Pivot-запросовPvtSqlGenerator. Превращает N полей свойств в один плоский CTE через GROUP BY + FILTER. Обрабатывает вложенные объекты, словари, массивы, ссылки ListItem. Генерирует array_agg / STRING_AGG для каждой БД.
  • Движок материализацииProLazyPropsLoader + ProPropsMaterializer: 2 bulk SELECT → ILookup-индексация → Parallel.ForEach. 20+ обработчиков типов. Рекурсивная вложенная загрузка. Нулевая сериализация.
  • Схема хранения — 6 таблиц, 3 уровня частичных уникальных индексов, FK-ограничения между всеми сущностями, глобальная последовательность идентификаторов. Не абстракция над чужими таблицами — таблицы и есть продукт.
  • Система кэшированияGlobalPropsCache с валидацией по SHA-256 хешу. GlobalMetadataCache для схем и структур. GlobalListCache с TTL. Изоляция по доменам. Потокобезопасность. Межзапросное хранение.
  • Change trackingValueTreeBuilder строит два дерева (память vs БД), ValueTreeComparer сравнивает их с пропуском поддеревьев по хешам. Только изменённые узлы генерируют SQL. Без delete-all/re-insert.

Dapper — это маппер. EF Core — это change tracker с транслятором запросов. RedBase — это движок данных — компилятор, планировщик запросов, материализатор, схема хранения, кэш, алгоритм diff — созданный для нагрузок со сложными вложенными сущностями, глубокими графами свойств и документоподобными структурами, для которых ORM никогда не были предназначены.

ExpressionToSqlCompiler — что компилируется

Внутренний SqlExpressionVisitor обходит деревья выражений C# и генерирует нативный SQL. Для каждого типа узла есть свой обработчик:

Бинарные и унарные

Арифметика: +, -, *, /, %. Сравнение: >, >=, <, <=, ==, !=. Логические: AND, OR, NOT. Null-объединение: x ?? defaultCOALESCE(x, default). Проверка на null: x == nullIS NULL. Convert: прямой проброс для приведения типов.

Math.*

Math.AbsABS(), Math.RoundROUND(), Math.FloorFLOOR(), Math.CeilingCEIL(), Math.MaxGREATEST(), Math.MinLEAST(). Поддержка нескольких аргументов (напр. Round(x, 2)).

String.*

ContainsLIKE '%' || @p || '%', StartsWithLIKE @p || '%', EndsWithLIKE '%' || @p, ToUpper/ToLowerUPPER()/LOWER(), TrimTRIM(). Всё параметризовано — нет SQL-инъекций.

Коллекции

dict["key"]pvt."Dict[key]" (ссылка на колонку PVT). dict.ContainsKey("k")pvt."Dict[k]" IS NOT NULL. list.Contains(val)val = ANY(pvt."List"). Пути вложенных свойств: p.Address.City → разрешение structure ID через SchemeFieldResolver.

ProSqlBuilder добавляет: компиляцию фильтров с рекурсивным обходом дерева, сортировку на основе выражений (арифметика, функции, пользовательский SQL в ORDER BY), генерацию pivot-подзапросов через PivotSqlGenerator, построение агрегатных колонок и типизированные SQL-приведения. SqlParameterCollector обеспечивает защиту от инъекций во всём конвейере.

API-поверхность IRedbQueryable<T>

Where(), WhereRedb(), WhereIn(), OrderBy() / OrderByDescending(), Take(), Skip(), Select<TResult>()IRedbProjectedQueryable<TResult>, Distinct(), DistinctBy(), ToListAsync(), CountAsync(), FirstOrDefaultAsync(), AnyAsync(), AllAsync(), WithMaxRecursionDepth(), WithLazyLoading().

Древовидные запросы добавляют: WhereHasAncestor, WhereHasDescendant, WhereLevel, WhereRoots, WhereLeaves. С ограничением: TreeQuery<T>(rootObjectId, maxDepth), мульти-корневые: TreeQuery<T>(rootIds).

Pro-движок

C# → SQL Конвейер компиляции

Pro не интерпретирует выражения — он их компилирует. ExpressionToSqlCompiler обходит дерево выражений C# узел за узлом и генерирует нативный параметризованный SQL. ProSqlBuilder обрабатывает деревья фильтров, арифметику, функции, сортировку, pivot-подзапросы. Реализации для каждой БД: PostgreSQL и MS SQL.

Бинарные операторы

Арифметика: +, -, *, /, %. Сравнение: ==, !=, >, >=, <, <=. Логические: &&, ||. Null-объединение: ??COALESCE(x, y). Конкатенация строк: +|| (PostgreSQL).

Математические функции

Math.AbsABS(), Math.RoundROUND(), Math.FloorFLOOR(), Math.CeilingCEIL(), Math.MaxGREATEST(), Math.MinLEAST(). Компилируются инлайн — без оверхеда UDF.

Строковые функции

.Contains()LIKE '%' || @p || '%', .StartsWith()LIKE @p || '%', .EndsWith()LIKE '%' || @p, .ToUpper()UPPER(), .ToLower()LOWER(), .Trim()TRIM(), .LengthLENGTH(). Регистронезависимые варианты через ILIKE.

DateTime и доступ к свойствам

.YearEXTRACT(YEAR FROM col), .Month, .Day, .Hour, .Minute, .Second. Вложенные свойства: x.Address.Citypvt."Address.City". Неограниченная глубина вложенности через цепочечный обход GetPropertyPath().

Операции с коллекциями

x.Tags.Contains("v")@p = ANY(pvt."Tags"). x.Tags.CountCOALESCE(array_length(pvt."Tags", 1), 0). Словарь: x.Dict["key"] → pivot-колонка. x.Dict.ContainsKey("k")IS NOT NULL. Доступ к элементу массива: x.Roles[].Value= ANY(col).

Продвинутые возможности

Sql.Function<T>("COALESCE", ...) — вставка произвольных SQL-функций. Захват замыканий: var age = 30; x => x.Age > age — вычисляется и параметризуется. Nullable.GetValueOrDefault() — проброс. Унарный !NOT, преобразования типов проходят напрямую.

30+ операторов сравнения в ProSqlBuilder

Помимо базовых =, <>, >, <: Contains, StartsWith, EndsWith (регистрозависимый LIKE), ContainsIgnoreCase, StartsWithIgnoreCase, EndsWithIgnoreCase (ILIKE). Операторы массивов: ArrayContains, ArrayAny, ArrayEmpty, ArrayCount, ArrayCountGt/Gte/Lt/Lte. Словарь: ContainsKeyIS NOT NULL. Всё параметризовано. Всё защищено от инъекций.

Генерация Pivot-запросов — PivotSqlGenerator

Превращает структурированное хранение свойств в плоские строки для запросов. Генерирует мульти-CTE SQL: GeneratePvtCte() для плоских полей, GenerateNestedDictCte() для вложенных ключей словарей, GenerateNestedOnlyPvtCte() для доступа только к вложенным элементам. Поля словарей вроде PhoneBook["home"] становятся реальными колонками в pivot. Поля массивов агрегируются через array_agg(). Отдельные реализации для Postgres и MSSQL.

Кэширование SQL-выражений

ExpressionSqlCacheConcurrentDictionary по хешу строки выражения. Скомпилированный SQL из C#-выражений кэшируется, так что повторные запросы пропускают полный обход. Кэш потокобезопасный и разделяется на время жизни IRedbService.

Компиляция крайних случаев

Nullable-навигация: r.Auction != null && r.Auction.Costs > 100 → компилируется в IS NOT NULL + доступ к вложенному свойству. Без NullReferenceException на уровне SQL.

Тернарный оператор в OrderBy: r.Auction != null ? r.Auction.Baskets : 0CASE WHEN pvt."Auction" IS NOT NULL THEN pvt."Auction.Baskets" ELSE 0 END. Полная условная сортировка без постобработки.

StringComparison: .Contains(val, StringComparison.OrdinalIgnoreCase)ILIKE. Регистронезависимый поиск компилируется нативно, без обёртки в LOWER().

ListItem в запросах: x.Status.Value == "Active" → join к _list_items + фильтр по _name. WhereIn для нескольких значений ListItem. array.Any(s => s == x.Status) — ID ListItem компилируются как = ANY(@p).

WhereInRedb: .WhereInRedb(x => x.Name, names) → фильтрует по базовым колонкам _objects (_id, _name, _note) через IN. Комбинируется с .Where() по Props-полям в одном запросе.

Аналитика

Агрегация, GroupBy, оконные функции

Вся аналитика выполняется как SQL на стороне базы данных — не в памяти C#. Агрегация, группировка и оконные функции исполняются внутри выделенных хранимых процедур, возвращая только финальный результат.

Агрегация

SumAsync, AverageAsync, MinAsync, MaxAsync, CountAsync — одноколоночные агрегаты. GetStatisticsAsync — все пять за один вызов (параллельный SQL).

AggregateAsync для нескольких полей:

await query.AggregateAsync(x => new {
    Total = Agg.Sum(x.Props.Amount),
    Avg   = Agg.Avg(x.Props.Price),
    Count = Agg.Count()
});

SQL: aggregate_field(), aggregate_batch(). Агрегация массивов: Agg.Sum(x.Props.Items.Select(i => i.Price)).

GroupBy

GroupBy(x => x.Category)IRedbGroupedQueryableSelectAsync. Также GroupByRedb для базовых полей, GroupByArray для элементов массивов.

await query
    .GroupBy(x => x.Category)
    .SelectAsync(g => new {
        Category = g.Key,
        Total = Agg.Sum(g, x => x.Stock),
        Count = Agg.Count(g)
    });

SQL: aggregate_grouped(), aggregate_array_grouped(). GroupBy + WithWindow для комбинированной аналитики.

Оконные функции

WithWindow(w => w.PartitionBy(...).OrderBy(...))IRedbWindowedQueryableSelectAsync.

await query
    .WithWindow(w => w
        .PartitionBy(x => x.Category)
        .OrderByDesc(x => x.Stock))
    .SelectAsync(x => new {
        x.Props.Name,
        Rank = Win.RowNumber()
    });

Ранжирование: RowNumber, Rank, DenseRank, Ntile. Смещение: Lag, Lead, FirstValue, LastValue. Агрегаты: Sum, Avg, Count, Min, Max OVER (...). Рамка: ROWS/RANGE BETWEEN с FrameSpec. SQL: query_with_window().

Аналитика в контексте дерева: TreeQuery().GroupBy() — агрегация в рамках поддерева (не глобальная). TreeQuery().WithWindow() — оконные функции внутри иерархии дерева: RowNumber(), RunningTotal(), ранжирование — всё вычисляется в пределах выбранного поддерева. CreateBatchChildAsync — фабрика для пакетного создания дочерних объектов.

Предпросмотр SQL — ToSqlString()

Аналог EF Core ToQueryString(). Вызовите ToSqlStringAsync() или ToFilterJsonAsync() для любого запроса — плоского, древовидного, сгруппированного или оконного — чтобы увидеть точный SQL, который будет выполнен. Предпросмотр GroupBy через aggregate_grouped_preview() и aggregate_batch_preview(). Предпросмотр поиска через get_search_sql_preview() / get_search_tree_sql_preview().

Производительность

Трёхуровневое кэширование

Не дополнение «по остаточному принципу». Кэширование встроено в ядро с изоляцией по доменам, валидацией по хешам и управлением TTL.

GlobalMetadataCache

Кэширование схем, типов и CLR-типов. Изоляция по доменам через ConcurrentDictionary. Внутренние карты поиска: SchemeByName, SchemeById, TypeById, SchemeNameToClrType, ClrTypeToSchemeId.

GlobalPropsCache

Полное кэширование RedbObject с валидацией по хешу. Get<T>(objectId, hash) — возвращает кэшированный объект только при совпадении хеша с БД. Бэкенд MemoryRedbObjectCache: TTL, максимальный размер, квоты на пользователя.

GlobalListCache

Списки и элементы списков. На основе TTL (по умолчанию 5 мин). Кэширует: ListsById, ListsByName, ItemsByListId, ItemsById.

Кэш метаданных на уровне БД

Помимо C#-кэшей, RedBase поддерживает таблицу _scheme_metadata_cache непосредственно в базе данных. Автоматические триггеры поддерживают её в актуальном состоянии:

  • sync_metadata_cache_on_hash_change() — триггер на _schemes: пересчитывает кэш при изменении хеша схемы
  • cleanup_metadata_cache_on_scheme_delete() — удаляет записи кэша при удалении схемы
  • check_metadata_cache_consistency() — проверяет целостность кэша, выявляет устаревшие/отсутствующие/осиротевшие записи
  • warmup_all_metadata_caches() — вызывается в InitializeAsync, предзагружает все метаданные схем за один проход

Это значит, что метаданные остаются консистентными даже если несколько экземпляров приложения используют одну БД, или если изменения схемы происходят вне приложения (напр., прямая SQL-миграция).

Ленивая загрузка — двухфазная оптимизация

WithLazyLoading(true) или глобальная EnableLazyLoadingForProps. Фаза 1: загружаются только базовые поля из _objects (быстро, легковесно). Фаза 2: при первом обращении к .Props загружаются все значения пакетно через LoadPropsForManyAsync для всего набора результатов. Нет проблемы N+1 — один пакетный запрос вместо одного на каждый объект.

SQL: get_object_base_fields(), execute_objects_query_base(), get_filtered_object_ids(). Fallback на eager-загрузку через get_object_json() для единичных объектов.

Портируемость

Одна кодовая база, несколько БД

PostgreSQL и MS SQL Server. Те же модели, те же запросы, те же деревья. Абстракция провайдеров обрабатывает различия диалектов. А ещё можно использовать несколько баз данных в одном приложении одновременно.

ISqlDialect

Каждая БД реализует свой SQL-диалект: синтаксис параметров, маппинг типов, имена функций. Pro расширяет это через ISqlDialectPro для SQL миграций и материализации. Ваш код никогда не вызывает методы диалекта напрямую.

Экспорт и импорт

ExportService записывает FK-безопасный JSONL: Types → Lists → Schemes → Structures → Roles → Users → Objects → Values → Permissions. ImportService читает построчно, массовая вставка через IDataProvider. Сжатый .redb (ZIP) или простой JSONL.

Пакетная генерация ID

IRedbContext предоставляет NextObjectIdBatchAsync(count) и NextValueIdBatchAsync(count) для высокопроизводительных вставок. Каждая БД реализует свою стратегию последовательностей.

Мульти-БД — изоляция доменов

Подключайте две и более баз данных в одном процессе — каждая получает свой экземпляр IRedbService с полностью изолированными кэшами, схемами и провайдерами. Никакого перекрёстного загрязнения.

// Основная БД — PostgreSQL
services.AddRedbPro(o => o.UsePostgres(mainConnection));

// Вторая БД — SQL Server (отдельный DI-scope)
var docServices = new ServiceCollection();
docServices.AddRedbPro(o => o.UseMsSql(docConnection));
var redbDoc = docServices.BuildServiceProvider()
    .GetRequiredService<IRedbService>();
await redbDoc.InitializeAsync();

Каждый IRedbService вычисляет CacheDomain (из строки подключения или явной конфигурации). Все три уровня кэша — GlobalMetadataCache, GlobalPropsCache, GlobalListCache — партиционированы по домену через static ConcurrentDictionary<string, DomainCache>. Схемы из базы A никогда не появляются в запросах к базе B.

Комбинируйте Postgres + MSSQL в одном приложении, запускайте Postgres как основную БД с MSSQL-сайдкаром для аналитики, или разделяйте базы для чтения и записи — всё через один и тот же API IRedbService.

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

Пользователи, роли, разрешения — встроенные

Не прикручено «сбоку». Системные таблицы _users, _roles, _permissions создаются в InitializeAsync. Работает без ASP.NET Identity — в консольных приложениях, Blazor, фоновых сервисах.

IUserProvider

CreateUserAsync(CreateUserRequest), ValidateUserAsync(login, password), ChangePasswordAsync(user, currentPwd, newPwd), GetUsersAsync(UserSearchCriteria?), DeleteUserAsync (мягкое удаление, системные пользователи 0/1 защищены).

IRoleProvider

CreateRoleAsync, AssignUserToRoleAsync(user, role), RemoveUserFromRoleAsync. Удаление роли каскадно удаляет разрешения.

IPermissionProvider

CanUserEditObject, CanUserSelectObject, CanUserInsertScheme, CanUserDeleteObject. Читается из IRedbSecurityContext (ambient per-request user scope).

Хеширование паролей

IPasswordHasher / SimplePasswordHasher — SHA256 + per-user salt. Заменяемый: подключите bcrypt/scrypt через DI.

Иерархическое наследование разрешений

Разрешения распространяются по дереву объектов через рекурсивный CTE (до 50 уровней). Система находит ближайшего предка с явными разрешениями и применяет их. Глобальные разрешения (_id_ref = 0) служат fallback. Приоритет: пользователь > роль.

get_user_permissions_for_object() — рекурсивный поиск вверх по дереву, останавливается на первом совпадении. Системный пользователь (id=0) получает полный доступ без рекурсии. Триггер auto_create_node_permissions() — при вставке дочернего объекта автоматически создаёт разрешения на родителе, если они отсутствуют, уменьшая глубину рекурсии при последующих проверках.

Триггеры валидации на уровне БД

Целостность данных обеспечивается на уровне SQL — даже прямой доступ к БД не может повредить схему:

  • validate_structure_name() — отклоняет имена полей, нарушающие правила именования C#, начинающиеся с цифр, использующие зарезервированные слова или конфликтующие с системными колонками. Максимум 64 символа.
  • validate_scheme_name() — валидирует имена схем с поддержкой пространств имён (MyApp.Orders), вложенных классов (Order+Item), отклоняет пустые части, последовательные точки. Системные схемы (@__deleted) обходят валидацию.
  • protect_system_users() — предотвращает удаление или переименование системных пользователей (id=0 sys, id=1 admin). Изменения пароля/email/статуса разрешены.
Продакшн

Создано для продакшна

Функции, которые компании строят месяцами — включены из коробки.

Валидация схемы

IValidationProvider проверяет C#-модели на соответствие базе данных при запуске. ValidateSchemaAsync<T>(), AnalyzeSchemaChangesAsync<T>(), ValidatePropertyConstraints. Обнаруживает несоответствия типов, отсутствующие структуры и дрейф схемы до того, как они попадут в продакшн.

Мягкое удаление + Фоновая очистка

SoftDeleteAsync атомарно перемещает объекты (и всех потомков через рекурсивный CTE) в контейнер-корзину со схемой @__deleted. Опциональный параметр trashParentId позволяет разместить корзину под любым родителем (напр., личная корзина пользователя) — или null для корневого уровня. Удалённые объекты мгновенно исчезают из всех запросов и списков — дополнительные фильтры не нужны. Контейнер-корзина хранит прогресс: total, deleted count, status. BackgroundDeletionService забирает задачу через Channel и удаляет данные пакетами (настраиваемый batchSize). ON DELETE CASCADE автоматически обрабатывает связанные значения. При перезапуске RecoverOrphanedTasksAsync находит прерванные задачи и возобновляет с места остановки. Кластер-безопасно: TryClaimOrphanedTaskAsync гарантирует, что только один экземпляр захватит каждую задачу.

Отслеживание изменений — Diff Tree Save (Pro)

EavSaveStrategy.ChangeTracking. Вместо DeleteInsert (удалить всё + вставить заново), Pro строит два дерева ValueTreeNode — одно из памяти, другое из БД — и выполняет структурный diff для генерации только минимального набора SQL-операций.

Конвейер: ValueTreeBuilder.BuildTreeFromMemory() сериализует C#-объект → плоский список RedbValue → дерево. BuildTreeFromDB() загружает существующие значения → та же структура дерева. BuildTreeFromFlat() восстанавливает иерархию parent-child через _array_parent_id, строит StructureMap для O(1) поиска, назначает Hash для каждого узла.

Алгоритм diff (ValueTreeDiff.CompareTreesWithHash): группирует узлы по structure_id, затем для каждой группы: CompareNodeGroup определяет массив vs скаляр. CompareNodes — если оба узла имеют Hash и хеши совпадают → пропуск всего поддерева (без сравнения значений, без обхода потомков). CompareArrayElements — сопоставление элементов массива по индексу, обнаружение вставок, удалений и обновлений по элементам. Результат: List<TreeChange> с типами Insert, Delete, Update, Skip. Только изменённые узлы порождают SQL — неизменённые поддеревья бесплатны.

Миграции данных (Pro)

Fluent API (аналог EF Core IEntityTypeConfiguration): .Property(p => p.Field).ComputedFrom(p => p.A * p.B), .DefaultValue("value"), .Transform(v => v.Replace("-", "")), .OnTypeChange<string, decimal>().Using(v => decimal.Parse(v)), .When(p => condition). MigrationDiscovery автоматически находит реализации. MigrationExpressionCompiler компилирует C# в SQL UPDATE. История хранится в БД, идемпотентно (детекция изменений по хешу).

Параллельный конвейер материализации (Pro)

ProLazyPropsLoader.LoadPropsForManyAsync — 7-этапный конвейер с Parallel.ForEach:

  1. Пакетная проверка кэшаFilterNeedToLoad<T>() по хешу: пропуск объектов, уже имеющихся в GlobalPropsCache
  2. Один пакетный SELECT — все _values для всех ID объектов одним запросом
  3. Пакетная предзагрузка ListItems — один запрос для всех связанных _list_items
  4. Предзагрузка схем + типов — загрузка структур схем и реестра типов один раз, до параллелизации
  5. Parallel.ForEach материализацияProPropsMaterializer.GetObjectProps<T>() работает в пуле потоков; внутри параллельного цикла нет обращений к БД
  6. Рекурсивная загрузка вложенных объектовMaterializeNestedObjectsDynamically() разрешает ссылки на вложенные RedbObject с защитой от циклов
  7. ПодстановкаSubstituteNestedObjects() заменяет ID-заглушки загруженными объектами в Props родителя

ProPropsMaterializer обрабатывает иерархическую сборку свойств: BuildHierarchicalPropertiesOptimized<T>, BuildArrayField, BuildDictionaryField, ConvertValue (30+ маппингов типов). Потокобезопасно: всё состояние передаётся параметрами, нет разделяемых мутабельных данных. Защита от бесконечной рекурсии через ConcurrentDictionary<long, byte> _loadingInProgress.

Интроспекция дерева структур

Рефлексия схемы на уровне SQL: get_scheme_structure_tree() возвращает полную иерархию полей схемы. get_structure_children(), get_structure_descendants() — навигация по деревьям полей. validate_structure_tree() — проверка структурной целостности (осиротевшие поля, циклические ссылки, несоответствия типов). Используется для синхронизации схем, планирования миграций и диагностики.

Миграция типов

migrate_structure_type(structureId, oldType, newType, dryRun) — изменение типа поля на месте. get_value_column(typeName) определяет целевую колонку. Поддержка dry-run для анализа последствий. Доступно через ISqlDialect.Schemes_SyncMetadataCache().

Массовые операции — протокол COPY

IBulkOperations — 8 методов для высокопроизводительных операций с данными. BulkInsertObjectsAsync / BulkInsertValuesAsync — используют бинарный протокол PostgreSQL COPY (или MSSQL SqlBulkCopy) для максимальной скорости вставки. BulkUpdateObjectsAsync / BulkUpdateValuesAsync — паттерн temp table + UPDATE FROM: COPY во временную таблицу, затем один UPDATE JOIN. BulkDeleteObjectsAsync / BulkDeleteValuesAsync / BulkDeleteValuesByObjectIdsAsync / BulkDeleteValuesByListItemIdsAsync. Всё соблюдает FK-ограничения и каскады. Используется внутренне ImportService и AddNewObjectsAsync.

Полиморфная пакетная загрузка и цепочка родителей

LoadAsync(IEnumerable<long> objectIds) — полиморфная пакетная загрузка. Загружает объекты разных схем одним вызовом, разрешает CLR-типы в runtime через SetTypeResolver. LoadWithParentsAsync<T>() — 8 перегрузок. Загружает объект с полной цепочкой родителей до корня. Возвращает TreeRedbObject<T> с заполненным свойством Parent. Пакетные варианты разделяют общие ссылки на родителей (дедупликация). Полиморфные варианты через ITreeRedbObject для деревьев со смешанными схемами.

Полиморфные операции с деревьями

LoadPolymorphicTreeAsync, GetPolymorphicChildrenAsync, GetPolymorphicPathToRootAsync, GetPolymorphicDescendantsAsync — операции с деревьями, где дочерние узлы могут иметь разные схемы. InitializeTypeRegistryAsync() маппит ID схем на CLR-типы при старте. Возвращает ITreeRedbObject с runtime-разрешением типов.

Пресеты конфигурации

PredefinedConfigurations — 8 именованных профилей: Default, Development, Production, BulkOperations, HighPerformance, Debug, IntegrationTesting, DataMigration. Каждый профиль настраивает 35+ параметров: размеры кэшей, TTL, ленивая загрузка, валидация, вычисление хешей. IsProductionSafe(), IsPerformanceOptimized() — runtime-интроспекция. GetByName() для выбора профиля из конфигурации.

Пакетное сохранение и смешанные операции

SaveAsync(IEnumerable<IRedbObject>) — сохранение микса новых и существующих объектов одним вызовом. Автоматически определяет, какие объекты требуют INSERT, а какие UPDATE. Работает как с EavSaveStrategy.DeleteInsert, так и с ChangeTracking. AddNewObjectsAsync — оптимизированная пакетная вставка только для новых объектов (использует протокол COPY внутри). Производительность: пакетное сохранение 100 объектов ~10x быстрее последовательного SaveAsync для каждого объекта.

Производительность

40+ оптимизированных индексов

Каждый индекс обоснован через EXPLAIN ANALYZE на реальных запросах. Избыточные индексы явно удалены с документированным обоснованием.

Покрывающие индексы

INCLUDE содержит все типы значений для Index Only Scan — обращение к таблице не требуется. IX__values__object_structure_lookup включает каждую типизированную колонку. Замерено: снижение стоимости 30%+ на фасетных запросах.

Частичные индексы

WHERE _array_index IS NULL уменьшает размер индекса на 30-40%. WHERE _String IS NOT NULL для целевых NOT NULL запросов. WHERE _id_parent IS NULL для быстрого поиска корневых объектов.

GIN-поиск по триграммам

Расширение pg_trgm для поиска по паттернам LIKE/ILIKE без полного сканирования таблицы. Обеспечивает операторы $contains, $startsWith, $endsWith, $matches (regex).

Внутреннее устройство

Сериализация и разрешение типов

IRedbObjectSerializer обрабатывает конвертацию объект ↔ JSON. Реализация по умолчанию использует System.Text.Json.

SystemTextJsonRedbSerializer

Deserialize<TProps>(string json) — типизированная десериализация.
DeserializeDynamic(string json, Type propsType) — разрешение типов в runtime.
SetTypeResolver(Func<long, Type?>) — маппинг ID схем → CLR-типы для полиморфной загрузки.

Schema Field Resolution

SchemeFieldResolver (Pro) разрешает пути C#-свойств в ID структур. Кэшируется по схеме. Используется ProSqlBuilder и ExpressionToSqlCompiler для маппинга p.Address.City в правильный structure ID в базе данных.

Документация

195+ рабочих примеров

Каждый пример работает с реальной базой данных. Каждый помечен [ExampleMeta]: id, title, category, tier (ExampleTier.Free / Pro / Enterprise), difficulty (1-5), tags, related APIs.

174+ Бесплатных примеров
21 Pro примеров
5 Уровней сложности
100% Реальная БД

Бесплатные примеры охватывают

Bulk Insert, CRUD, Schema Sync, Nested Objects, Trees, LINQ Queries, GroupBy, Window Functions, Aggregation, Security (Users, Roles, Permissions), Export/Import, Soft Delete, Lists, Validation.

Pro примеры охватывают

Deep Nesting (E041), Distinct Queries (E139-E140), Arithmetic/String/Math/DateTime Expressions (E151-E159), Tree Expressions (E160), Sql.Function (E161-E163), GroupBy+Window combined (E175, E195), TreeDistinct (E176-E177, E180).

В цифрах

0 Миграций. Никогда.
60+ SQL-функций и триггеров
40+ Оптимизированных индексов
50+ Встроенных типов данных
195+ Рабочих примеров
8 NuGet-пакетов
10+ Интерфейсов провайдеров
2 Бэкенда БД
.NET 8+ net8.0, net9.0, net10.0
Мы используем свой продукт. Этот сайт документации работает на ASP.NET Blazor с RedBase в качестве слоя данных. Каждая страница, пример и справка по API, которые вы видите, хранятся и отдаются как REDB-объект.

Продакшн-класс. Проверено боем. Настоящий движок данных.

Компилятор выражений, PVT-планировщик запросов, параллельный материализатор, движок структурного diff, трёхуровневый кэш — всё написано с нуля. Бесплатная MIT-редакция покрывает CRUD, запросы, деревья, безопасность, экспорт. Pro добавляет скорость и продвинутые выражения.

dotnet new install redb.Templates && dotnet new redb -n MyApp