DTO conventions
A DTO is a plain C# class whose shape gets serialised across the bridge. DTOs are used as:
- Event payload types:
Event<DamageEvent>. - Nested state: a
[BridgeState]property whose type is a DTO ([BridgeState] public PlayerStats Stats { get; set; }), pushed wholesale when you reassign it. Give the nested class its own[BridgeState]members if you want a single inner field to push on its own (deep per-leaf updates). - Action parameter types:
[BridgeAction] public void Submit(SubmissionData data).
Rules
Public class.
public class FooEvent { ... }. No structs yet; they may be added in a future release.Public fields or auto-properties. The serialiser walks public fields and auto-properties. Private members are ignored.
public class DamageEvent { public float Amount; public bool IsCritical; }Supported field types:
- Integers:
int,uint,short,ushort,byte,sbyte(→ TSnumber);long/ulong(→ TSbigint, see below) - Floating point:
float,double bool,string,char- Enums (serialised by name, so the TS side is a string-literal union
e.g.
'MainMenu' | 'Playing') byte[](→ TSUint8Array)Nullable<T>of a supported value type (→T | null)List<T>/T[]/Dictionary<K,V>of supported types- Other DTOs
longandulongalways cross asbigint, nevernumber, whatever the magnitude. In TS use bigint literals (1n), andString(v)to display (notJSON.stringify, which throws on bigint), and don’t mix bigint with number in arithmetic. Usestringinstead if you just want a large id to carry in JSON.- Integers:
No constructor logic that matters. The deserialiser uses the parameterless constructor (or none) and sets fields. If you put logic in a constructor, it does not run on the UI side.
No reference loops. DTOs are serialised as a tree. If two DTOs reference each other, the serialiser stack-overflows.
Naming
The TypeScript type generator translates C# names to TS using the same casing rules as bridge members:
- C#
DamageEvent-> TSDamageEvent(type names are preserved). - C#
Amountfield -> TSamount. - C#
IsCriticalfield -> TSisCritical.
Only the first character is lowercased, the rest of the name is left
exactly as written. So HudVisible -> hudVisible, but an all-caps prefix
like HUDSettings becomes hUDSettings. Avoid all-caps acronym prefixes in
member names to keep the generated TS names clean (prefer HudSettings).
A worked example
public class InventoryItem {
public string Id;
public int Quantity;
}
public class PlayerLoadout {
public List<InventoryItem> Items;
public string Weapon;
}
[Bridge]
public partial class GameBridge : LoomBridgeBase {
[BridgeState] public PlayerLoadout Loadout { get; set; } = new() {
Items = new(),
Weapon = "shortsword",
};
} The generated TS:
interface InventoryItem { id: string; quantity: number }
interface PlayerLoadout { items: InventoryItem[]; weapon: string }
bridge.state.loadout // PlayerLoadout, reactive