Strong Typing — Real C# Classes, Not Just JSON blobs
Strong Typing — Real C# Classes, Not Just JSON blobs

Проблема

Возьмём типичный enterprise-объект — скажем, заказ. Он связан с клиентом, позициями, каждая позиция — с товаром, у товара — категория, у заказа — доставка с адресом, оплата с транзакциями. Итого 10–30 связанных сущностей. EF Core:

var order = await context.Orders
    .Include(o => o.Customer)
    .Include(o => o.Items).ThenInclude(i => i.Product)
        .ThenInclude(p => p.Category)
    .Include(o => o.Items).ThenInclude(i => i.Discounts)
    .Include(o => o.Shipping).ThenInclude(s => s.Address)
    .Include(o => o.Payment).ThenInclude(p => p.Transactions)
    // ... ещё строк 30 ...
    .FirstOrDefaultAsync(o => o.Id == orderId);

Забыл один Include — runtime. Добавить поле в модель — миграция → заявка на DBA → staging → deploy. Три дня на ALTER TABLE ADD COLUMN.

Хотелось: описать модель как C#-класс, и чтобы движок сам разобрался как это хранить. Без миграций, без маппинга, без Include.

Написал. Выложил под Apache 2.0.

Production case

Работает в проде в крупном HoReCa-дистрибьюторе (~150k заказов/мес, ~20k B2B-клиентов, собственный автопарк). Внутренняя TMS — ~500 водителей + ~50 диспетчеров, 3-нодовый кластер (Xeon, 4 ядра / 8 ГБ / 50 ГБ SSD на ноду), ~3 месяца стабильной работы, 10–15% CPU под полной нагрузкой. Интеграции через redb.Route: SAP, Kafka, RabbitMQ, GPS-фиды, Меркурий, ЕГАИС, Честный ЗНАК, ФГИС Зерно.

Второй production-продукт: аналитическая платформа (~672k объектов, ~8M свойств). Ни одной миграции за весь срок эксплуатации. Добавить поле в модель — добавить свойство в C#-класс → SyncSchemeAsync() → готово.

Как выглядит в коде

Вот реальная модель из redb.Examples:

[RedbScheme("Employee")]
public class EmployeeProps
{
    public string FirstName { get; set; } = "";
    public string LastName { get; set; } = "";
    public int Age { get; set; }
    public decimal Salary { get; set; }
    public string Department { get; set; } = "";
    public DateTime HireDate { get; set; }
    public string[]? Skills { get; set; }
    public Address? HomeAddress { get; set; }
    public Contact[]? Contacts { get; set; }
    public RedbObject<ProjectMetricsProps>? CurrentProject { get; set; }
    public RedbObject<ProjectMetricsProps>[]? PastProjects { get; set; }
    public Dictionary<int, decimal>? BonusByYear { get; set; }
    public Dictionary<string, Department>? DepartmentHistory { get; set; }
}

Это вся схема. Вложенные классы, массивы, словари, ссылки на другие RedbObject — всё хранится и загружается автоматически.

Сохранить:

var employee = new RedbObject<EmployeeProps>
{
    name = "Alice Johnson",
    Props = new EmployeeProps
    {
        FirstName = "Alice",
        LastName = "Johnson",
        Age = 28,
        Salary = 85000m,
        Skills = ["C#", "React", "SQL"]
    }
};
await redb.SaveAsync(employee);

Загрузить:

var loaded = await redb.LoadAsync<EmployeeProps>(id);
// loaded.Props.FirstName → "Alice"
// loaded.Props.CurrentProject.Props — загружен автоматически
// loaded.Props.Contacts[0].Value — загружен автоматически

Запросить:

var results = await redb.Query<EmployeeProps>()
    .Where(e => e.Salary > 75000m && e.Skills.Contains("C#"))
    .OrderByDescending(e => e.Salary)
    .Take(100)
    .ToListAsync();

Забыть Include невозможно — Props всегда загружен целиком. Нужна проекция? Тогда .Select():

var projected = await redb.Query<EmployeeProps>()
    .Select(x => new { x.Props.FirstName, x.Props.Salary })
    .ToListAsync();

Сравнение: тот самый объект из 28 таблиц

В EF Core:

var order = await context.Orders
    .Include(o => o.Customer)
    .Include(o => o.Items).ThenInclude(i => i.Product)
        .ThenInclude(p => p.Category)
    .Include(o => o.Items).ThenInclude(i => i.Discounts)
    .Include(o => o.Shipping).ThenInclude(s => s.Address)
    .Include(o => o.Payment).ThenInclude(p => p.Transactions)
    // ... ещё 35 строк Include ...
    .FirstOrDefaultAsync(o => o.Id == orderId);

