Skip to content
Shiny Controls v1.0 - The Ultra Control Suite for .NET MAUI & BlazorO...M...G!

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:

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 (UserUsers)
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);
}
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 want Use
Read db.Users.Get(id), db.Users.Where(…).ToList(), db.Users.Count()
Immediate write db.Users.Insert / Update / Upsert / Remove(…)
Atomic multi-write db.Add(x).Add(y); await db.SaveChanges();
Transaction / locking / isolation await using var tx = await db.BeginTransaction(IsolationLevel.Snapshot);await tx.Commit();
Escape hatch db.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.

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.

[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.

A property whose type carries a type-level [JsonConverter] is emitted as a value using that converter rather than being walked as an object — so converter-backed types work even when they’re immutable or abstract. This is how GeoPoint (an immutable struct), GeoPoint?, and Geometry (an abstract polymorphic base) serialize as GeoJSON under Generated.

The converter must be public, non-abstract, have a public parameterless constructor, and derive from JsonConverter<T> where T is exactly the declared member type. The generated resolver lives in your assembly and constructs the converter directly, so:

  • An inaccessible converter raises DDB005 — make it public.
  • A JsonConverterFactory is not supported (raises DDB005).
  • Declare the member as the type the converter converts. A JsonConverter<Geometry> cannot produce metadata for a derived GeoPolygon member — type the property as Geometry.
  • Member-level [JsonConverter] (on the property rather than its type) is not supported and raises DDB005; move the attribute onto the type, or use a non-Generated mode.
  • A document type itself cannot carry a type-level [JsonConverter] — the query translator needs the document to serialize as a JSON object.
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.