Declaring table types
Declare a column shape once with [TableType], bind the table name at runtime, and get fully typed results. The right tool for per-tenant, sharded, and time-partitioned tables.
Some tables are created at runtime: per tenant, per period, or sharded (audit_2026_06, tenant_42_orders). Their names are not known at build time, but their column shape usually is. A [TableType] class lets you declare that shape once and bind the name at runtime, returning the typed entity. Only the name is dynamic, so the whole feature stays NativeAOT-safe.
Declaring a table type
using Socigy.OpenSource.DB.Attributes;
[TableType]
public partial class AuditEntry // the generator implements IDbTableType<AuditEntry>
{
[PrimaryKey, Default(DbDefaults.Guid.Random)]
public Guid Id { get; set; }
public Guid UserId { get; set; }
public required string Action { get; set; }
public int Amount { get; set; }
public DateTime At { get; set; }
}Columns use the same attributes as [Table] ([PrimaryKey], [Column], [JsonColumn], [Encrypted], and the rest). A class may carry both [Table("default")] and [TableType]: a fixed default name plus a runtime override.
The generator emits two static entry points on the class:
| Method | Returns | Use |
|---|---|---|
AuditEntry.WithTableName(string name) |
DynamicTable<AuditEntry> |
bind a runtime name |
AuditEntry.MapTypeAsync(string name, DbConnection conn, bool force = false, CancellationToken ct = default) |
Task<DynamicTable<AuditEntry>> |
bind and auto-discover extra columns (see Runtime operations) |
Querying and writing without a context
Bind the runtime name with WithTableName, attach a connection, then use the full typed API. DynamicTable<T> lives in Socigy.OpenSource.DB.Core.Dynamic.
await using var conn = factory.Create();
await conn.OpenAsync();
var t = AuditEntry.WithTableName("audit_2026_06");
List<AuditEntry> rows = await t.WithConnection(conn).Query(x => x.UserId == id).ToListAsync();
AuditEntry? first = await t.WithConnection(conn).Query(x => x.Amount > 100).FirstOrDefaultAsync();
long count = await t.WithConnection(conn).Query(x => x.Action == "login").CountAsync();
int sum = await t.WithConnection(conn).SumAsync<int>(x => x.Amount) ?? 0;
await t.WithConnection(conn).InsertAsync(new AuditEntry { UserId = id, Action = "login", At = DateTime.UtcNow });
await t.WithConnection(conn).InsertMultipleAsync(batch); // batched multi-row INSERT
await t.WithConnection(conn).UpdateAsync(changed, x => x.Id == changed.Id);
await t.WithConnection(conn).DeleteAsync(x => x.At < cutoff);WithTableName(...) returns a fresh handle. Build one per query rather than reusing a handle that already has a predicate.The handle exposes the full surface: Query/Where, OrderBy, Limit/Offset, WithCustomColumns; the readers ExecuteAsync (stream), ToListAsync, FirstOrDefaultAsync, ExistsAsync, CountAsync; the aggregates SumAsync/AvgAsync/MinAsync/MaxAsync/ScalarAsync; the writers InsertAsync/InsertMultipleAsync/UpdateAsync/DeleteAsync; and the lifecycle methods covered in Runtime operations.
Through the context
The generated context exposes a generic DynamicTable<T>(name) that joins the unit-of-work scope, so it enlists the ambient transaction and shares the connection:
await db.ExecuteTransactionAsync(async d =>
{
await d.DynamicTable<AuditEntry>("audit_2026_06").InsertAsync(entry);
long count = await d.DynamicTable<AuditEntry>("audit_2026_06")
.Query(x => x.UserId == id)
.CountAsync();
});[Table] name, never as untrusted user input.