Unified Object Model

UORM (Unified Object-Relational Mapping) is Altruist's data model system. It provides a consistent set of attributes for defining how your entities are stored, queried, and managed across different persistence providers.

Database Provider

Altruist currently supports PostgreSQL as the primary database provider:

altruist:
  persistence:
    database:
      provider: postgres
      host: localhost
      port: 5432
      username: myuser
      password: mypassword
      database: mydb

Model Attributes

[Vault] — Table Definition

Defines a stored entity:

[Vault("game_state", StoreHistory = true)]
public class GameState : IVaultModel
{
    public string StorageId { get; set; } = Guid.NewGuid().ToString();
}
  • Name: The table name
  • StoreHistory: If true, all updates are stored as historical records (creates a _history table)

Note:

Avoid enabling StoreHistory on frequently updated tables — it can significantly increase storage usage.

[PrimaryKey] — Primary Key

[PrimaryKey("StorageId")]
public class Player : IVaultModel { ... }

[Column] — Explicit Column Name

[Column("player_name")]
public string Name { get; set; }

[ColumnIndex] — Indexed Column

[ColumnIndex]
public int Level { get; set; }

[Ignore] — Exclude from Storage

[Ignore]
public string TemporaryData { get; set; }

[SortingBy] — Default Sort Order

[SortingBy("SaveTime", Ascending: false)]
public class GameSave : IVaultModel { ... }

[VaultUniqueKey] — Unique Constraints

Enforce uniqueness on one or more columns:

[Vault("accounts")]
[VaultUniqueKey("username")]
[VaultUniqueKey("email")]
public class AccountVault : VaultModel
{
    [VaultColumn] public override string StorageId { get; set; } = "";
    [VaultColumn("username")] public string Username { get; set; } = "";
    [VaultColumn("email")] public string Email { get; set; } = "";
}

Note:

Each [VaultUniqueKey] creates a separate unique constraint. Use multiple column names in one attribute for composite unique keys: [VaultUniqueKey("first_name", "last_name")].

[VaultForeignKey] — Foreign Key Relationships

Define foreign key constraints with cascade behavior:

[Vault("character_effects")]
public class EffectVault : VaultModel
{
    [VaultColumn] public override string StorageId { get; set; } = "";

    [VaultColumn("character_id")]
    [VaultForeignKey(typeof(CharacterVault), nameof(CharacterVault.StorageId))]
    public string CharacterId { get; set; } = "";

    [VaultColumn] public string EffectType { get; set; } = "";
    [VaultColumn] public int Duration { get; set; }
}

The third parameter controls ON DELETE behavior:

// Default: CASCADE — delete children when parent is deleted
[VaultForeignKey(typeof(CharacterVault), nameof(CharacterVault.StorageId))]

// Prevent deletion if children exist
[VaultForeignKey(typeof(CharacterVault), nameof(CharacterVault.StorageId),
    VaultForeignKeyDeleteBehavior.Restrict)]

// Set to NULL when parent is deleted
[VaultForeignKey(typeof(CharacterVault), nameof(CharacterVault.StorageId),
    VaultForeignKeyDeleteBehavior.SetNull)]
BehaviorSQLDescription
CascadeON DELETE CASCADEDelete children when parent is deleted
NoActionON DELETE NO ACTIONPrevent deletion if children exist (deferred)
RestrictON DELETE RESTRICTPrevent deletion if children exist (immediate)
SetNullON DELETE SET NULLSet FK column to NULL
SetDefaultON DELETE SET DEFAULTSet FK column to its default value

Note:

Foreign keys create database-level constraints. Make sure the referenced table and column exist (the referenced vault model must be registered first). Use IDatabaseInitializer with ordering to control table creation order if needed.

[VaultPrimaryKey] — Composite Primary Keys

For models with composite primary keys (multiple columns forming the key):

[Vault("player_achievements")]
[VaultPrimaryKey(nameof(PlayerId), nameof(AchievementCode))]
public class PlayerAchievementVault : VaultModel
{
    [VaultColumn] public override string StorageId { get; set; } = "";
    [VaultColumn("player_id")] public string PlayerId { get; set; } = "";
    [VaultColumn("achievement_code")] public string AchievementCode { get; set; } = "";
    [VaultColumn] public DateTime UnlockedAt { get; set; }
}

Note:

If you don't specify [VaultPrimaryKey], the framework uses StorageId as the default primary key.

Keyspaces

A Keyspace organizes your tables (similar to a database schema):

public class MyKeyspace : IKeyspace
{
    public string Name { get; set; } = "myapp";
}

Querying with Vault

The Vault API provides LINQ-like query capabilities:

var vault = repository.Select<Player>();

// Filter
var highLevel = await vault.Where(p => p.Level > 50).ToListAsync();

// Sort
var topPlayers = await vault.OrderByDescending(p => p.Level).Take(10).ToListAsync();

// Single record
var player = await vault.Where(p => p.Name == "Hero").FirstOrDefaultAsync();

// Save
await vault.SaveAsync(newPlayer);

// Save with history
await vault.SaveAsync(player, storeHistory: true);

Historical Storage

When StoreHistory = true, the framework creates two tables:

  1. Regular table — stores the current state
  2. History table (<name>_history) — retains past versions with timestamps

You control when to write history:

// Regular save (only updates current state)
await vault.SaveAsync(gameState);

// Save with historical snapshot
await vault.SaveAsync(gameState, storeHistory: true);

Using Vaults

Inject IVault<T> directly into your services — the framework resolves the correct database provider and keyspace automatically:

[Service]
public class PlayerService
{
    private readonly IVault<PlayerVault> _players;
    private readonly IVault<ItemVault> _items;

    public PlayerService(IVault<PlayerVault> players, IVault<ItemVault> items)
    {
        _players = players;
        _items = items;
    }
}