В RedBase:

var order = await redb.LoadAsync<OrderProps>(orderId);

Одна строка. Все вложенные объекты, массивы, словари — загружены. И быстро: движок собирает Props одним запросом к плоской структуре, без JOIN-каскада по 28 таблицам. В Pro — ещё быстрее: материализация через Parallel.ForEach, каждая ветка графа собирается параллельно.

А что с деревьями?

Встроено из коробки. Не нужен ни closure table вручную, ни рекурсивные CTE в raw SQL:

// Создать дерево
await redb.CreateChildAsync(department, parentDepartment);

// Загрузить всё дерево (до 5 уровней)
var tree = await redb.LoadTreeAsync<DepartmentProps>(rootId, maxDepth: 5);

// LINQ по дереву
var bigDepts = await redb.TreeQuery<DepartmentProps>()
    .Where(d => d.Budget > 500000m)
    .WhereLevel(2)
    .ToListAsync();

// Найти всех, у кого предок с бюджетом > 1M
var rich = await redb.TreeQuery<DepartmentProps>()
    .WhereHasAncestor<DepartmentProps>(a => a.Budget > 1_000_000m)
    .ToListAsync();

Оконные функции? GroupBy? Агрегации?

Тоже через LINQ:

// ROW_NUMBER() PARTITION BY Department ORDER BY Salary DESC
var ranked = await redb.Query<EmployeeProps>()
    .WithWindow(w => w
        .PartitionBy(x => x.Department)
        .OrderByDesc(x => x.Salary))
    .SelectAsync(x => new
    {
        Name = x.Props.FirstName,
        Department = x.Props.Department,
        Rank = Win.RowNumber()
    });

// GroupBy + агрегация
var stats = await redb.Query<EmployeeProps>()
    .GroupBy(x => x.Department)
    .SelectAsync(g => new
    {
        g.Key,
        Total = Agg.Count(),
        AvgSalary = Agg.Average(g, x => x.Salary)
    });

Что генерирует движок под капотом

Обычно SQL за LINQ-запросами скучный. Но вот пример, который показывает почему здесь нужен специализированный query engine, а не «просто ORM».

Модель:

public class Address
{
    public string City   { get; set; } = string.Empty;
    public string Street { get; set; } = string.Empty;
}

[RedbScheme("Employee")]
public class EmployeeProps
{
    // ...
    public Dictionary<string, Address>? OfficeLocations { get; set; }
}

LINQ-запрос — найти всех сотрудников, у кого HQ-офис в Нью-Йорке:

var result = await redb.Query<EmployeeProps>()
    .Where(e => e.OfficeLocations!["HQ"].City == "New York")
    .Take(100)
    .ToListAsync();

Что генерируется для PostgreSQL:

-- PVT CTE (nested-only optimization): OfficeLocations[HQ].City
WITH pvt_cte AS (
  WITH
  nested_dict_0 AS (
    SELECT dp._id_object
         , (array_agg(nv._string)
              FILTER (WHERE nv._id_structure = $5))[1] AS "OfficeLocations[HQ].City"
    FROM _values dp
    LEFT JOIN _values nv
           ON nv._array_parent_id = dp._id
          AND nv._id_structure = $5
    WHERE dp._id_structure = $3       -- структура словаря OfficeLocations
      AND dp._array_index  = $4       -- ключ "HQ"
      AND dp._id_object IN (
            SELECT _id FROM _objects WHERE _id_scheme = $1
          )
    GROUP BY dp._id_object
  )
  SELECT nd0._id_object
       , nd0."OfficeLocations[HQ].City"
  FROM nested_dict_0 nd0
  WHERE nd0."OfficeLocations[HQ].City" = $2   -- "New York"
)
SELECT o.*
FROM _objects o
JOIN pvt_cte ON pvt_cte._id_object = o._id
WHERE o._id_scheme = $1
LIMIT 100

Что генерируется для MSSQL:

