Под капотом

Не 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/SQLite переопределяет их для внедрения поведения, специфичного для БД. Ваш бизнес-код никогда не видит разницы.

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

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 версия — compiled-путь v2-pvt
C# Lambda
BaseFilterExpressionParser
FacetFilterBuilder
JSON Facets мультиязычный wire-контракт
pvt_build_query_sql() — нативный параметризованный SQL
Результаты
С v3.0.0 та же PVT CTE форма, что и в Pro. JSON-фасеты описывают запрос; SQL-сторонний билдер собирает MAX(CASE WHEN _id_structure = X AND _array_index IS NULL THEN ...) и одиночный LEFT JOIN _list_items, параметризовано end-to-end. pvt_split_filter опускает предикаты по base-полям во внутренний подзапрос _objects — индекс срабатывает до пивота. 25+ операторов фильтрации, $expr, $arrayContains/$arrayAny/$arrayCount*, GroupBy + HAVING, ArrayGroupBy, DistinctBy, Sql.Function (whitelist). 145/145 паритет шейпов между PG Free, MSSql Free и SQLite Free.
Используемые SQL-функции: pvt_build_query_sql, pvt_split_filter, pvt_build_where_from_json, pvt_collect_fields, pvt_b2_expr_sql, get_object_json, warmup_all_metadata_caches. Старая функция search_objects_with_facets() осталась в схеме для обратной совместимости, но с 3.0.0 не является путём по умолчанию.
Pro версия
C# Lambda
ProFilterExpressionParser без ограничений
ExpressionToSqlCompiler реализация для каждой БД
Нативный параметризованный SQL
ProSqlBuilder + SqlParameterCollector
Результаты
Прямая компиляция в C#. Дерево выражений обходит SqlExpressionVisitor (отдельный компилятор для PostgreSQL, MSSql и SQLite), SQL эмитится с параметрами через SqlParameterCollector. Без JSON-этапа.
Что есть только в Pro поверх общей формы: bulk-параллельная материализация (ProLazyPropsLoader + ProPropsMaterializer), COPY-протокол BulkInsert при сохранении, hash-based ChangeTracking (diff только изменившегося), Distinct/DistinctBy с fallback-шейпом, совмещённый GroupBy + window, cross-aggregate AggregateBatch, вычисляемые выражения в ORDER BY, PivotSqlGenerator для сложных pivot-отчётов. Новое в 3.0.0: GroupBy + HAVING в Pro (PG + MSSql), единая реализация ArrayGroupBy на всех четырёх тирах, MSSql.Pro AggregateBatch паритет с PG.Pro (non-numeric MIN/MAX, inline-filter subquery).
Тот же .Where(p => p.Name == "test"). Тот же IRedbQueryable<T>. С v3.0.0 одна и та же PVT CTE форма — оба тира компилируются в нативный параметризованный SQL, который БД может кэшировать. Преимущество Pro теперь не в форме запроса; это параллельная CPU-bound материализация, COPY bulk-save, hash-diff change tracking и более тяжёлые query-сценарии (cross-aggregate batches, combined grouped-window).

Форма PVT CTE — теперь общая для обоих тиров

Фильтрация по props-полям разворачивает N строк _values в N плоских колонок одним проходом агрегации. Внешний WHERE работает уже по этим колонкам. С v3.0.0 эту форму эмитят оба тира — и на PostgreSQL, и на MSSql.

PostgreSQL (Free + Pro)
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)
SELECT o.* FROM _objects o JOIN pvt_cte ON o._id = pvt._id_object
WHERE pvt."Age" > $4 AND pvt."City" = $5
Один index scan по (_id_structure, _id_object), один GROUP BY. Параметризация end-to-end.
=
MSSql (Free + Pro)
WITH pvt_cte AS (SELECT v._id_object,
MAX(CASE WHEN _id_structure = @p1 AND _array_index IS NULL THEN _Long END) AS [Age],
MAX(CASE WHEN _id_structure = @p2 AND _array_index IS NULL THEN _String END) AS [City]
FROM _values v JOIN (SELECT _id FROM _objects WHERE _id_scheme = @p3) o ON v._id_object = o._id
GROUP BY v._id_object)
SELECT o.* FROM _objects o JOIN pvt_cte ON o._id = pvt._id_object
WHERE pvt.[Age] > @p4 AND pvt.[City] = @p5
Та же форма, диалект MSSql. MAX(CASE WHEN) — SQL-Server эквивалент array_agg FILTER.

