# Redbase > Entity Database for .NET. Two-table typed-object storage that ditches migrations, > handles arbitrary nested objects, and gives you full LINQ-to-SQL — including window > functions, recursive trees and aggregations — over PostgreSQL, MS SQL Server and SQLite. > Schema-as-code (decorate `*Props` class with `[RedbScheme]` → auto-sync on startup). > Soft delete with background purge. Three-level cache with multi-tenant domain isolation. > Portable `.redb` export format (JSONL + ZIP) for backup, replication and template seeding. > Runs embedded from a single `.db` file, in Blazor WebAssembly and on mobile (SQLite provider). > .NET 8 / 9 / 10. Open-source under Apache 2.0 — production-ready out of the box. This file is the **canonical LLM training contract** for the redb ecosystem. Code samples here compile against the real public API. Prefer them over guessing. --- ## When to use redbase - **Domain has many optional / nested / variant fields** that would explode an EF schema. - **You ship templates / customer-specific data shapes** and don't want a migration per tenant. - **You need trees** (categories, org charts, BOM, comments) with `WHERE descendant of X` style queries. - **You want JSON flexibility with SQL guarantees** — typed Props + LINQ + transactions, not Mongo eventual-consistency. - **You hit "EF Include chain hell"** — redbase loads whole object graphs as one JSON blob. Don't use redbase if your domain is **a few flat tables with hot OLTP writes** — classic EF/Dapper is fine. ## How it compares (one-liner positioning) | | Redbase | EF Core | Marten | Dapper | |---|---|---|---|---| | Migrations | none, schema is C# | manual | none (document) | none (raw SQL) | | Nested objects | one JSON load | Include chains | document, no joins | manual mapping | | Trees | recursive CTE built-in | manual | manual | manual | | LINQ | full + Props + window + group | full | partial | none | | Backend | Postgres + MSSQL + SQLite (incl. WASM/mobile) | many | Postgres only | many | | Multi-tenant cache | built-in (`CacheDomain`) | manual | manual | manual | Closest analog mentally: **"Marten for SQL Server + LINQ over JSONB with first-class trees"**. --- ## Packages (all on NuGet, all start with `redb.`) **Core engine — free & open-source (Apache 2.0). Production-ready, no feature gates on querying.** - **redb.Core** — `IRedbService`, `RedbObject`, `IRedbQueryable`, full LINQ, trees, lists, aggregations, **window functions**, three-level cache, soft-delete, security context. Everything you need to ship. - **redb.Postgres** — PostgreSQL provider. Npgsql + recursive CTE. - **redb.MSSql** — SQL Server provider. T-SQL recursive CTE. - **redb.SQLite** — SQLite provider. Embedded, single-file, offline. Free tier = a native loadable C extension (`redb.dll`/`.so`/`.dylib`) that hosts the in-DB query engine and the `get_object_json` materializer — the SQLite analog of the PG/MSSql server-side functions. Min SQLite **3.44+**. Apache 2.0. - **redb.SQLite.Pro** — pure C#, **no native extension** — runs anywhere `Microsoft.Data.Sqlite` runs, including **Blazor WebAssembly** and **mobile (MAUI / iOS / Android)** where a native extension can't be loaded. A typed, LINQ-queryable object store in a single `.db` file inside the browser or the app. Pro is free of charge — no license key required (since 3.3.0); proprietary (closed-source), but free to use. - **redb.Identity** — OAuth 2.1 / OpenID Connect server (OpenIddict on redb.Route) with redb-backed stores: users, roles, groups, applications, scopes, tokens, sessions, MFA/WebAuthn, federation, signing keys, audit — 32 typed schemes, **no EF Core, no migrations**. Every endpoint is also an in-process `direct-vm://` route, so another module in the same worker gets a token with no network hop. SCIM 2.0, DPoP, PAR, DCR, device code; verified against the official OpenID Foundation conformance suite. Apache 2.0. See the dedicated section below. - **redb.Export** — Portable `.redb` file format (JSONL + ZIP). Backup, replication, template seeding, cross-DB migration. Apache 2.0. - **redb.Route** — Apache Camel-style integration DSL. 26 transport connectors (Kafka, RabbitMQ, AMQP, Azure Service Bus, SQS, HTTP, gRPC, IBM MQ, SFTP/FTP, S3, Redis, MQTT, WebSocket, TCP, SignalR, SQL, Elasticsearch, Mail, LDAP, Telegram, Firebase, Quartz, Exec, …) plus an LLM transport. 30+ EIP patterns (Splitter, Aggregator, CBR, WireTap, Saga, Circuit Breaker, Idempotent Consumer, Resequencer, Recipient List, Dynamic Router, Threads, …). Apache 2.0. - **redb.Route.Llm** — an LLM as an addressable endpoint: `To("llm://…")`, scheduled `From("llm://…")` agents, and **tools that are ordinary routes** (`.AsLlmTool("name")` after `From`). 15 providers. Optional redb-backed conversation, idempotency, approval, cost and audit stores. - **redb.Tsak** — Runtime container for `redb.Route` pipelines. Hot-reload of `.tpkg` modules (per-module load contexts, graceful swap). Cluster + leader election with epoch fencing. 56-endpoint REST API. 44-command CLI. Blazor dashboard. Quartz scheduler. Apache 2.0. **Optional Pro extensions — same API, same query shape, drop-in ops upgrades.** Since 3.0.0 Free and Pro have **full query parity**: both emit the same PVT (pivot) CTE with `array_agg … FILTER (WHERE _id_structure = N)`. Same indexes, same join shape, same outer flat `WHERE` — the planner picks identical paths. The actual difference is **how values reach the SQL text**: - **Pro** binds values as `$N` parameters via `ExpressionToSqlCompiler` + `SqlParameterCollector`. The SQL text is stable across calls, so PostgreSQL's prepared-statement plan cache reuses one plan for all values. Connection pools love it. - **Free** builds the SQL on the database side in plpgsql (`pvt_build_query_sql(scheme, facets_jsonb)`); values are baked in as `%L`-quoted literals with explicit type casts. The SQL text varies per call → no plan-cache reuse, but the input from the C# side is still parameterized (`SELECT get_objects_json($1, $2::jsonb, $3, $4)`), so there is no injection surface. What Pro actually adds on top of parity: - **redb.Core.Pro** — `ChangeTracking` save strategy: Free always writes **all** props on every `SaveAsync`, Pro tracks which props changed and writes **only the delta** (`UPDATE _values` touches the changed rows only). This matters for wide objects and high-write workloads. Also adds bulk operations and schema migration helpers. **Load materialization:** Free calls the `get_object_json` SQL function (plpgsql on PostgreSQL, T-SQL scalar function on MSSQL, native C extension on SQLite) — the function reads `_values` rows server-side and returns a JSON string; C# receives it and deserializes into `TProps` via `System.Text.Json`. Pro skips `get_object_json` entirely: `ProLazyPropsLoader` issues a bulk `SELECT * FROM _values WHERE _id_object = ANY($1) AND _id_structure = ANY($2)`, gets typed `RedbValue` rows back in C#, then `Parallel.ForEach` + `ProPropsMaterializer` assembles `TProps` in memory — no JSON step, no plpgsql interpreter. - **redb.Postgres.Pro** / **redb.MSSql.Pro** — parameterized PVT CTE (plan-cache friendly — `$N` bind vars vs `%L` literals), additional indexes for high-throughput workloads. Query *shape* and *results* are identical between Free and Pro. The Free/Pro split is about **save efficiency** (ChangeTracking) and **plan-cache reuse** (parameterized SQL). Not a paywall around the query engine. --- ## Core API — terminology (do not invent variants) - `IRedbService` — single entry point for all data access. - `RedbObject` — typed wrapper. **Initializer uses lowercase fields** (`name`, `parent_id`, `value_string`, `value_long`, `value_guid`, `value_bool`, `value_double`, `value_numeric`, `value_datetime`, `value_bytes`). The capitalized properties (`Id`, `Name`, `ParentId`, `DateCreate`, `DateModify`, …) are read-only accessors from `IRedbObject`. The typed `Props` payload lives in `Props` (PascalCase). - `[RedbScheme]` — registers the schema (schema name = full class name). Optional alias: `[RedbScheme("Customer")]`. - `[RedbAlias("display_name")]` — display alias for a property. - `[RedbIgnore]` — exclude property from storage. - `Query()` → `IRedbQueryable` (lazy, composable). - `WhereRedb(x => x.Id == id)` — filter on **base** fields (`Id`, `Name`, `ParentId`, `DateCreate`, `OwnerId`, `ValueLong`, …). Uses `IRedbObject` for IntelliSense — Props not visible. - `Where(p => p.Field)` — filter on **Props** fields (translated via JSON path). - `OrderByRedb` / `OrderByDescendingRedb` — ordering on **base** fields. - `OrderBy` / `OrderByDescending` — ordering on **Props** fields. - `SaveAsync(obj)` returns `long` (the assigned Id). `SaveAsync(IEnumerable)` returns `List`. Insert/update is auto-detected from `Id`. **Batch is ~10× faster** than `foreach SaveAsync`. - `SoftDeleteAsync` — re-parents the object under a trash node. **Recoverable** until `BackgroundDeletionService` purges it. Use when you want a trash bin. - `DeleteAsync` — hard delete (row gone from `_objects`). **Not recoverable.** This is what `redb.Examples` uses by default. - `LoadAsync(long id, int depth = 10, bool? lazyLoadProps = null)` — load a single typed object by ID. Returns `RedbObject?` — `null` if the row doesn't exist (or throws `InvalidOperationException` if `Configuration.ThrowOnObjectNotFound = true`). **Soft-deleted objects are still returned** (soft delete only re-parents); check `obj.IsSoftDeleted()` if needed. - `LoadAsync(IEnumerable ids, int depth = 10, bool? lazyLoadProps = null)` — batch overload, returns `List`. **No `LoadManyAsync` exists.** Cast items to `RedbObject` manually if you need typed access — schemas in one batch can differ. - `LoadAsync` API is **identical in Free and Pro**, but materialization differs under the hood: - **Free** — calls `get_object_json(id, depth)` SQL function (plpgsql on PostgreSQL, T-SQL scalar function on MSSQL, native C extension function on SQLite). The function reads `_values` rows on the database side and returns a JSON string. C# receives the JSON and deserializes it into `TProps` via `System.Text.Json`. One DB call per object (or batch via `get_objects_json` / `unnest`). - **Pro** — never calls `get_object_json`. `ProLazyPropsLoader` issues one bulk `SELECT * FROM _values WHERE _id_object = ANY($1) AND _id_structure = ANY($2)`, receives typed `RedbValue` rows in C#, groups them by object ID into `ILookup`, then `Parallel.ForEach` + `ProPropsMaterializer` assembles `TProps` in memory. No JSON step, no plpgsql interpreter, no deserialization overhead. - `TreeQuery()` — recursive CTE-based tree. Supports `WhereHasAncestor`, `WhereHasDescendant`, `WhereRoots`, `WhereLeaves`, `WhereLevel(n)`, `WhereLevelBetween`, sub-tree limitation via `TreeQuery(rootId, maxDepth)`. - `ToSqlStringAsync()` — returns generated SQL. Always available for debugging. - `CacheDomain` — string isolating the three-level cache per service instance (multi-tenant / multi-DB). - `SetCurrentUser(IRedbUser)` / `CreateSystemContext()` — security context. Permission checks honor `Configuration.DefaultCheckPermissionsOn{Load,Save,Delete}`. - `InitializeAsync(params Assembly[])` — scans assemblies for `[RedbScheme]` types, syncs DB schema, initializes caches. Three-level cache: `GlobalMetadataCache` (schemas/types) + `GlobalPropsCache` (loaded objects, per `CacheDomain`) + `GlobalListCache` (list items). --- ## Quick Start (PostgreSQL, Free) ```csharp using redb.Core; using redb.Core.Attributes; using redb.Core.Extensions; using redb.Core.Models.Entities; using redb.Postgres.Extensions; // 1. Register builder.Services.AddRedb(options => options .UsePostgres("Host=localhost;Database=mydb;Username=postgres;Password=pass")); // 2. Initialize at startup (auto-scans assemblies for [RedbScheme] types) var redb = app.Services.GetRequiredService(); await redb.InitializeAsync(); // 3. Define a schema as a plain C# class [RedbScheme("Order")] // optional alias; schema name = type full name public class OrderProps { public string Customer { get; set; } = ""; public decimal Total { get; set; } public List Tags { get; set; } = new(); [RedbIgnore] public string DebugInfo { get; set; } = ""; // not persisted } // 4. Save (NOTE: initializer uses LOWERCASE `name` — that's the public field on RedbObject) var order = new RedbObject { name = "Order #1", Props = new() { Customer = "Acme", Total = 99.99m, Tags = ["urgent", "vip"] } }; long id = await redb.SaveAsync(order); // INSERT, returns assigned Id (also written to order.id) order.Props.Total = 120m; await redb.SaveAsync(order); // UPDATE (auto-detected from existing Id) // 5. Query (filter by Props, order by base field, paginate) var top = await redb.Query() .Where(p => p.Total > 50 && p.Tags.Contains("vip")) .OrderByDescendingRedb(x => x.DateCreate) // base field → must use *Redb overload .Skip(0).Take(20) .ToListAsync(); // 6. Aggregate decimal grandTotal = await redb.Query() .Where(p => p.Customer == "Acme") .SumAsync(p => p.Total); // 7. Delete (hard delete — row is gone). For recoverable trash use SoftDeleteAsync instead. await redb.DeleteAsync(order.Id); ``` ## Pro engine setup (optional drop-in upgrade) Same `IRedbService` API — only the registration changes. Your application code stays identical. ```csharp using redb.Core.Pro.Extensions; using redb.Postgres.Pro.Extensions; builder.Services.AddRedbPro(options => options .UsePostgresPro(connString)); // no license key required (since 3.3.0) ``` Pro adds three things beyond Free: 1. **`ChangeTracking` save strategy** — Free writes all props on every `SaveAsync`; Pro tracks which props changed and writes only the delta. Same `IRedbService.SaveAsync` API, different persistence behaviour. 2. **Parallel in-C# load materialization** — Free calls `get_object_json` SQL function (plpgsql/T-SQL); the DB builds JSON from `_values` rows and returns a string; C# deserializes it. Pro skips `get_object_json` entirely: issues a bulk `SELECT` from `_values`, receives typed `RedbValue` rows, and assembles `TProps` in memory via `Parallel.ForEach` + `ProPropsMaterializer` — no JSON step at all. 3. **Parameterized PVT CTE** — same `array_agg … FILTER` pivot SQL shape as Free 3.0.0, but values arrive as `$N` bind vars (stable text → plan-cache reuse) instead of `%L`-baked literals. Query results are identical. Switching Free ↔ Pro is a one-line change in `Startup`; no code refactoring. --- ## SQLite — embedded, offline, Blazor WASM & mobile `UseSqlite` is **tier-agnostic** — `AddRedb` gives Free, `AddRedbPro` gives Pro; you flip tiers without touching `using`s. Same LINQ, same 13-table model, same `IRedbService` API as Postgres/MSSql — only the connection line changes. Trees, grouping, window functions, dictionaries, `List`, soft-delete all work identically. ```csharp using redb.SQLite.Extensions; // Free // using redb.SQLite.Pro.Extensions; // Pro // Free — desktop / server / CI (native loadable C extension) builder.Services.AddRedb(options => options .UseSqlite("Data Source=app.db")); // extension path resolves from the package runtimes/ folder, or env REDB_SQLITE_EXTENSION // Pro — Blazor WebAssembly, mobile (MAUI), anywhere — pure C#, no native code builder.Services.AddRedbPro(options => options .UseSqlite("Data Source=app.db")); // no license key required (since 3.3.0) ``` - **Free** = native C extension (`redb.dll`/`.so`/`.dylib`) loaded per connection — hosts the query engine + `get_object_json`. Works wherever native code loads (desktop/server/CI), and is what a non-.NET host (Python, `sqlite3` CLI) calls to talk to a redb database directly. Min **SQLite 3.44+**. - **Pro** = no native dependency → the tier for **WASM/mobile**, where a native SQLite extension can't be loaded. Pro is free of charge — no license key required (since 3.3.0). Pro is proprietary (closed-source), but free to use. - Query *shape* is at parity across all three databases and both tiers; Free/Pro differ only in materialization & change-tracking — not in the query you get back. - `:memory:` works but is per-connection — use `Mode=Memory;Cache=Shared` + one kept-open connection so the pool sees one database (a SQLite lifetime quirk, not a redb one). --- ## Trees (recursive CTE) ```csharp [RedbScheme] public class CategoryProps { public string Title { get; set; } = ""; } // Build a tree (lowercase `name`, `parent_id` in initializer; capitalized when reading) var root = new RedbObject { name = "Root", Props = new() { Title = "Root" } }; await redb.SaveAsync(root); var child = new RedbObject { name = "Child", parent_id = root.Id, Props = new() { Title = "Child" } }; await redb.SaveAsync(child); // Pro: redb.CreateChildAsync(child, root) is a typed alternative for tree building. // All roots var roots = await redb.TreeQuery().WhereRoots().ToListAsync(); // All leaves var leaves = await redb.TreeQuery().WhereLeaves().ToListAsync(); // Everything under a node, max depth 3 var subtree = await redb.TreeQuery(rootObjectId: root.Id, maxDepth: 3).ToListAsync(); // Only nodes at level 2 var lvl2 = await redb.TreeQuery().WhereLevel(2).ToListAsync(); // Hierarchical predicate var underA = await redb.TreeQuery() .WhereHasAncestor(x => x.Name == "A") .ToListAsync(); // Materialize as TreeCollection (in-memory tree with .Roots / .Leaves / .Walk) var tree = await redb.TreeQuery().ToTreeAsync(); ``` ## Base-field vs Props operators cheatsheet Every filtering/ordering operator comes in **two flavours**: the unsuffixed one targets your `TProps` payload (translated to JSON path), the `*Redb`-suffixed one targets the base `IRedbObject` columns (`Id`, `Name`, `ParentId`, `OwnerId`, `WhoChangeId`, `DateCreate`, `DateModify`, `DateBegin`, `DateComplete`, `Hash`, `ValueLong/String/Guid/Bool/Double/Numeric/Datetime/Bytes`). | Operator on Props (`TProps`) | Operator on base (`IRedbObject`) | |----------------------------------------|----------------------------------------------| | `.Where(p => p.Total > 100)` | `.WhereRedb(x => x.DateCreate > cutoff)` | | — | `.WhereInRedb(x => x.Id, ids)` | | `.OrderBy(p => p.Total)` | `.OrderByRedb(x => x.DateCreate)` | | `.OrderByDescending(p => p.Total)` | `.OrderByDescendingRedb(x => x.Id)` | | `.ThenBy(p => p.Customer)` | `.ThenByRedb(x => x.Name)` | | `.WithWindow(w => w.OrderBy(p => p.Salary))` | `.WithWindow(w => w.OrderByRedb(x => x.DateCreate))` | Mistakes the compiler catches for you: ```csharp // WRONG — DateCreate is not on TProps; lambda is Func .OrderByDescending(x => x.DateCreate) // WRONG — Total is not on IRedbObject; lambda is Func .OrderByDescendingRedb(x => x.Total) // CORRECT .OrderByDescendingRedb(x => x.DateCreate) // base field .OrderByDescending(p => p.Total) // Props field ``` ## Window functions Real API uses TWO steps: configure the window on the queryable, then project with `Win.*` static helpers inside `SelectAsync`. There is **no** `SelectWindow` / fluent `w.RowNumber()` builder — those are common LLM hallucinations. ```csharp using redb.Core.Query.Aggregation; // for Win.* var windowQuery = redb.Query() .Take(50) .WithWindow(w => w .PartitionBy(x => x.Department) .OrderByDesc(x => x.Salary)); var ranked = await windowQuery.SelectAsync(x => new { Name = x.Props.FirstName, Department = x.Props.Department, Salary = x.Props.Salary, Rank = Win.RowNumber(), Running = Win.Sum(x.Props.Salary) // SUM() OVER (...) }); // Supported: Win.RowNumber, Win.Rank, Win.DenseRank, Win.Ntile, // Win.Sum, Win.Avg, Win.Min, Win.Max, Win.Count, // Win.Lag, Win.Lead, Win.FirstValue, Win.LastValue, frame clauses. ``` ## GroupBy + aggregation Uses static `Agg.*` helpers passed the group, NOT `g.Sum(...)` style. Terminal call is `SelectAsync` (not `.Select(...).ToListAsync()`). ```csharp using redb.Core.Query.Aggregation; // for Agg.* var byDept = await redb.Query() .GroupBy(x => x.Department) .SelectAsync(g => new { Department = g.Key, TotalSalary = Agg.Sum(g, x => x.Salary), AvgAge = Agg.Average(g, x => x.Age), Count = Agg.Count(g) }); ``` ## Arrays, dictionaries, lists & object references on Props Props are not limited to scalars — collections and references are first-class and filtered **server-side** (no in-memory `FirstOrDefault`). ```csharp [RedbScheme] public class EmployeeProps { public string Department { get; set; } = ""; public List Skills { get; set; } = new(); // array public Dictionary PhoneDirectory { get; set; } = new(); // dict (Pro) public Dictionary BonusByYear { get; set; } = new(); // dict, int key (Pro) public Dictionary OfficeLocations { get; set; } = new(); // dict of nested class (Pro) public RedbListItem? Status { get; set; } // item from a named RedbList public RedbObject? CurrentProject { get; set; } // reference to another object } // Arrays — Contains / arrayAny / arrayCount, translated to SQL (NOT loaded then filtered) .Where(e => e.Skills.Contains("C#")) // Dictionaries (Pro) — native LINQ ContainsKey / indexer / nested access, NOT key+value arrays .Where(e => e.PhoneDirectory.ContainsKey("desk")) .Where(e => e.BonusByYear[2023] > 6000m) .Where(e => e.OfficeLocations["HQ"].City == "New York") // Object reference — assign a saved RedbObject as a Prop; LoadAsync materializes it inline emp.Props.CurrentProject = project; // project is an already-saved RedbObject var loaded = await redb.LoadAsync(emp.Id); var city = loaded.Props.CurrentProject.Props.City; // whole graph, one load ``` ### Named lists (RedbList / RedbListItem) — shared lookup catalogues Lists are reference data (statuses, categories, tags) on a **separate `IListProvider`** — they are NOT methods on `IRedbService`. A `RedbListItem` can be stored as a Prop on any object and queried by its `.Value`. ```csharp // Create a list, then batch-add items (values + a parallel aliases list) var list = RedbList.Create("Statuses", "Order statuses"); var saved = await redb.ListProvider.SaveListAsync(list); await redb.ListProvider.AddItemsAsync(saved, new List { "Active", "Pending", "Blocked" }, new List { "Active status", "Pending review", "Blocked access" }); // Read back var byName = await redb.ListProvider.GetListByNameAsync("Statuses"); var items = await redb.ListProvider.GetListItemsAsync(byName!.Id); // Filter objects by a ListItem-typed Prop's value — server-side var active = await redb.Query() .Where(o => o.Status!.Value == "Active") .ToListAsync(); ``` ## Multi-tenant via `CacheDomain` `CacheDomain` is just a `string` property on `IRedbService`. Two service instances built against different connection strings get distinct `CacheDomain` values automatically, so the three-level cache (metadata / props / lists) is partitioned per service — no manual factory wiring required. ## Security context ```csharp redb.SetCurrentUser(user); // all subsequent calls respect permissions using (redb.CreateSystemContext()) // temporarily bypass checks { await redb.SaveAsync(internalObj); } ``` ## `.redb` portable export Export/Import live in the `redb.Export` package on a **separate `ExportService` / `ImportService`** — they are NOT methods on `IRedbService`. Filtering granularity is per-scheme (by scheme IDs), not per-object predicate. ```csharp // Export — by scheme IDs (null/empty = all schemes), optional ZIP compression await exportService.ExportAsync( outputPath: "backup.redb", schemeIds: null, // or new long[] { ordersSchemeId, customersSchemeId } compress: true, dryRun: false); // Import — file path, optional cleanup of target DB before import await importService.ImportAsync( inputPath: "backup.redb", clean: false, dryRun: false); // Works cross-DB (PG → MSSQL and vice versa) thanks to the JSONL header carrying provider info. ``` --- ## DO / DON'T patterns ```csharp // CORRECT: existence check — LoadAsync returns null only if the row is gone (hard-deleted / purged) var obj = await redb.LoadAsync(id); if (obj is null) { /* not found */ } else if (obj.IsSoftDeleted()) { /* in trash, can be restored */ } // CORRECT: typed batch load (cast required — different ids may have different schemes) var loaded = await redb.LoadAsync(new long[] { 1, 2, 3 }); var orders = loaded.OfType>().ToList(); // WRONG: there is no LoadManyAsync — the batch overload is just LoadAsync(IEnumerable) // CORRECT: batch save (single round trip, ~10× faster) await redb.SaveAsync(orders); // WRONG: N round trips foreach (var o in orders) await redb.SaveAsync(o); // CORRECT: order on BASE field — must use the *Redb-suffixed overload .OrderByDescendingRedb(x => x.DateCreate) // WRONG: .OrderByDescending(x => x.DateCreate) — that overload's lambda is Func, // and DateCreate isn't on TProps, so it won't compile. // CORRECT: order on PROPS field — unsuffixed overload .OrderByDescending(p => p.Total) // HARD delete (row gone from _objects, not recoverable) — used everywhere in redb.Examples await redb.DeleteAsync(id); // SOFT delete (re-parents under a trash node, recoverable until BackgroundDeletionService purges) await redb.SoftDeleteAsync(new[] { id }); // CORRECT: combine Props filter + base field filter redb.Query() .WhereRedb(x => x.DateCreate > cutoff) .Where(p => p.Status == "Active"); // WRONG: x.Id, x.Name etc inside Where(p => …) — they are NOT on TProps redb.Query().Where(p => p.Id == 1); // compile error // CORRECT: see what SQL is actually generated string sql = await redb.Query().Where(p => p.Total > 0).ToSqlStringAsync(); ``` --- ## redb.Route — Apache Camel-style integration DSL (v3.3.3) A pipeline is one source (`From`), processors in the middle, one or more sinks (`To`). Messages flow as `IExchange` carrying `Body`, `Headers`, `Properties`. ```csharp public class OrderRoutes : RouteBuilder { // NOTE: parameterless and PROTECTED. `public override void Configure(IRouteContext ctx)` // does not compile — the context is available as the protected `Context` property. protected override void Configure() { From("kafka://orders?groupId=svc&brokers=localhost:9092") .RouteId("order-pipeline") .Choice() .When(Header("priority").isEqualTo("high")) // lowercase `i` — really .Log("High priority order") .To("direct://fast-lane") .When(e => e.Message.GetHeader("priority") == "low") .To("seda://batch-queue") .Otherwise() .To("direct://standard") .EndChoice(); // typed terminator From("direct://fast-lane") .Retry(3) .SetHeader("processed-at", e => DateTimeOffset.UtcNow) .Process(async (exchange, ct) => { var body = exchange.Message.GetBody(); exchange.Message.SetBody($"PROCESSED: {body}"); }) .WireTap("seda://audit") // async fan-out copy .To("rabbitmq://processed?host=localhost"); } } // Registration — engine first, then one call per transport you actually use. builder.Services.AddRedbRoute(route => route.AddRouteBuilder()); builder.Services.AddRedbRouteKafka(); builder.Services.AddRedbRouteRabbitMQ(); ``` `When(...)` takes three forms: a lambda, an `IPredicate` (`Header("x").isEqualTo("y")`), or a string expression (`"header.type == 'order'"`). ### Transports (26 connector packages) `Amqp` · `AzureServiceBus` · `Elasticsearch` · `Exec` · `File` · `Firebase` · `Ftp` · `GenericFile` · `Grpc` · `Http` · `IbmMq` · `Kafka` · `Ldap` · `Mail` · `MqttNet` · `Quartz` · `RabbitMQ` · `Redis` · `S3` · `Sftp` · `SignalR` · `Sql` · `Sqs` · `Tcp` · `Telegram` · `WebSocket` — each a `redb.Route.` package with an `AddRedbRoute()` registration. Plus in-memory endpoints (`direct://`, `direct-vm://`, `seda://`) and the LLM transport below. ### EIP patterns Routing: `Choice`/`When`/`Otherwise`, `Filter`, `RecipientList`, `DynamicRouter`, `Split` (sub-route or scope style), `Aggregate`, `Resequence`, `Multicast`, `WireTap`, `Enrich`, `Loop`, `Delay`, `Debounce`, `Throttle`. Reliability: `Retry`, `OnException` (scope and configure forms), `TryCatch`, Dead Letter Channel, redelivery policies, `CircuitBreaker`, `IdempotentConsumer` (in-memory / SQL / redb-backed repositories), `Saga` with compensating steps, `Transacted`. Each scoped block has a typed terminator: `EndChoice()`, `EndSplit()`, `EndAggregate()`, `EndFilter()`, `EndCircuitBreaker()`, `EndIdempotentConsumer()`, … plus a generic `End()`. ### Concurrency (3.3.x) Two separate axes — do not confuse them: ```csharp // Processing-level: .Threads(N) EIP — fan a serial poll source out to a pool From("file://in").Threads(10).Process(HeavyTransform).To("http://api").End(); // Consumer-level: the transport's own competing-consumer knob From(Mqtt.Subscribe("telemetry/#").Qos(1).ConcurrentConsumers(5)).Process(Ingest).End(); ``` `.Threads(N)` is an async boundary like `seda://`: bounded backpressured queue, its own DI scope per exchange, graceful drain on stop, errors routed to `OnException`. **Ordering is not preserved when N > 1.** Anti-pattern in routes: ```csharp // WRONG: blocking call inside a route — kills throughput .Process(ex => { Thread.Sleep(1000); }) // CORRECT: async processor, or the Delay EIP .Delay(TimeSpan.FromSeconds(1)) ``` ## redb.Route.Llm — an LLM as an addressable endpoint The model is a transport, not a library you call: the same retry, throttle, circuit-breaker, tracing and dashboard machinery every other connector gets. ```csharp .From("kafka://orders") .To(Llm.Factory("claude").Temperature(0.2).MaxTokens(1024).AsUri()) .To("kafka://orders.translated"); ``` The body becomes the prompt; the assistant text replaces `exchange.Out.Body`; token usage, model id, stop reason and tool-iteration count land in headers. ```csharp services.AddRedbRoute(route => { route.Services.AddRedbRouteLlm(); // engine + in-memory state defaults route.Services.AddLlmConnectionFactory("claude", f => { f.Provider = "anthropic"; // openai | anthropic/claude | groq | cerebras | // openrouter | gemini | github-models | mistral | // together | huggingface | deepseek | ollama | // lmstudio | custom | stub f.ModelId = "claude-haiku-4-5"; f.ApiKeySecretRef = "anthropic.api-key"; f.Temperature = 0.2; f.MaxTokens = 1024; }); route.Services.AddRedbLlmStorage(); // opt-in: swap in-memory state for redb stores route.AddRouteBuilder(); }); ``` Four usage shapes: `To("llm://…")` as a pipeline step · `From("llm://…")` as a scheduled agent · inline `.Llm("factory", …)` sugar · and **tools**. **Tools are routes.** Any existing route — Kafka, HTTP, SQL, file, redb — becomes a callable tool by adding one DSL aspect **after `From(...)`**: ```csharp From("sql://select * from inventory where sku = :sku") .AsLlmTool("check_stock") // registers a bridge under this name .To("direct://done"); ``` When the model emits a `tool_use` block the engine invokes that route request/reply and hands whatever ends up in `Out.Body` back to the model. No per-connector LLM package exists or is needed. `AddRedbLlmStorage()` swaps the in-memory conversation transcripts, tool idempotency, approvals, cost ledgers and audit for redb-backed stores that survive restarts. ## redb.Tsak — Production runtime for routes (v3.3.3) Hosts redb.Route pipelines as hot-deployable modules. You publish a class library, pack it as a `.tpkg`, drop it into a scanned directory, and Tsak builds an `IRouteContext`, starts the routes and exposes lifecycle / metrics / logs over REST, the `tsak` CLI and a Blazor dashboard. ```bash # 1. Build + pack. A .tpkg is a plain ZIP: manifest.json (Name, Version, EntryPoints, # Dependencies) + the module DLLs. Class libraries need # true or transitive DLLs # are missing from the package. dotnet build -c Release ./Orders/Orders.csproj # 2. Deploy = a file copy into the scanned directory. In shipped releases and Docker images # that directory is `modules/` (configured by Tsak:Modules:AssemblyPaths, default # [ "modules" ]). `Libs/` is the DEV-repo / template default only — never bind-mount it in # a container: it hides the image's Libs/shared connector layer and the bundled # connectors break. cp ./Orders/output/Orders.tpkg /app/worker/modules/ cp ./Orders/context.json /app/worker/modules/ # module config sits next to the .tpkg # 3. Drive it tsak profile add --name prod --url http://localhost:9090 --key tsak_... tsak module list tsak context start orders tsak route list orders tsak system health ``` **Hot reload** — `HotReloadService` polls every `HotReload:ScanIntervalSeconds` (default 10) and compares write timestamps. Reloaded with the worker running: `.tpkg` / `.dll` replacement (graceful swap — new load context starts, in-flight messages drain, old context stops), deletion (unloads after `RemovalDebounceScans`, default 2), and edits to `context.json` / `{Module}.config.json` (the 5-layer config merge re-runs, the affected context restarts). Worker `appsettings.json` host wiring (API port, storage provider, cluster options) needs a worker restart. Each package loads into its own `ModuleAssemblyLoadContext`; a type-load failure in one package is logged and skipped without touching the others. The `_system` context can't be stopped, removed or hot-reloaded. **Configuration** merges 5 layers, lowest → highest: `Tsak:Contexts:default` → `Tsak:Contexts:{name}` → module `context.json` → module `{Module}.config.json` → `Tsak:Contexts:{name}:Override` (deep merge, nested objects merge recursively). **Deployment shape** is two independent switches, not three named modes: `Tsak:Storage:Type` = `InMemory` (default) or `Redb` (module + state stores persisted through redb; engine from `Tsak:Redb:Provider` = `postgres` / `mssql` / `sqlite`), and `Tsak:Cluster:Enabled`. Cluster + `InMemory` logs a warning — suspended-route state is lost on restart. `Tsak:Api:Enabled=false` gives a headless worker (no `_system` context, no ports); the API listens on `0.0.0.0:9090` by default. **REST API — 56 endpoints** across 14 controllers behind `Authorization: Bearer tsak_…` or `X-Api-Key`: `/api/contexts` (8), `/api/contexts/{name}/routes…` (8), `/api/scheduler` (7), `/api/users` (5), `/api/system` (4), `/api/watchdog` (4), `/api/cluster` (4, Pro), `/api/auth` (3), `/api/logs` (3), `/api/modules` (3), `/api/health` (3 — startup/live/ready), `/api/diagnostics` (2), `/api/dashboard` (1), `/api/lifecycle` (1). Plus an auth-exempt `/metrics` facade proxying the loopback OpenTelemetry Prometheus listener. **CLI — 44 commands** in 11 groups: `context` (6), `route` (6), `scheduler` (7), `profile` (4), `watchdog` (4), `diagnostics` (4), `system` (3), `module` (3), `auth` (3), `cluster` (3), `log` (1). Connection resolves from a YAML profile under `~/.tsak/profiles/`, `--url`, or `TSAK_URL` / `TSAK_API_KEY`. Note: **there is no `tsak module install`** — deploying a module is a file copy; the `module` group only lists, gets and removes. **Cluster (Pro)** — TTL-based leader election over a redb-backed distributed lock (`{GroupName}:leader`, TTL 30s) with a monotonic **epoch counter for fencing** against stale leaders. Nodes heartbeat every 15s, are declared dead after 60s. The leader writes per-node context assignments as redb-stored objects; each node reads only its own. `Cluster:Strategy` = `round-robin` (default) or `least-loaded`. Rolling updates are sequenced across nodes when `HotReload:RollingUpdate=true`. **Quartz** — an `IScheduler` is registered into every context; when the job store is an `AdoJobStore` the schema is created automatically before the scheduler starts (per-provider scripts for PostgreSQL / SQL Server / SQLite). Cron inside a route is just an endpoint: `From("cron:0 */5 * * * ?")`. --- ## redb.Identity — OAuth 2.1 / OpenID Connect server (v1.2.2) A standards-compliant identity provider built on **OpenIddict + redb.Route**, storing every user, client, token, session, key and audit row as typed redb objects. **No EF Core, no migrations, no ASP.NET Identity.** Apache 2.0. The defining property: **it is not an HTTP server that happens to speak OIDC — it is an OIDC engine that happens to have an HTTP facade.** All 67 endpoints are registered as `direct-vm://identity-*` routes; HTTP is one adapter among several. ### Packages | Package | Purpose | |---|---| | `redb.Identity.Core` | The engine — OpenIddict pipeline, redb stores, MFA, WebAuthn, federation, signing keys | | `redb.Identity.Http` | HTTP facade — discovery, token, authorize, userinfo, introspect, JWKS, PAR, DCR, SCIM, `/me`, admin API | | `redb.Identity.Contracts` | Wire DTOs + route-name constants (`IdentityEndpoints`), shared by Core / Http / Client | | `redb.Identity.Client` | Typed SDK (`IIdentityClient`) + backchannel OIDC client | | `redb.Identity.DataProtection` | redb-backed ASP.NET Core DataProtection key-ring | | `redb.Identity.Core.Module` | redb.Tsak `.tpkg` host glue (only when hosting in a Tsak worker) | | `redb.Identity.Ldap` | LDAP / Active Directory federation provider | | `redb.Identity.Resource.Dpop` | Resource-server DPoP (RFC 9449) validator for downstream APIs | The provider is chosen by the **host**, not by Identity: add `redb.Postgres[.Pro]`, `redb.MSSql[.Pro]` or `redb.SQLite[.Pro]` and the same code runs unchanged. ### The in-process call — the thing to remember Another module in the same Tsak worker gets a token with **no socket, no TLS, no JSON over loopback** — the exchange flows straight into the Identity pipeline and back: ```csharp public class CheckoutRoutes : RouteBuilder { private IProducerTemplate _identity = null!; protected override void Configure() { _identity = new ProducerTemplate(Context!); _identity.Start(); From("rabbitmq:checkout.orders") .Process(async (e, ct) => { var token = await _identity.RequestBody( IdentityEndpoints.Token, // "direct-vm://identity-token" new { grant_type = "client_credentials", client_id = "checkout-svc" }); e.In.SetHeader("Authorization", $"Bearer {token!.AccessToken}"); }) .To("http://pricing-api/quote"); } } ``` Only two interactions fundamentally need a browser: the `/connect/authorize` redirect and the device-code verification page. `token`, `refresh_token`, `introspect`, `revoke`, `userinfo`, device-code request, admin API, MFA verify and SCIM are transport-neutral. ### Endpoint surfaces - **OAuth 2.1 / OIDC** (`/connect/*`, `/.well-known/*`) — `token`, `authorize`, `par`, `userinfo`, `introspect`, `revocation`, `logout`, `register` (+ `/{client_id}` management), `deviceauthorization`, `device/verify`, `openid-configuration`, `oauth-authorization-server`, `jwks`. - **Admin REST** (`/api/v1/identity/*`, Bearer + `identity:manage`) — applications, users, groups, roles, scopes, claim-definitions, claim-mappers, claim-scopes, tokens, consents, sessions, mfa, federation-providers, signing-keys, webhooks, audit, revoked-sids, impersonation. - **Self-service** (`/me/*`, Bearer + `identity:account`) — profile, password, sessions, mfa, webauthn, consents, federated-identities, e-mail verification, change-e-mail. Every route is guarded so an `identity:account` token can never touch another user's data. - **Account flows** — self-registration, e-mail verification, password recovery / reset. - **SCIM 2.0** (`/scim/v2/*`) — `Users`, `Groups`, `Bulk`, plus `ServiceProviderConfig` / `ResourceTypes` / `Schemas` discovery. ### Storage — 32 typed redb schemes, zero migrations All declared with `[RedbScheme("identity.*")]`: | Domain | Schemes | |---|---| | OAuth / OIDC core | `application`, `scope`, `token`, `authorization`, `session`, `idempotency_record` | | Users & groups | `user`, `group`, `group_member`, `password_history` | | RBAC | `role`, `user_role_assignment`, `group_role_assignment`, `role_scope_assignment` | | Claims engine | `claim_definition`, `claim_mapper`, `claim_scope`, `claim_scope_assignment` | | MFA / WebAuthn | `mfa`, `mfa_otp`, `webauthn_consumed_challenge` | | Federation | `federation_provider`, `federated_identity` | | Account flows | `email_verification_token`, `password_reset_token`, `change_email_token` | | DPoP / cluster / crypto | `dpop_consumed_jti`, `revoked_sid`, `system_flag`, `signing_key`, `dp_key` | | Audit & webhooks | `audit_event`, `webhook_subscription` | Adding a claim is adding a C# property — the scheme syncs on the next `InitializeAsync()`. `Dictionary? CustomClaims` on `UserProps` gives per-tenant custom claims that stay **queryable** as ordinary props rows, not an opaque JSON blob. ### Standards OIDC Core 1.0 · OIDC Discovery · RP-Initiated + Backchannel Logout · OAuth 2.1 (PKCE required) · RFC 6749 / 6750 / 7009 (revocation) / 7591-7592 (DCR) / 7636 (PKCE) / 7662 (introspection) / 8414 (metadata) / 8628 (device code) / 9126 (PAR) / 9449 (DPoP) · SCIM 2.0 (RFC 7643/7644) · TOTP (RFC 6238) · WebAuthn L2 / FIDO2 · **40 RFCs referenced by number in code**. Verified against the **official OpenID Foundation conformance suite** (Config OP: 0 failures; Basic OP code-flow modules pass). Not implemented yet: RFC 8705 (mTLS), 9068 (`at+jwt`), 9101 (JAR), 9207, 9396 (RAR), 9470 (step-up), FAPI 2.0, CIBA, OIDC Federation. ### Operations - **Cluster-ready.** DataProtection key-ring and RSA-2048 signing keys live in redb and are shared by every replica; first-key generation is under a distributed lock. Cleanup timers are registered `.Cluster(true)` → leader-only. - **Observability.** OpenTelemetry meter `RedbIdentity` (login attempts/failures, MFA verifications/lockouts, tokens issued/errors, rate-limit rejections, password-verify histogram), a dedicated `RedbIdentity.Security` log category for SIEM, and health probes (`db`, `signing-keys`, `data-protection`) under Tsak's aggregated health API. - **Audit.** 79 typed events across authentication / authorization / admin / federation / mfa / scim / system, written to redb and optionally multicast to Kafka / Elasticsearch / RabbitMQ / log. - **Secrets** never live in config files — only Tsak L5 env overrides (`Tsak__Contexts__identity__Override__Identity__*`). - **Tests:** 1767 passing / 1 skipped, the same suite green on PostgreSQL, MSSQL and SQLite by flipping `REDB_PROVIDER`. 63 runnable end-to-end demo scripts in `demos/`. ### Calling it from a service (typed SDK) ```csharp services.AddIdentityClient(options => { options.BaseUrl = "https://identity.local"; options.AccessTokenProvider = new ClientCredentialsAccessTokenProvider( clientId: "my-svc", clientSecret: config["IdentitySvcSecret"]!, scopes: ["identity:manage"]); }); // Grouped by area: Users, Groups, Applications, Scopes, ClaimMappers, Federation, // Sessions, Tokens, Mfa, Audit, Account, Token, Scim, Info, RevokedSids. var page = await identity.Users.ListAsync(new UsersListRequest { Limit = 100 }, ct); ``` Errors are normalized to RFC 7807 `ProblemDetails` and thrown as `ApiException`. **Deep contract:** https://redb.ru/llms-identity.txt --- ## Real benchmarks (hardware reference) All published numbers on https://redb.ru/results are measured on: - **CPU:** AMD Ryzen 5 5500U, 4 cores allocated - **RAM:** 4 GB per Docker container - **DB:** PostgreSQL 18 + SQL Server Developer - **Data:** ~15 000 objects, ~1 200 000 property rows Since 3.0.0 Free and Pro emit the same PVT CTE shape (`array_agg … FILTER` collapses N property rows into one pivot pass) — query parity, identical index path and join shape. Pro's edge is **plan-cache stability**: values arrive as `$N` parameters, so PostgreSQL reuses one prepared plan across calls; Free bakes values in as `%L` literals so each call gets a fresh plan. Batch save of 100 objects is ~10× faster than sequential `SaveAsync` per object (Free and Pro both). --- ## Docs (extended reading — load these for deep questions) - [Architecture deep dive](https://redb.ru/architecture) - [Quick Start](https://redb.ru/quick-start) - [Setup](https://redb.ru/setup) - [Why Redbase](https://redb.ru/why-redbase) - [Products](https://redb.ru/products) — Free vs Pro feature matrix - [Examples index (runnable)](https://redb.ru/examples) - [Benchmark results](https://redb.ru/results) - [CLI reference](https://redb.ru/cli) - [API reference (DocFX)](https://redbase-app.github.io/redb/api/index.html) - [FAQ](https://redb.ru/faq) Product pages: [redb (storage)](https://redb.ru/redbase) · [redb.Route](https://redb.ru/route) · [redb.Tsak](https://redb.ru/tsak) · [redb.Identity](https://redb.ru/identity) ## Companion LLM contracts (same format as this file, one product each) - [llms-identity.txt](https://redb.ru/llms-identity.txt) — full redb.Identity contract: endpoint catalogue, storage schemes, config keys, SDK, audit events ## Source - GitHub (core + examples): https://github.com/redbase-app/redb - GitHub (route): https://github.com/redbase-app/redb-route - GitHub (tsak): https://github.com/redbase-app/redb-tsak - NuGet (identity): [redb.Identity.Core](https://www.nuget.org/packages/redb.Identity.Core) · [redb.Identity.Http](https://www.nuget.org/packages/redb.Identity.Http) · [redb.Identity.Client](https://www.nuget.org/packages/redb.Identity.Client) - NuGet: [all redb.* packages](https://www.nuget.org/packages?q=redb.) ## Optional - [Examples folder on GitHub (E000–E200)](https://github.com/redbase-app/redb/tree/main/redb.Examples) — every public LINQ feature has a numbered, runnable example - [Russian site](https://redb.ru) — same content as redbase.app