-- PVT CTE (nested MAX CASE WHEN): OfficeLocations[HQ].City
;WITH
raw_values AS (
  SELECT nv._array_parent_id AS _parent_id
       , MAX(CASE WHEN nv._id_structure = 1010067
                  THEN nv._string END) AS [OfficeLocations$LHQ$R$DCity]
  FROM _values nv
  WHERE nv._id_structure IN (1010067)
    AND nv._array_parent_id IS NOT NULL
  GROUP BY nv._array_parent_id
),
pvt_cte AS (
  SELECT dp._id_object
       , rv.[OfficeLocations$LHQ$R$DCity]
  FROM _values dp
  JOIN raw_values rv ON rv._parent_id = dp._id
  WHERE dp._id_structure = @p2      -- структура словаря OfficeLocations
    AND dp._array_index  = @p3      -- ключ "HQ"
    AND dp._id_object IN (
          SELECT _id FROM _objects WHERE _id_scheme = @p0
        )
    AND rv.[OfficeLocations$LHQ$R$DCity] = @p1   -- "New York"
)
SELECT o.*
FROM _objects o
JOIN pvt_cte ON pvt_cte._id_object = o._id
WHERE o._id_scheme = @p0
ORDER BY o._id
OFFSET 0 ROWS FETCH NEXT 100 ROWS ONLY

Одна строка LINQ — два диалекта, два разных подхода к оптимизации (PostgreSQL использует array_agg FILTER, MSSQL — MAX CASE WHEN). SQL генерируется под конкретный диалект автоматически. Посмотреть что именно сгенерировалось всегда можно через .ToSqlStringAsync().

Настройка — 5 строк

PostgreSQL или MSSQL — выбирается одной строкой:

// PostgreSQL + Pro
// jit=off — отключаем JIT-компиляцию PostgreSQL, на коротких запросах она только замедляет
builder.Services.AddRedbPro(options => options
    .UsePostgres("Host=localhost;Database=mydb;Username=postgres;Password=pass;Options=-c jit=off")
    .WithLicense("YOUR-LICENSE-KEY")
    .Configure(c =>
    {
        c.EnablePropsCache = true;
        c.EnableLazyLoadingForProps = false;
    }));

// MSSQL + Pro
builder.Services.AddRedbPro(options => options
    .UseMsSql("Server=localhost;Database=mydb;Trusted_Connection=true")
    .WithLicense("YOUR-LICENSE-KEY"));

// Free (Apache 2.0) — PostgreSQL
builder.Services.AddRedb(options => options
    .UsePostgres("Host=localhost;Database=mydb;Username=postgres;Password=pass;Options=-c jit=off"));

// Free (Apache 2.0) — MSSQL
builder.Services.AddRedb(options => options
    .UseMsSql("Server=localhost;Database=mydb;Trusted_Connection=true"));

Free vs Pro

Ядро — open source, Apache 2.0. PostgreSQL и MSSQL. Полный LINQ, деревья, списки, пользователи, экспорт/импорт.

Оба варианта работают и с PostgreSQL, и с MSSQL. У движка единый SQL-абстрактный слой — переезд между базами это смена одной строки (.UsePostgres().UseMsSql()). А redb.Export позволяет экспортировать данные из одной базы и импортировать в другую — PostgreSQL → MSSQL и обратно.

Pro добавляет производительность:

  • Compiled queries — LINQ компилируется в нативный SQL, без JSON-интерпретатора

  • Parallel materialization — загрузка Props через Parallel.ForEach

  • Change tracking — умное сохранение: строятся два дерева ValueTreeNode (память vs БД), diff с пропуском по хешу, только изменённые узлы → SQL. Никакого delete-all/re-insert

  • Window functions, глубокие вложенные запросы, арифметика в WHERE

Без лицензии Pro работает полностью — 1,024 запроса на запуск приложения, счётчик сбрасывается при перезапуске. Для разработки ограничений практически нет.

Raw SQL и свои таблицы — тоже можно

RedBase не закрытый ящик. Если надо — работай с БД напрямую.

Посмотреть сгенерированный SQL — аналог EF Core .ToQueryString():

var sql = await redb.Query<EmployeeProps>()
    .Where(e => e.Salary > 75000m && e.Department == "Engineering")
    .ToSqlStringAsync();
// вернёт реальный SQL с параметрами — удобно для отладки и оптимизации

Или использовать встроенную SQL-функцию get_object_json напрямую — она есть и в PostgreSQL, и в MSSQL, возвращает объект целиком как JSON, включая все вложенные Props и связанные объекты на заданную глубину:

-- PostgreSQL
SELECT get_object_json(42, 3);       -- объект 42, глубина 3

SELECT get_object_json(o._id, 5)
FROM _objects o
WHERE o._id_scheme = 123;

