Skip to content
Client v5: BLE, BLE Hosting, HTTP, Jobs - Linux, MacOS, & Blazor Support! Full AOT, RX on BLE only & MANY other features! Power up!

Typed Context

DocumentContext is an optional, EF-Core-style typed front-end over IDocumentStore. Instead of re-typing <T> and remembering the JsonTypeInfo<T> at every call site, you declare your aggregates once on a partial context and work against discoverable DocumentSet<T> properties. A source generator bundled in the core Shiny.DocumentDb package emits the sets, the configuration lowering, and a DI extension — all compile-time and AOT-clean.

// store-first
var adults = await store.Query<User>().Where(x => x.Age > 18).ToList();
await store.Insert(user, AppJsonContext.Default.User);
// context (this feature) — model-first, JsonTypeInfo threaded for you
var adults = await db.Users.Where(x => x.Age > 18).ToList();
await db.Users.Insert(user);
Terminal window
dotnet add package Shiny.DocumentDb

That’s all you need. DocumentContext and DocumentSet<T> are runtime types in the core Shiny.DocumentDb package, and the source generator that emits your sets ships inside that same package (bundled under analyzers/dotnet/cs) — there is no separate generator package to install.

Derive a partial class from DocumentContext and decorate it with [Document(typeof(T))] per aggregate.

[Document(typeof(User), Id = nameof(User.Email), JsonContext = typeof(AppJsonContext))]
[Document(typeof(Order), Table = "orders", JsonContext = typeof(AppJsonContext))]
public partial class AppContext : DocumentContext;

[Document] lowers into the same DocumentStoreOptions.Map* calls you’d write by hand:

PropertyEffectDefault
TableBacking table/collection nameTypeNameResolution
IdId property name (e.g. nameof(User.Email))convention (Id)
SetGenerated set property namepluralized type name (UserUsers)
SerializationHow the type’s JsonTypeInfo resolvesAuto
JsonContextYour JsonSerializerContext for AOT-safe metadatanone

The generator emits the other half of the partial — a DocumentSet<T> per type, a ConfigureModel lowering, and a DI extension:

// <auto-generated/>
partial class AppContext
{
public AppContext(IDocumentStore store) : base(store) { } // emitted if you didn't write one
public DocumentSet<User> Users => /* ... */;
public DocumentSet<Order> Orders => /* ... */;
internal static void ConfigureModel(DocumentStoreOptions options) { /* Map* + resolver wiring */ }
}
public static class AppContextRegistration
{
// Scoped context (ASP.NET Core request scopes)
public static IServiceCollection AddAppContext(this IServiceCollection services, Action<DocumentStoreOptions> configure);
// Singleton IDocumentContextFactory<AppContext> (MAUI / Blazor / desktop — no ambient scope)
public static IServiceCollection AddAppContextFactory(this IServiceCollection services, Action<DocumentStoreOptions> configure);
}
builder.Services.AddAppContext(o =>
{
o.DatabaseProvider = new SqliteDatabaseProvider("Data Source=app.db");
o.UseReflectionFallback = false; // strict AOT — JsonContext mode carries everything
});
// inject AppContext anywhere (registered scoped, like a DbContext)
public class UserService(AppContext db)
{
public Task<IReadOnlyList<User>> Adults() => db.Users.Where(u => u.Age >= 18).ToList();
}

DocumentContext only needs an IDocumentStore, so it works over any provider — including the ones with their own options type (LiteDB, MongoDB, Cosmos): construct that store yourself and pass it to the context. The generated ConfigureModel / AddAppContext target the relational DocumentStoreOptions.

Each set forwards to the store with its JsonTypeInfo pre-applied:

// queries — return IDocumentQuery<T> as-is, so the full surface is available
IDocumentQuery<User> q = db.Users.Query();
var adults = await db.Users.Where(u => u.Age >= 18).OrderBy(u => u.Name).ToList();
var alice = await db.Users.Get("alice@x.com");
var count = await db.Users.Count();
// immediate writes
await db.Users.Insert(user);
await db.Users.Update(user);
await db.Users.Upsert(patch);
await db.Users.Remove("alice@x.com");
// batch writes (atomic where the provider supports it)
await db.Users.BatchInsert(users);
// the context IS the unit of work — buffer with Add/Update/Upsert/Remove, commit atomically
db.Add(order).Remove<User>("old@x.com");
await db.SaveChanges();
// explicit transaction (relational) — locking reads via the raw session, then commit
await using var tx = await db.BeginTransaction();
var u = await db.Session.Get<User>("u1", LockMode.Update);
db.Update(u);
await db.SaveChanges(); // flushes into the transaction
await tx.Commit();

