Runtime operations
Read undeclared columns, auto-discover a live schema, order and paginate, and create or drop a dynamic table at runtime, all from the declared [TableType].
This page covers the runtime-only operations on a DynamicTable<T>: undeclared columns, schema auto-mapping, ordering, and table lifecycle. See Declaring table types for the declaration and CRUD basics.
Custom (undeclared) columns
A runtime table may carry extra columns beyond the declared shape. Opt in per query with WithCustomColumns(...), filter on them with the DB.CustomField<T>("name") marker inside a normal predicate, and read them back with TryGetCustomValue<T>(...) on each row:
using static Socigy.OpenSource.DB.Core.SyntaxHelper.DB;
var rows = await AuditEntry.WithTableName("audit_2026_06")
.WithCustomColumns("region", "score") // capture extra columns into each row
.Query(x => CustomField<int>("score") > 10) // filter on an undeclared column
.WithConnection(conn)
.ToListAsync();
foreach (var r in rows)
if (r.TryGetCustomValue<string>("region", out var region))
Console.WriteLine(region);CustomField<T> is a translation marker, rewritten into SQL at build time; it never runs at runtime. Custom values are dictionary-backed, so they cost slightly more than a declared property. TryGetCustomValue<T> converts the stored value to T and returns false when the column was not captured.
Auto-mapping a live schema
Skip naming the extras: MapTypeAsync(name, conn) introspects the live table once, discovers every column beyond the declared shape, binds them, and caches the resolved schema per (type, table name):
var t = await AuditEntry.MapTypeAsync("audit_2026_06", conn); // discovers + binds extras, then cached
var rows = await t.Query(x => x.UserId == id).ToListAsync(); // declared props + every extra columnOrdering and pagination
On a dynamic table OrderBy takes a raw SQL fragment (the column shape is bound late, so there is no lambda form). Pair it with Limit/Offset:
var page = await AuditEntry.WithTableName("audit_2026_06")
.WithConnection(conn)
.Query(x => x.UserId == id)
.OrderBy("\"at\" DESC")
.Limit(20).Offset(40)
.ToListAsync();Use the generated {Property}ColumnName constants to keep raw fragments in sync with the schema, for example OrderBy($"{AuditEntry.AtColumnName} DESC").
Table lifecycle (create and drop)
Because [TableType] tables are not part of the migration history, the type can create and drop its own table at runtime, with DDL derived from the declared shape:
var t = AuditEntry.WithTableName("audit_2026_06").WithConnection(conn);
await t.InstantiateAsync(); // CREATE TABLE IF NOT EXISTS "audit_2026_06" (...declared columns...)
bool exists = await t.InstanceExistsAsync();
await t.DeleteInstanceAsync(); // DROP TABLE IF EXISTS "audit_2026_06"Notes and limits
- Results are typed
T, notdynamic, so dynamic tables work under NativeAOT. Only custom-column access goes through a dictionary. InstantiateAsyncmaps the common CLR types (Guid,int/long/short,string,bool,decimal/double/float,DateTime,byte[]),[Encrypted]tobytea, and JSON tojsonb.[Default]expressions are not emitted in the runtime DDL, so provide those values yourself or create the table out of band. Enum columns fall back totext.[TableType]-only classes are never touched by the migration runner.UpdateAsyncrequires a predicate and excludes the primary key (and, by default, auto-increment columns) from the SET list.