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. The Shiny.DocumentDb.Generators package source-generates 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
dotnet add package Shiny.DocumentDb.Generators

Shiny.DocumentDb.Generators is an analyzer-only package (no runtime assembly). DocumentContext and DocumentSet<T> live in the core Shiny.DocumentDb package.

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);
// explicit transaction
var uow = db.CreateUnitOfWork();
uow.Add(order);
uow.Remove<User>("old@x.com");
await uow.SaveChanges();

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.

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.