The typed sets (db.Users.Insert(…)) are immediate; the context’s own Add/Update/Upsert/Remove + SaveChanges are the buffered unit-of-work path. Reach the raw IDocumentSession via db.Session and the root IDocumentStore via db.Store.

Because the set returns the store’s IDocumentQuery<T> unchanged, Select, Paginate, aggregates, and the spatial/vector/full-text terminators all come for free — see Querying.

End-to-end: two aggregates, a context, both registration styles, and the three ways to write.

// ── models ──────────────────────────────────────────────────────────────
public class User { public string Id { get; set; } = ""; public string Name { get; set; } = ""; public int Age { get; set; } }
public class Order { public string Id { get; set; } = ""; public string UserId { get; set; } = ""; public decimal Total { get; set; } }
// ── context: one [Document] per aggregate; the generator emits db.Users / db.Orders ──
[Document(typeof(User))]
[Document(typeof(Order))]
public partial class AppDb : DocumentContext;
// Program.cs — registered scoped, one context per request
builder.Services.AddAppDb(o =>
o.DatabaseProvider = new PostgreSqlDatabaseProvider(connectionString));
// inject the context directly — the container disposes it at end of request
app.MapPost("/users", async (User user, AppDb db) =>
{
await db.Users.Insert(user); // immediate
return Results.Created($"/users/{user.Id}", user);
});
app.MapGet("/users/{id}", async (string id, AppDb db) =>
await db.Users.Get(id) is { } u ? Results.Ok(u) : Results.NotFound());
// ── the three ways to write ─────────────────────────────────────────────
await using var db = dbf.Create();
// (a) immediate — typed sets commit each call
await db.Users.Insert(new User { Id = "u1", Name = "Allan", Age = 41 });
// (b) unit of work — buffer on the context, commit atomically (all-or-nothing)
db.Add(new Order { Id = "o1", UserId = "u1", Total = 50 })
.Add(new Order { Id = "o2", UserId = "u1", Total = 75 });
await db.SaveChanges();
// (c) explicit transaction — locking read + grouped writes (relational providers)
await using var tx = await db.BeginTransaction();
var user = await db.Session.Get<User>("u1", LockMode.Update);
user.Age++;
db.Update(user);
await db.SaveChanges(); // flushes into the transaction (no commit yet)
await tx.Commit();
You wantUse
Readdb.Users.Get(id), db.Users.Where(…).ToList(), db.Users.Count()
Immediate writedb.Users.Insert / Update / Upsert / Remove(…)
Atomic multi-writedb.Add(x).Add(y); await db.SaveChanges();
Transaction / locking / isolationawait using var tx = await db.BeginTransaction(IsolationLevel.Snapshot);await tx.Commit();
Escape hatchdb.Session (raw IDocumentSession), db.Store (root IDocumentStore)

No change tracking or identity map — a document store embeds, it does not join.

Serialization chooses how each type’s JsonTypeInfo<T> resolves. AOT is the goal; reflection is the explicit opt-out. See AOT Setup for the full comparison.

ModeAOT-safe?Notes
Auto (default)Yes, if a context is registeredInherits the store’s resolver, else reflection fallback.
JsonContextYesPoint JsonContext = typeof(MyJsonCtx) at your JsonSerializerContext. Recommended for AOT.
ReflectionNoExplicit opt-out for non-AOT apps that won’t maintain a context.
GeneratedYesThe generator emits the metadata-mode JsonTypeInfo for you — AOT-safe with no JsonSerializerContext. Supported subset below.

[Document(typeof(T), Serialization = DocumentSerialization.Generated)] makes the generator emit an IJsonTypeInfoResolver that builds T’s metadata (and its whole reachable type closure) via JsonMetadataServices — the same AOT-safe shape System.Text.Json’s own generator produces — so you get AOT serialization from the single [Document] list, no hand-written JsonSerializerContext.

Supported per-type: a POCO with a public parameterless constructor and settable public properties whose types are JSON primitives, Guid/date-time types, enums, nullable value types, nested supported objects, List<T>, or T[]. [JsonPropertyName] and [JsonIgnore] are honored. Anything outside that subset (records / parameterized constructors, init-only or get-only members, dictionaries, interfaces) raises DDB005 — use JsonContext for those types.

IdMeaning
DDB001A [Document] type is not declared partial.
DDB002A [Document] type does not derive from DocumentContext.
DDB003Two [Document] declarations resolve to the same set name — set Set = on one.
DDB005A Generated type (or something in its closure) is outside the supported subset — use JsonContext.