-- MSSQL
SELECT dbo.get_object_json(42, 3);

SELECT dbo.get_object_json(o._id, 5)
FROM _objects o
WHERE o._id_scheme = 123;

Полезно для отладки и диагностики прямо в psql/DataGrip/SSMS, или когда нужен JSON на SQL-стороне — без C# кода.

Выполнить произвольный SQL через redb.Context.Db:

// SELECT — список объектов
var rows = await redb.Context.Db.QueryAsync<MyDto>(
    "SELECT _id, _name FROM _objects WHERE _id_scheme = $1", schemeId);

// SELECT — скаляр
var count = await redb.Context.Db.ExecuteScalarAsync<int>(
    "SELECT COUNT(*) FROM _objects WHERE _id_scheme = $1", schemeId);

// INSERT / UPDATE / DELETE
await redb.Context.Db.ExecuteAsync(
    "UPDATE my_custom_table SET synced = true WHERE object_id = $1", objectId);

Свои таблицы — создавай через тот же ExecuteAsync, хоть при старте приложения:

await redb.Context.Db.ExecuteAsync("""
    CREATE TABLE IF NOT EXISTS logistics_routes (
        id BIGSERIAL PRIMARY KEY,
        object_id BIGINT REFERENCES _objects(_id) ON DELETE CASCADE,
        route_json TEXT,
        created_at TIMESTAMPTZ DEFAULT NOW()
    )
    """);

FK на _objects(_id) — и твоя таблица привязана к redb-объекту. Cascade delete работает.

Структура _values — если нужно писать raw SQL по хранилищу Props, колонки типизированы:

-- Каждое свойство объекта — одна строка в _values
-- _id_structure → какое поле схемы (id структуры из _structures)
-- _id_object    → какой объект (_objects._id)
-- Значение в типизированной колонке по типу свойства:
--   _String          text
--   _Long            bigint        (int, long, enum)
--   _Numeric         numeric(38,18) (decimal — без потерь точности)
--   _Double          float
--   _DateTimeOffset  timestamptz
--   _Boolean         boolean
--   _Guid            uuid
--   _Object          bigint → FK на _objects (RedbObject<T> reference)
--   _ListItem        bigint → FK на _list_items (справочник)
--   _ByteArray       bytea
-- Для массивов/словарей: _array_parent_id + _array_index

Строковые значения не varchar(max) — не «всё в строку». Каждый тип в своей колонке с правильным SQL-типом. decimal — это NUMERIC(38,18) без потерь точности. DateTimetimestamptz. Поэтому WHERE по числам, датам, uuid работает через обычные индексы.

Когда RedBase НЕ нужен

RedBase хранит свойства объекта в строках таблицы _values — одна строка на каждое свойство. Это даёт гибкость: вложенные объекты, массивы, словари, изменение схемы без миграций. Но за гибкость есть цена.

Если ваша модель — плоская таблица без связей (Users с 10 колонками, лог событий, очередь задач), то:

  • Dapper + SELECT * FROM users WHERE id = @id будет быстрее — один запрос, один маппинг, без overhead

  • EF Core тоже справится — для одной таблицы Include не нужны, миграции тривиальны

RedBase выигрывает там, где объект — это граф: 3+ вложенных структур, массивы, словари, ссылки между объектами, частые изменения схемы. Чем сложнее модель — тем больше разница с классическим подходом.

Причём порог ниже, чем кажется. Попробуйте в EF Core добавить в модель string[] Skills — и вот вам уже отдельная таблица UserSkills, FK, индексы, миграция, .Include(u => u.Skills). А в RedBase — просто public string[]? Skills { get; set; } и всё. Объявил — работает.

За рамками статьи