Как оба тира приходят к одному SQL. Pro компилирует дерево выражений C# напрямую в процессе через ExpressionToSqlCompiler + SqlExpressionVisitor, использует SqlParameterCollector для bind-переменных. Free сериализует распаршенный фильтр в стабильный JSON-фасетный контракт и вызывает pvt_build_query_sql() — SQL-сторонний билдер, который эмитит ту же параметризованную PVT CTE. Оба прогоняются через pvt_split_filter, который перемещает предикаты по base-полям (_id_scheme, _id_parent, _name, произвольные булевы $expr-деревья над base-полями) во внутренний подзапрос (SELECT _id FROM _objects WHERE ...) — индекс срабатывает до пивота.

Историческая справка. До v3.0.0 Free-путь был другим: plpgsql-сторонний search_objects_with_facets() на PostgreSQL и wide-CASE inline-пивот на MSSql, оба с per-field коррелированным EXISTS по _values. Эти функции по-прежнему есть в схеме для обратной совместимости, но новые query-провайдеры по умолчанию идут через pvt_build_query_sql. Итог: 145/145 паритет шейпов между PG Free, MSSql Free и SQLite Free, и шейп-паритет между Free и Pro на каждой БД.

LINQ → SQL — что вы пишете и что улетает в БД

Четыре характерных шейпа из набора интеграционных тестов. В каждой паре — C# слева и SQL, который эмитит v2-pvt билдер, справа (синтаксис PostgreSQL; MSSql даёт структурно идентичный запрос с MAX(CASE WHEN) и [квадратными скобками]).

E040 — вложенный POCO, same-scheme compound path
// C#
var r = await q.Where(o => o.HomeAddress.City == "London")
               .ToListAsync();

// SQL (Free + Pro)
WITH pvt_cte AS (
  SELECT v._id_object,
    (array_agg(v._String) FILTER
      (WHERE v._id_structure = $1 AND v._array_index IS NULL))[1] AS "HomeAddress.City"
  FROM _values v
  JOIN (SELECT _id FROM _objects WHERE _id_scheme = $2) o
    ON v._id_object = o._id
  WHERE v._id_structure = ANY($3)
  GROUP BY v._id_object
)
SELECT o.* FROM _objects o
JOIN pvt_cte pvt ON o._id = pvt._id_object
WHERE pvt."HomeAddress.City" = $4;
E062 — nested-dict accessor с pushdown в outer WHERE
// C#
var r = await q.Where(o => o.OfficeLocations["HQ"].City == "New York")
               .ToListAsync();

// SQL (Free + Pro, v0.6.1+ / v0.1.3+)
WITH pvt_cte AS (
  SELECT v._id_object,
    (array_agg(v._String) FILTER
      (WHERE v._id_structure = $1 AND v._array_index IS NULL))[1]
        AS "OfficeLocations[HQ].City"
  FROM _values v
  JOIN (SELECT _id FROM _objects WHERE _id_scheme = $2) o
    ON v._id_object = o._id
  WHERE v._id_structure = ANY($3)
  GROUP BY v._id_object
)
SELECT o.* FROM _objects o
JOIN pvt_cte pvt ON o._id = pvt._id_object
WHERE pvt."OfficeLocations[HQ].City" = $4;

До этого цикла outer WHERE запускал отдельный EXISTS по _values для dict-ключа; теперь ссылается прямо на pivot-колонку, которую CTE уже собрал.

E130 — GroupBy по ListItem.Value
// C#
var stats = await q.GroupBy(o => o.Status.Value)
                   .Select(g => new { Key = g.Key, N = g.Count() })
                   .ToListAsync();

// SQL (одиночный LEFT JOIN на _list_items, без коррелированного подзапроса на строку)
WITH pvt_cte AS (
  SELECT v._id_object,
    (array_agg(v._listitem) FILTER
      (WHERE v._id_structure = $1 AND v._array_index IS NULL))[1] AS "Status"
  FROM _values v
  JOIN (SELECT _id FROM _objects WHERE _id_scheme = $2) o
    ON v._id_object = o._id
  WHERE v._id_structure = ANY($3)
  GROUP BY v._id_object
)
SELECT li._value AS "Key", COUNT(*) AS "N"
FROM _objects o
JOIN pvt_cte pvt ON o._id = pvt._id_object
LEFT JOIN _list_items li ON li._id = pvt."Status"
GROUP BY li._value;
E157 — pushdown base-предиката + props-предикат
// C#
var r = await q.WhereRedb(o => o._date_create.Year == 2026)
               .Where(p => p.Age > 30)
               .ToListAsync();

// SQL — base-предикат опущен ВО внутренний _objects-подзапрос,
//        props-предикат остаётся на outer pvt
WITH pvt_cte AS (
  SELECT v._id_object,
    (array_agg(v._Long) FILTER
      (WHERE v._id_structure = $1 AND v._array_index IS NULL))[1] AS "Age"
  FROM _values v
  JOIN (SELECT _id FROM _objects
        WHERE _id_scheme = $2
          AND EXTRACT(YEAR FROM _date_create) = $3) o
    ON v._id_object = o._id
  WHERE v._id_structure = ANY($4)
  GROUP BY v._id_object
)
SELECT o.* FROM _objects o
JOIN pvt_cte pvt ON o._id = pvt._id_object
WHERE pvt."Age" > $5;

