loomgui.com ↗

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

  1. Public class. public class FooEvent { ... }. No structs yet; they may be added in a future release.

  2. 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;
    }
  3. Supported field types:

    • Integers: int, uint, short, ushort, byte, sbyte (→ TS number); long / ulong (→ TS bigint, 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[] (→ TS Uint8Array)
    • Nullable<T> of a supported value type (→ T | null)
    • List<T> / T[] / Dictionary<K,V> of supported types
    • Other DTOs

    long and ulong always cross as bigint, never number, whatever the magnitude. In TS use bigint literals (1n), and String(v) to display (not JSON.stringify, which throws on bigint), and don’t mix bigint with number in arithmetic. Use string instead if you just want a large id to carry in JSON.

  4. 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.

  5. 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 -> TS DamageEvent (type names are preserved).
  • C# Amount field -> TS amount.
  • C# IsCritical field -> TS isCritical.

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