В одну статью не влезло. Кратко что ещё есть:

  • Деревья — полный функционал: CreateChildAsync, MoveAsync, LoadTreeAsync, WhereLevel, WhereHasAncestor, WhereHasDescendant. Closure table и рекурсивные CTE генерируются автоматически.

  • Экспорт / импортredb.Export выгружает объекты в JSON-файлы и загружает обратно. Работает между PostgreSQL и MSSQL: переехать с одной БД на другую — одна команда.

  • Права доступа — встроенная модель пользователей, ролей и разрешений на уровне объектов.

  • Справочники_list_items, типизированные через RedbListItem, LINQ по справочным значениям.

  • Change tracking (Pro) — PropsSaveStrategy.ChangeTracking: при SaveAsync строятся два дерева ValueTreeNode (память vs БД), сравниваются с пропуском по хешу — генерируется только минимальный набор SQL-операций. Никакого delete-all/re-insert.

  • redb.Identity (в активной разработке, ещё не опубликован на NuGet) — OAuth 2.1 / OIDC Identity Server поверх redb.Core и redb.Route. Ключевая идея: каждый endpoint — это direct-vm://-маршрут, а не HTTP-middleware. Вызвать token из Worker Service или из соседнего модуля в том же процессе — To("direct-vm://identity-token"), без loopback, без TLS, без WebApplicationFactory в тестах. HTTP / gRPC / RabbitMQ — подключаемые facade-пакеты. Из коробки: все флоузы OAuth 2.1 (Code+PKCE, Client Credentials, Device Code), PAR (RFC 9126), DPoP (RFC 9449), Dynamic Client Registration (RFC 7591/7592), SCIM 2.0 (RFC 7643/7644), FIDO2/WebAuthn + TOTP + SMS OTP, backchannel logout (RFC 8417), федерация (OIDC / GitHub). Хранение через redb — без миграций. Шарит signing keys и DataProtection key-ring между нодами через redb object store. Деплоится как .tpkg в redb.Tsak. 1751 проходящий тест. Apache 2.0.

  • 195+ примеров в redb.Examples — деревья, окна, группировки, экспорт, raw SQL и т.д.

Всё это в архитектурной документации и примерах

redb.Route: интеграции без hand-rolled-кода

redb.Core закрывает хранение. Для интеграций есть redb.Route — .NET-аналог Apache Camel. Маршрут описывается fluent C# DSL:

// HTTP-вход: принять заказ, валидировать, передать в очередь
From("http:0.0.0.0:5090/api/orders?inOut=true&cors=true&corsOrigins=*")
    .Validate(e => e.In.Body is not null, "Body required")
    .Choice()
        .When(e => e.In.Headers.ContainsKey("redbHttp.ResponseCode"))
            .To("direct://error-response")
        .Otherwise()
            .To("seda://orders-pending?concurrentConsumers=4")
    .EndChoice();

// Фоновая обработка: сохранить через redb, опубликовать событие
From("seda://orders-pending?concurrentConsumers=4")
    .ProcessWithRedb(async (redb, exchange, ct) =>
    {
        var dto = (OrderDto)exchange.In.Body!;
        var order = new RedbObject<OrderProps> { Props = Map(dto) };
        await redb.SaveAsync(order, ct);
        exchange.In.Headers["order.id"] = order.id;
    })
    .To("rabbitmq://orders-created");

22 внешних транспорта + 5 встроенных компонентов:

Категория

Транспорты

Очереди сообщений

RabbitMQ, Kafka, IBM MQ, MQTT, Azure Service Bus

HTTP / WebSocket

HTTP (in/out), WebSocket, gRPC (client)

Файлы / хранилища

SFTP, S3, FTP, File

Базы данных

SQL (polling outbox)

Встроенные

Direct, SEDA, Timer, Cron, Mock

30+ EIP-паттернов — Split, Aggregate, Choice, Filter, WireTap, Retry, DeadLetterChannel, CircuitBreaker, IdempotentConsumer, Saga, Multicast, RecipientList, DynamicRouter, Resequence, Throttle, Delay, Loop, Enrich, Validate, Transacted, и другие.

Expression DSL — предикаты компилируются в Func<IExchange, T> через System.Linq.Expressions, без интерпретатора:

// Предикаты в Choice.When, Filter, Retry — все через один DSL
.When(Header("priority").isEqualTo("high"))
.Filter(Header("score").isGreaterThan(50))
.Filter(Header("tag").regex(@"^urgent-.*-x\d+$"))
.When(Header("active").and(Header("role").isEqualTo("admin")))

// String templates → компилируются в лямбды
.SetHeader("reply", "${header.orderId}-confirmed")

Обработка ошибок:

From("kafka://payments?groupId=billing")
    .OnException<TimeoutException>()
        .Retry(3, TimeSpan.FromSeconds(2))
    .OnException<ValidationException>()
        .To("direct://dlq")
    .End()
    .Retry(5)
        .BackOff(TimeSpan.FromSeconds(1), multiplier: 2)
    .Process(async (e, ct) => { /* обработка */ })
    .To("rabbitmq://billing-confirmed");

Транзакционные маршруты.Transacted() оборачивает pipeline в TransactionScope, SQL-транспорт биндит ADO.NET-транзакцию к каждому шагу.