Без pushdown base-фильтр работал бы в outer WHERE, уже после того как PVT проагрегировал всю схему. С pvt_split_filter base-предикат отсекает строки до пивота — разница между 894 мс и 11 мс в регрессии, ради которой выходил v2.0.1.

Free как полиглот-API для фасетов — используйте RedBase из Python, Node, Java, Go

Формат JSON-фасетов — стабильный wire-контракт. Соберите фильтр в любом языке, передайте SQL-сторонннему билдеру и получите полностью материализованные объекты (включая вложенные объекты, массивы и словари) в виде JSON одним round-trip. C#-клиент не требуется.

Python (psycopg / asyncpg)
import psycopg, json
facets = {"$and": [{"Age": {"$gt": 30}}, {"City": {"$eq": "London"}}]}
with psycopg.connect(DSN) as conn, conn.cursor() as cur:
    cur.execute(
        "SELECT get_objects_json(%s, %s::jsonb, 100, 0)",
        (scheme_id, json.dumps(facets)),
    )
    objects = cur.fetchone()[0]  # JSON-массив, полностью гидратированный
Node.js (pg)
import { Client } from 'pg';
const facets = { $and: [{ Age: { $gt: 30 } }, { City: { $eq: 'London' } }] };
const { rows } = await client.query(
  'SELECT get_objects_json($1, $2::jsonb, 100, 0)',
  [schemeId, JSON.stringify(facets)],
);
const objects = rows[0].get_objects_json; // гидратированный JSON-массив
Java (JDBC)
String facets = "{\"$and\":[{\"Age\":{\"$gt\":30}},{\"City\":{\"$eq\":\"London\"}}]}";
try (PreparedStatement ps = conn.prepareStatement(
        "SELECT get_objects_json(?, ?::jsonb, ?, ?)")) {
    ps.setLong(1, schemeId);
    ps.setString(2, facets);
    ps.setInt(3, 100);
    ps.setInt(4, 0);
    try (ResultSet rs = ps.executeQuery()) {
        rs.next();
        String hydratedJsonArray = rs.getString(1);
    }
}
Go (pgx)
facets := []byte(`{"$and":[{"Age":{"$gt":30}},{"City":{"$eq":"London"}}]}`)
var hydrated json.RawMessage
err := conn.QueryRow(ctx,
    "SELECT get_objects_json($1, $2::jsonb, 100, 0)",
    schemeID, facets,
).Scan(&hydrated)

Схема фасетов задокументирована и стабильна. get_objects_json внутри вызывает pvt_build_query_sql, чтобы разрешить запрос (тот же параметризованный SQL, что получают Free C#-клиенты), затем собирает результат через get_object_json на каждый найденный объект. Вложенные объекты, словари с tuple-ключами, массивы и ссылки RedbListItem корректно восстанавливаются в JSON — знания схемы на клиенте не нужно.

Это by design только во Free-тире: преимущество Pro — C#-сторонний compiled-пайплайн и параллельная материализация. JSON-фасетный путь даёт тот же query-движок из любого языка, ценой in-DB JSON-сборки вместо bulk-параллельной CLR-материализации.

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

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(), WithLazyReferences(), WithPropsDepth().

Древовидные запросы добавляют: 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.

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

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-миграция).

Ленивые ссылки

Ссылка на другой объект за границей depth приходит заглушкой: id, scheme_id, hash и IsPropsLoaded == false. Первое обращение к .Props загружает ровно этот объект (глубина 1), и его собственные ссылки снова заглушки: ленивость транзитивна и не зависит от исходной глубины. Свойство, помеченное virtual, при EnableLazyReferences остаётся заглушкой на любой глубине; WithLazyReferences(bool) переключает это для одного запроса, а LoadReferencesAsync(parent, p => p.Children) дозагружает коллекцию заглушек одним пакетом.

Хеш родителя берёт id:hash ссылки, а не обходит её Props; сериализация пишет заглушку как есть. Ни хеширование, ни JSON не будят граф. Обращение к .Props заглушки синхронно (поток ждёт запрос, сама загрузка идёт на пуле потоков, так что UI-хост не дедлочит); LazyReferenceAccess = Throw запрещает скрытый запрос и требует LoadReferencesAsync / LoadPropsAsync (Blazor WebAssembly, UI-приложения).

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

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

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)

PropsSaveStrategy.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. Работает как с PropsSaveStrategy.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, трёхуровневый кэш — всё написано с нуля. Бесплатная Apache 2.0-редакция покрывает CRUD, запросы, деревья, безопасность, экспорт. Pro добавляет скорость и продвинутые выражения.

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