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-firstvar 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 youvar adults = await db.Users.Where(x => x.Age > 18).ToList();await db.Users.Insert(user);Install
Section titled “Install”dotnet add package Shiny.DocumentDbdotnet add package Shiny.DocumentDb.GeneratorsShiny.DocumentDb.Generators is an analyzer-only package (no runtime assembly). DocumentContext and
DocumentSet<T> live in the core Shiny.DocumentDb package.
Declare a context
Section titled “Declare a context”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:
| Property | Effect | Default |
|---|---|---|
Table | Backing table/collection name | TypeNameResolution |
Id | Id property name (e.g. nameof(User.Email)) | convention (Id) |
Set | Generated set property name | pluralized type name (User → Users) |
Serialization | How the type’s JsonTypeInfo resolves | Auto |
JsonContext | Your JsonSerializerContext for AOT-safe metadata | none |
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);}Register & use
Section titled “Register & use”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();}// MAUI / Blazor / desktop have no per-request scope. Register a singleton factory and// create a short-lived context per unit of work — the parallel of EF's IDbContextFactory<T>.builder.Services.AddAppContextFactory(o =>{ o.DatabaseProvider = new SqliteDatabaseProvider($"Data Source={dbPath}"); o.UseReflectionFallback = false;});
// inject the factory anywhere — even into a singleton page/view-modelpublic class UserViewModel(IDocumentContextFactory<AppContext> factory){ public async Task<IReadOnlyList<User>> Adults() { var db = factory.Create(); // cheap; a facade over the shared store, no disposal needed return await db.Users.Where(u => u.Age >= 18).ToList(); }}var opts = new DocumentStoreOptions { DatabaseProvider = new SqliteDatabaseProvider("Data Source=app.db") };AppContext.ConfigureModel(opts);var db = new AppContext(new DocumentStore(opts));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.
DocumentSet<T>
Section titled “DocumentSet<T>”Each set forwards to the store with its JsonTypeInfo pre-applied:
// queries — return IDocumentQuery<T> as-is, so the full surface is availableIDocumentQuery<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 writesawait 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 transactionvar 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 modes
Section titled “Serialization modes”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.
| Mode | AOT-safe? | Notes |
|---|---|---|
Auto (default) | Yes, if a context is registered | Inherits the store’s resolver, else reflection fallback. |
JsonContext | Yes | Point JsonContext = typeof(MyJsonCtx) at your JsonSerializerContext. Recommended for AOT. |
Reflection | No | Explicit opt-out for non-AOT apps that won’t maintain a context. |
Generated | Yes | The generator emits the metadata-mode JsonTypeInfo for you — AOT-safe with no JsonSerializerContext. Supported subset below. |
Generated mode
Section titled “Generated mode”[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.
Diagnostics
Section titled “Diagnostics”| Id | Meaning |
|---|---|
DDB001 | A [Document] type is not declared partial. |
DDB002 | A [Document] type does not derive from DocumentContext. |
DDB003 | Two [Document] declarations resolve to the same set name — set Set = on one. |
DDB005 | A Generated type (or something in its closure) is outside the supported subset — use JsonContext. |