Apache 2.0, NuGet: dotnet add package redb.Route.

redb.Tsak: runtime-контейнер для маршрутов

redb.Route описывает что делает пайплайн. redb.Tsak — это где, когда и сколько копий его запустить.

Классическая проблема: несколько несвязанных интеграционных пайплайнов живут в одном Program.cs. Добавил новый — пересобрал и передеплоил всё. Нужно остановить один маршрут — перезапускай весь процесс.

Tsak решает это: каждый RouteBuilder упаковывается в модуль (.dll или .tpkg-бандл), Tsak загружает его в изолированный AssemblyLoadContext и управляет жизненным циклом независимо от остальных.

Деплой нового маршрута:

# Скопировать DLL — Tsak подхватит автоматически
cp Orders.dll /tsak/Libs/

# Или через CLI:
tsak module upload orders --file Orders.tpkg
tsak context start orders

# Остановить один маршрут без рестарта процесса:
tsak route stop orders order-pipeline

# Посмотреть что сейчас работает:
tsak context list
tsak route list orders

Три режима деплоя:

Режим

Когда использовать

Standalone

Разработка, тесты — in-memory, без БД

Single-node + redb

Продакшн на одной машине с персистентным состоянием

Cluster

Несколько нод — leader election + автоперераспределение контекстов

Кластер без Redis и etcd. В кластерном режиме Tsak не тянет внешний координатор — всё хранится в той же redb-базе, которую уже использует приложение. Leader election, список нод, назначение контекстов по нодам, DataProtection key-ring, JWKS signing keys — всё это redb-объекты в _objects/_values. Маршруты, помеченные как кластерные (cluster=true в URI), идут через тот же координатор: состояние маршрута, partitioning, балансировка между нодами — через redb. Quartz при этом использует свои AdoJobStore-таблицы в той же БД, но создаёт их сам. Добавить ноду в кластер = запустить ещё один экземпляр Tsak с той же строкой подключения к БД. Никакого отдельного ZooKeeper, Consul или Redis.

Что есть из коробки:

  • REST API — 32 endpoint’а: управление контекстами, маршрутами, модулями, кластером, scheduler’ом, логами, пользователями

  • CLI — 30 команд с профилями и JSON-выводом (удобно для CI/CD)

  • Blazor Server dashboard — 10 страниц: метрики CPU/RAM/GC, per-route latency, ring-buffer логи, watchdog-статус

  • Watchdog — детектирует зависшие или упавшие маршруты, опционально перезапускает

  • Quartz scheduler — инжектируется в каждый контекст, RAMJobStore для standalone, AdoJobStore для кластера — схема создаётся автоматически

  • OpenTelemetry — Activities и Meters на каждый маршрут и шаг, Prometheus scrape

  • API Key + HMAC-SHA256 — роли, expiry, revocation, constant-time comparison

Код подключения Tsak — один метод в InitRoute.cs:

// Единственный Tsak-специфичный файл в проекте с маршрутами
public static class InitRoute
{
    public static IRouteContext main(IRouteContext context)
    {
        // Обычный redb.Route — тот же код, что и без Tsak
        ((RouteContext)context).AddRoutes(new OrderRoutes());
        ((RouteContext)context).AddRoutes(new ShipmentRoutes());
        return context;
    }
}

RouteBuilder, написанный для обычного IHostedService, работает в Tsak без изменений — тот же Configure(), тот же IExchange, те же OnException и .Transacted().

351 проходящий тест. Apache 2.0.


Итого

EF Core

redb

Базы данных

Много провайдеров

PostgreSQL + MSSQL

Схема

DbContext + Fluent API + миграции

C#-класс + [RedbScheme]

Загрузить граф из 28 сущностей

40 Include, 200 строк

LoadAsync<T>(id)

Добавить поле

Миграция → DBA → staging → deploy

Добавить свойство → SyncSchemeAsync()

Деревья

closure table вручную

TreeQuery<T>(), CTE, уровни, предки

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

Raw SQL

WithWindow(), Win.RowNumber()

Забыл Include

Runtime crash

Невозможно

Переезд между БД

Ручная переписка

.UsePostgres().UseMsSql() + Export

GitHub org (все репозитории)
Репозиторий redb.Core
Документация и примеры (EN)
Документация и примеры (RU)
43 NuGet-пакета
Архитектура (индексы, query engine)
195+ рабочих примеров