Skip to content
Shiny.NET
Shiny MAUI Shell v7 - App Links, App Shortcuts, & Navigation Interception!Shortcut me to it

What DocumentDb Does

Most apps end up with the same data layer problems: an object graph that doesn’t fit flat tables, a migration for every new field, then search, audit history, encryption, events and an API bolted on as separate libraries. Shiny.DocumentDb gives you all of that as one library on top of the database you already run. This page shows what it does, one capability at a time, with the code for each.

A document is your object — nested addresses, child collections and all — saved as one JSON document. There is no CREATE TABLE, no mapping, and no migration when you add a property. Queries are ordinary LINQ, including over nested collections, and are translated to the database’s own query language rather than run in memory.

public class Order
{
public Guid Id { get; set; } // generated on insert
public string Customer { get; set; } = "";
public string Status { get; set; } = "open";
public Address ShipTo { get; set; } = new();
public List<OrderLine> Lines { get; set; } = [];
}
await store.Insert(order); // the whole graph, one write
var bigOpenOrders = await store.Query<Order>()
.Where(o => o.Status == "open" && o.Lines.Any(l => l.Price > 100))
.OrderBy(o => o.Customer)
.Paginate(0, 20)
.ToList();

Projections, aggregates, GroupBy, bulk ExecuteUpdate / ExecuteDelete, JSON indexes, and units of work are all on the same fluent surface. The same code runs on every provider — only the registration changes.

CRUD · Querying · Aggregates · Indexes & transactions · Typed context

Three kinds of search, each backed by the database’s own index — pgvector, DiskANN, FTS5, tsvector, GiST, R*Tree, 2dsphere and friends — not a second search service you have to keep in sync.

Find the documents closest in meaning. Pair it with Microsoft.Extensions.AI and embeddings are generated on every write.

opts.ConfigureDocument<Article>(cfg =>
cfg.MapVectorProperty(a => a.Embedding, dimensions: 1536, metric: VectorDistance.Cosine));
var hits = await store.Query<Article>()
.Where(a => a.Tenant == tenantId)
.NearestVectors(queryEmbedding, k: 10);

Vector / ANN search · VectorData connector

Know what a document looked like last Tuesday, undo a bad write, keep deleted rows recoverable, and make sure a leaked backup doesn’t leak the sensitive fields.

Opt a type into append-only versioning; every write records a snapshot.

opts.ConfigureDocument<Order>(cfg => cfg.MapTemporal(o => o.Retention = TimeSpan.FromDays(90)));
var temporal = (ITemporalDocumentStore)store;
var versions = await temporal.History<Order>(orderId); // every version, oldest first
var lastWeek = await temporal.AsOf<Order>(orderId, when); // state at a point in time

Diff two versions, restore one, or ask what a given user changed. Temporal history

“Save the order, then publish OrderPlaced” is two writes, and one of them eventually fails. The outbox commits the message in the same transaction as the document, and change feeds let anything react to writes.

await using var session = store.OpenSession();
session.Add(order)
.Enqueue(new OrderPlaced(order.Id, order.Total));
await session.SaveChanges(); // the order and the message commit together, or neither does

At-least-once dispatch with retries, backoff and dead-lettering. Transactional outbox

The document model you already have becomes an API — for browsers, for integrations, and for LLM agents — with the scoping rules enforced by the library, not by each endpoint.

List, get, count, create, replace, merge-patch, delete, and a Server-Sent Events live tail — in one call.

app.MapDocuments<Order>("/orders", o =>
{
o.Operations = DocumentEndpoints.All;
o.AllowFilterOn(x => x.Status, x => x.CustomerId, x => x.Total);
o.Scope<ITenantContext>((tenant, _) => x => x.TenantId == tenant.TenantId);
});

REST & live queries · OData

From one SQLite file to a tenant-per-database cluster behind Orleans — without rewriting the data layer.

services.AddSingleton<ITenantResolver, MyTenantResolver>();
services.AddMultiTenantDocumentStore(tenantId => new DocumentStoreOptions
{
DatabaseProvider = new SqliteDatabaseProvider($"Data Source={tenantId}.db")
});

Shared-table or database-per-tenant, applied transparently. Multi-tenancy

ShinyDocDbMyAdmin is a phpMyAdmin-style front end for any DocumentDb store: browse and edit documents, run queries and EXPLAIN, diff temporal versions, map geometry, inspect vectors, generate test data, and ask an AI assistant about your data. It ships as a container image and as a terminal UI.

ShinyDocDbMyAdmin browsing documents in a sampled-column grid

Admin UI · Terminal UI · Try the live demo