Skip to content
Document DB v12 - Improved Interceptors with Soft Delete Integration, AI protections, & Admin UI with Aspire Integration!How!?

AOT Setup

For AOT/trimming compatibility, create a source-generated JSON context:

[JsonSerializable(typeof(User))]
[JsonSerializable(typeof(Order))]
[JsonSerializable(typeof(Address))]
[JsonSerializable(typeof(OrderLine))]
public partial class AppJsonContext : JsonSerializerContext;

Create an instance with your desired options and attach it to the store:

var ctx = new AppJsonContext(new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
});
var store = new SqliteDocumentStore(new DocumentStoreOptions
{
DatabaseProvider = new SqliteDatabaseProvider("Data Source=mydata.db"),
JsonSerializerOptions = ctx.Options,
UseReflectionFallback = false // recommended for AOT
});

If your types are spread across multiple JsonSerializerContext classes, use TypeInfoResolverChain to combine them. The chain is tried in order — the first context that knows about the requested type wins.

var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
options.TypeInfoResolverChain.Add(UserJsonContext.Default);
options.TypeInfoResolverChain.Add(OrderJsonContext.Default);
var store = new DocumentStore(new DocumentStoreOptions
{
DatabaseProvider = new SqliteDatabaseProvider("Data Source=mydata.db"),
JsonSerializerOptions = options,
UseReflectionFallback = false
});

All JsonTypeInfo<T> parameters across the entire API are optional (= null default). When omitted, type info is automatically resolved from the configured JsonSerializerOptions.TypeInfoResolver. You can configure a JsonSerializerContext once and skip passing JsonTypeInfo<T> on every call — while retaining full AOT safety.

With explicit JsonTypeInfo<T> With auto-resolution (recommended)
store.Insert(user, ctx.User) store.Insert(user)
store.Get("id", ctx.User) store.Get<User>("id")
store.Upsert(patch, ctx.User) store.Upsert(patch)
store.SetProperty("id", (User u) => u.Age, 31, ctx.User) store.SetProperty<User>("id", u => u.Age, 31)
store.RemoveProperty("id", (User u) => u.Email, ctx.User) store.RemoveProperty<User>("id", u => u.Email)

| store.Query(ctx.User) | store.Query<User>() | | store.Query<User>("sql", ctx.User, parms) | store.Query<User>("sql", parameters: parms) | | store.QueryStream<User>("sql", ctx.User, parms) | store.QueryStream<User>("sql", parameters: parms) |

// All of these are AOT-safe when ctx.Options is configured
var user = new User { Id = "alice-1", Name = "Alice", Age = 25 };
await store.Insert(user); // Id auto-generated for Guid/int/long; string Ids must be set
var fetched = await store.Get<User>(user.Id);
var all = await store.Query<User>().ToList();
await store.Upsert(new User { Id = user.Id, Name = "Alice", Age = 30 });
var results = await store.Query<User>(
"json_extract(Data, '$.age') > @minAge",
parameters: new { minAge = 30 });
await foreach (var u in store.Query<User>().ToAsyncEnumerable())
Console.WriteLine(u.Name);

By default (UseReflectionFallback = true), if no resolver is configured or the type isn’t registered, methods fall back to reflection-based serialization. This preserves backwards compatibility.

For AOT deployments, set UseReflectionFallback = false. Reflection-based serialization produces hard-to-diagnose errors under trimming and AOT. With this flag disabled, you get a clear InvalidOperationException at the point of use:

InvalidOperationException: No JsonTypeInfo registered for type 'MyApp.UnregisteredType'.
Register it in your JsonSerializerContext or pass a JsonTypeInfo<UnregisteredType> explicitly.

This tells you exactly which type is missing and what to do about it. Every type must either be registered in your JsonSerializerContext via [JsonSerializable(typeof(T))] or passed with an explicit JsonTypeInfo<T> parameter.

Strongly-typed DocumentContext serialization modes

Section titled “Strongly-typed DocumentContext serialization modes”

The strongly-typed DocumentContext (source-generated typed sets over the store) declares its document types with [Document(typeof(T))]. Each declaration chooses how that type’s JsonTypeInfo<T> is resolved via DocumentSerialization — AOT is the goal, but reflection stays available for callers who opt out:

