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});var ctx = new AppJsonContext(new JsonSerializerOptions{ PropertyNamingPolicy = JsonNamingPolicy.CamelCase});
var store = new DocumentStore(new DocumentStoreOptions{ DatabaseProvider = new SqlServerDatabaseProvider("Server=localhost;Database=mydb;Trusted_Connection=true;"), JsonSerializerOptions = ctx.Options, UseReflectionFallback = false // recommended for AOT});Multiple JSON contexts
Section titled “Multiple JSON contexts”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});Optional JsonTypeInfo<T> parameters
Section titled “Optional JsonTypeInfo<T> parameters”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 configuredvar 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 setvar 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);Disabling reflection fallback
Section titled “Disabling reflection fallback”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-outpublic partial class AppContext : DocumentContext;AOT-safe feature summary
Section titled “AOT-safe feature summary”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. |
Known limitations under AOT
Section titled “Known limitations under AOT”Two things behave differently under AOT. Both are permanent, and both fail loudly rather than silently.
Grouped projections need a named type
Section titled “Grouped projections need a named type”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 }); // reflectiveawait store.Query<User>("json_extract(Data, '$.age') > @minAge", parameters: new Dictionary<string, object?> { ["minAge"] = 30 }); // AOT-safeThe 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.
Verifying AOT in your own app
Section titled “Verifying AOT in your own app”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>dotnet publish -r linux-x64 -c ReleaseA 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.


