Под капотом
Не ORM. Не обёртка. Платформа данных с собственным компилятором выражений, протоколом массовой загрузки и движком структурного сравнения. На этой странице описано, как всё это работает, с реальными именами классов и архитектурой SQL.
Компилятор выражений
ExpressionToSqlCompiler обходит деревья выражений C# и генерирует нативный параметризованный SQL.
Тернарный оператор → CASE WHEN, nullable-навигация → IS NOT NULL,
StringComparison → ILIKE.
Покрывает крайние случаи, которые даже 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, которые переопределяют абстрактные фабричные методы.
SyncSchemeAsync<T>(), автообнаружение через атрибут [RedbScheme].CreateAsync, LoadAsync, SaveAsync, DeleteAsync. Стратегии: DeleteInsert, ChangeTracking.LoadTree, GetChildren, MoveObject, GetPathToRoot. Полиморфные (мульти-схемные) деревья.Query<T>() → IRedbQueryable<T>. А также TreeQuery<T>() для запросов в контексте дерева.CreateUserAsync, ValidateUserAsync, ChangePasswordAsync, GetUsersAsync.CreateRoleAsync, AssignUserToRoleAsync, RemoveUserFromRoleAsync. Каскадное удаление при удалении роли.CanUserEditObject, CanUserSelectObject, CanUserInsertScheme, CanUserDeleteObject.RedbListItem с ленивой загрузкой объектов. Кэширование с TTL (по умолчанию 5 мин).QueryAsync<T>, ExecuteAsync, BeginTransactionAsync, ExecuteAtomicAsync, пакетная генерация ID.Инициализация — 9 шагов в InitializeAsync()
- Установить резолвер типов для
SystemTextJsonRedbSerializer - Авто-синхронизация схем — сканирование сборок на
[RedbSchemeAttribute], вызовSyncSchemeAsync<T>()для каждого типа - Синхронизация системной схемы
UserConfigurationProps - Инициализация конфигурации пользователя по умолчанию
RedbObjectFactory.Initialize(this)RedbObject.SetSchemeSyncProvider(_schemeSync)WarmupMetadataCacheAsync()— вызывает SQLwarmup_all_metadata_caches()InitializePropsCache()— создаётMemoryRedbObjectCache_treeProvider.InitializeTypeRegistryAsync()
Паттерн фабрики провайдеров
RedbServiceBase определяет 10 абстрактных фабричных методов:
CreateSchemeSyncProvider, CreateObjectStorageProvider,
CreateTreeProvider, CreateQueryableProvider,
CreateUserProvider, CreateRoleProvider,
CreatePermissionProvider, CreateListProvider,
CreateLazyPropsLoader, CreateValidationProvider.
Каждый проект провайдера PostgreSQL/MSSQL переопределяет их для внедрения поведения, специфичного для БД.
Ваш бизнес-код никогда не видит разницы.
RedbObject<T> — Основная сущность
Каждый объект в RedBase отображается на таблицу _objects.
Системные поля хранятся в колонках. Бизнес-данные (Props) хранятся как структурированные значения
со строгой типизацией и ссылочной целостностью на уровне базы данных.
_id_type FK → _types — каждое поле имеет строгий тип, контролируемый БД[RedbScheme]_structures и _objects.
Ссылки на объекты (_Object FK → _objects) и ссылки на списки (_ListItem FK → _list_items) —
«висячие» ссылки невозможны
_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') или строковые ключи для словарей.
Это значит, что можно запрашивать отдельные элементы массива через SQL —
WHERE, 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.
BaseFilterExpressionParserFilterExpressionFacetFilterBuildersearch_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_cachesProFilterExpressionParser без ограниченийExpressionToSqlCompiler реализация для каждой БДProSqlBuilder + SqlParameterCollectorSqlExpressionVisitor рекурсивно обходит дерево.
Реализация для каждой БД: PostgreSQL и MSSQL имеют собственные компиляторы.
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 компиляция
FacetFilterBuilder{"Age":{"$gt":30}}search_objects_with_facets()ExpressionToSqlCompiler рекурсивный обход дереваWHERE pvt."Age" > @p1Free сериализует ваш 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, фильтрация по нескольким полям
требует стратегии. Выбор этой стратегии определяет потолок производительности.
v._id_object,_Long) FILTER (WHERE _id_structure = $1))[1] AS "Age",_String) FILTER (WHERE _id_structure = $2))[1] AS "City"_values WHERE _id_structure = ANY($3)v._id_object)pvt."Age" > $4 AND pvt."City" = $5_objects o JOIN pvt_cte ON ...o._id_scheme = $1_values WHERE _id_object = o._id AND _id_structure = $2 AND _Long > 30)_values WHERE _id_object = o._id AND _id_structure = $3 AND _String = 'NY')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 становится всё более выгодным.
Материализация — сложные объекты без сложности
_values_list_itemsParallel.ForEach — CPU-bound, нуль обращений к БДILookup, прямое преобразование типовget_object_json() для каждого объектаSystem.Text.Json.Deserialize<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.
Дальше происходят принципиально разные вещи.
ProjectionFieldExtractorfieldPaths + structureIdssearch_objects_with_projection_by_pathsbuild_flat_projection() → частичный JSONPropscompiledProjection(obj) → TResult_values через SQL-функцию.
Фильтр проходит через JSON → plpgsql динамический SQL.
ProjectionFieldExtractorstructureIds HashSet<long>BuildQuerySqlAsync → скомпилированный WHERE + PVT CTE_objects → ID подходящих объектовProLazyPropsLoader: _values WHERE _id_structure = ANY($2)Parallel.ForEach материализация без JSONcompiledProjection(obj) → TResult_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 tracking —
ValueTreeBuilderстроит два дерева (память vs БД),ValueTreeComparerсравнивает их с пропуском поддеревьев по хешам. Только изменённые узлы генерируют SQL. Без delete-all/re-insert.
Dapper — это маппер. EF Core — это change tracker с транслятором запросов. RedBase — это движок данных — компилятор, планировщик запросов, материализатор, схема хранения, кэш, алгоритм diff — созданный для нагрузок со сложными вложенными сущностями, глубокими графами свойств и документоподобными структурами, для которых ORM никогда не были предназначены.
ExpressionToSqlCompiler — что компилируется
Внутренний SqlExpressionVisitor обходит деревья выражений C# и генерирует нативный SQL.
Для каждого типа узла есть свой обработчик:
Бинарные и унарные
Арифметика: +, -, *, /, %.
Сравнение: >, >=, <, <=, ==, !=.
Логические: AND, OR, NOT.
Null-объединение: x ?? default → COALESCE(x, default).
Проверка на null: x == null → IS NULL.
Convert: прямой проброс для приведения типов.
Math.*
Math.Abs → ABS(),
Math.Round → ROUND(),
Math.Floor → FLOOR(),
Math.Ceiling → CEIL(),
Math.Max → GREATEST(),
Math.Min → LEAST().
Поддержка нескольких аргументов (напр. Round(x, 2)).
String.*
Contains → LIKE '%' || @p || '%',
StartsWith → LIKE @p || '%',
EndsWith → LIKE '%' || @p,
ToUpper/ToLower → UPPER()/LOWER(),
Trim → TRIM().
Всё параметризовано — нет 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).
C# → SQL Конвейер компиляции
Pro не интерпретирует выражения — он их компилирует.
ExpressionToSqlCompiler обходит дерево выражений C# узел за узлом
и генерирует нативный параметризованный SQL. ProSqlBuilder обрабатывает деревья фильтров,
арифметику, функции, сортировку, pivot-подзапросы. Реализации для каждой БД: PostgreSQL и MS SQL.
Бинарные операторы
Арифметика: +, -, *, /, %.
Сравнение: ==, !=, >, >=, <, <=.
Логические: &&, ||.
Null-объединение: ?? → COALESCE(x, y).
Конкатенация строк: + → || (PostgreSQL).
Математические функции
Math.Abs → ABS(),
Math.Round → ROUND(),
Math.Floor → FLOOR(),
Math.Ceiling → CEIL(),
Math.Max → GREATEST(),
Math.Min → LEAST().
Компилируются инлайн — без оверхеда UDF.
Строковые функции
.Contains() → LIKE '%' || @p || '%',
.StartsWith() → LIKE @p || '%',
.EndsWith() → LIKE '%' || @p,
.ToUpper() → UPPER(),
.ToLower() → LOWER(),
.Trim() → TRIM(),
.Length → LENGTH().
Регистронезависимые варианты через ILIKE.
DateTime и доступ к свойствам
.Year → EXTRACT(YEAR FROM col),
.Month, .Day, .Hour, .Minute, .Second.
Вложенные свойства: x.Address.City → pvt."Address.City".
Неограниченная глубина вложенности через цепочечный обход GetPropertyPath().
Операции с коллекциями
x.Tags.Contains("v") → @p = ANY(pvt."Tags").
x.Tags.Count → COALESCE(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.
Словарь: ContainsKey → IS NOT NULL.
Всё параметризовано. Всё защищено от инъекций.
Генерация Pivot-запросов — PivotSqlGenerator
Превращает структурированное хранение свойств в плоские строки для запросов. Генерирует мульти-CTE SQL:
GeneratePvtCte() для плоских полей,
GenerateNestedDictCte() для вложенных ключей словарей,
GenerateNestedOnlyPvtCte() для доступа только к вложенным элементам.
Поля словарей вроде PhoneBook["home"] становятся реальными колонками в pivot.
Поля массивов агрегируются через array_agg().
Отдельные реализации для Postgres и MSSQL.
Кэширование SQL-выражений
ExpressionSqlCache — ConcurrentDictionary по хешу строки выражения.
Скомпилированный SQL из C#-выражений кэшируется, так что повторные запросы пропускают полный обход.
Кэш потокобезопасный и разделяется на время жизни IRedbService.
Компиляция крайних случаев
Nullable-навигация:
r.Auction != null && r.Auction.Costs > 100
→ компилируется в IS NOT NULL + доступ к вложенному свойству. Без NullReferenceException на уровне SQL.
Тернарный оператор в OrderBy:
r.Auction != null ? r.Auction.Baskets : 0
→ CASE 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) → IRedbGroupedQueryable → SelectAsync.
Также 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(...))
→ IRedbWindowedQueryable → SelectAsync.
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=0sys,id=1admin). Изменения пароля/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
гарантирует, что только один экземпляр захватит каждую задачу.
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 — неизменённые поддеревья бесплатны.
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.
История хранится в БД, идемпотентно (детекция изменений по хешу).
ProLazyPropsLoader.LoadPropsForManyAsync — 7-этапный конвейер с Parallel.ForEach:
- Пакетная проверка кэша —
FilterNeedToLoad<T>()по хешу: пропуск объектов, уже имеющихся вGlobalPropsCache - Один пакетный SELECT — все
_valuesдля всех ID объектов одним запросом - Пакетная предзагрузка ListItems — один запрос для всех связанных
_list_items - Предзагрузка схем + типов — загрузка структур схем и реестра типов один раз, до параллелизации
- Parallel.ForEach материализация —
ProPropsMaterializer.GetObjectProps<T>()работает в пуле потоков; внутри параллельного цикла нет обращений к БД - Рекурсивная загрузка вложенных объектов —
MaterializeNestedObjectsDynamically()разрешает ссылки на вложенныеRedbObjectс защитой от циклов - Подстановка —
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().
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.
Бесплатные примеры охватывают
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).
В цифрах
Продакшн-класс. Проверено боем. Настоящий движок данных.
Компилятор выражений, PVT-планировщик запросов, параллельный материализатор, движок структурного diff, трёхуровневый кэш — всё написано с нуля. Бесплатная MIT-редакция покрывает CRUD, запросы, деревья, безопасность, экспорт. Pro добавляет скорость и продвинутые выражения.
dotnet new install redb.Templates && dotnet new redb -n MyApp