Mode AOT-safe? When to use
DocumentSerialization.Auto (default) Yes, if a context is registered Inherit the store — uses a registered JsonSerializerContext if present, else reflection fallback. Register a context and you get AOT safety with no extra annotation.
DocumentSerialization.JsonContext Yes You own serialization. Point JsonContext = typeof(MyJsonCtx) at your JsonSerializerContext; its source-generated metadata drops into the store’s resolver chain. Recommended for AOT.
DocumentSerialization.Reflection No Explicit opt-out for non-AOT apps that won’t maintain a context. Convenient, not trimming-safe.
DocumentSerialization.Generated Yes The generator emits the metadata-mode JsonTypeInfo for you — AOT-safe with no hand-written JsonSerializerContext. Covers POCOs (parameterless ctor + settable props) of primitives, enums, nullable value types, nested objects, List<T>, arrays, and types with a public type-level [JsonConverter] (e.g. GeoPoint, Geometry); anything outside that subset raises DDB005 (use JsonContext).
[Document(typeof(User), JsonContext = typeof(AppJsonContext))] // AOT-safe — you own STJ
[Document(typeof(Product), Serialization = DocumentSerialization.Generated)] // AOT-safe — generator owns metadata
[Document(typeof(Order))] // Auto — inherits the store
[Document(typeof(AuditLog), Serialization = DocumentSerialization.Reflection)] // explicit non-AOT opt-out
public partial class AppContext : DocumentContext;

All features are fully AOT/trimming-safe:

Feature AOT strategy
Table-per-type mapping Type name resolved at registration time via TypeNameResolver.Resolve(). No runtime reflection.
Custom Id property Property name extracted from expression at registration time. No MemberInfo at runtime.
GetDiff Uses JsonNode / JsonElement for deep comparison. No Deserialize<object>() or reflection.
Query translation Where predicates lower to a shared IR and emit native SQL/BSON — no Expression.Compile(). Relational OrderBy/projections push to SQL.
In-memory evaluation LiteDB/IndexedDB and client-side filters use a compile-free tree-walking interpreter instead of Expression.Compile() — no RequiresDynamicCode (IL3050).
DI extensions Thin wrapper — delegates all work to the AOT-safe core.
Mapping APIs MapVersionProperty, MapSpatialProperty, MapVectorProperty, MapFullTextProperty, MapComputedProperty, MapBlob, and MapBlobCollection annotate their T with [DynamicallyAccessedMembers(PublicProperties)], so the trimmer preserves the properties they resolve by name.
Spatial serialization Geometry and DocumentBlob go through an internal source-generated context, not the caller’s reflection resolver.

Two things behave differently under AOT. Both are permanent, and both fail loudly rather than silently.

GroupBy(...).Select(...) needs a JsonTypeInfo for its projection target, and an anonymous type cannot have one. The anonymous-type overload throws at runtime under AOT:

InvalidOperationException: This operation requires a JsonTypeInfo<<>f__AnonymousType0`2>.
Pass it to the Select() call.

Project to a named type registered in your context instead:

public class NameCount // registered with [JsonSerializable(typeof(NameCount))]
{
public string Name { get; set; } = "";
public int Count { get; set; }
}
var grouped = await store.Query<Person>()
.GroupBy(x => x.Name)
.Select(g => new NameCount { Name = g.Key, Count = g.Count() }, AppJsonContext.Default.NameCount)
.ToList();

The projection still raises IL2026 in your own code, because Expression.Bind and Expression.New are RequiresUnreferencedCode for every LINQ expression tree — EF Core behaves identically. It is safe once the projection type is in your context, since that preserves exactly the members the tree names. Suppress it on the smallest method that holds the projection.

Anonymous-type parameter bags are not trim-safe

Section titled “Anonymous-type parameter bags are not trim-safe”

The string-query overloads accept parameters either as an anonymous type or as a dictionary:

await store.Query<User>("json_extract(Data, '$.age') > @minAge", parameters: new { minAge = 30 }); // reflective
await store.Query<User>("json_extract(Data, '$.age') > @minAge",
parameters: new Dictionary<string, object?> { ["minAge"] = 30 }); // AOT-safe

The anonymous-type form reads its values with GetType().GetProperties(). DynamicallyAccessedMembers cannot be applied to an object parameter (IL2098), so that branch genuinely cannot be made analyzable — pass the dictionary under trimming or AOT.

Publish with per-site warnings turned on, so a problem points at a line rather than an assembly:

<PropertyGroup>
<PublishAot>true</PublishAot>
<TrimmerSingleWarn>false</TrimmerSingleWarn>
</PropertyGroup>
Terminal window
dotnet publish -r linux-x64 -c Release

A clean publish is the real check — the Roslyn analyzers cannot see warnings coming out of dependencies, nor anything reachable only through a call graph. The repo runs exactly this on every build via samples/Sample.Aot, which exercises each mapping kind and every query surface once and treats any trim/AOT warning as a build error.