Skip to content

Perspective Registry

Verified by tests

SchemaHashUtilitiesTests, NamingConventionUtilitiesTests, EFCoreServiceRegistrationGeneratorCoverageTests — library CI run #31657041675 (2026-08-13)

The perspective registry is a system table that tracks the mapping between your perspective model types (the TModel in IPerspectiveFor<TModel, ...>) and their corresponding database tables. It enables automatic schema management, drift detection, and safe table renaming across deployments.

Overview

When Whizbang creates perspective tables, it registers metadata about each perspective in the wh_perspective_registry table. This enables:

Feature Description
CLR Type Tracking Maps fully-qualified model type names to table names
Schema Hashing SHA-256 hash of table schema for drift detection
Auto-Rename Automatically renames tables when perspective names change
Multi-Service Tracks which service owns each perspective

How It Works

Registration Flow

flowchart TD
    Generator["Source Generator<br/>(compile time)<br/><br/>Generates metadata:<br/>- CLR type name<br/>- Table name<br/>- Schema JSON<br/>- Schema hash"]
    AppStart["Application Start<br/>(runtime)<br/><br/>Calls reconcile_perspective_registry()"]
    Database["Database<br/><br/>- Inserts new types<br/>- Detects renames<br/>- Detects drift"]

    Generator --> AppStart
    AppStart --> Database

Registry Table Schema

Registry Table Schema

CREATE TABLE wh_perspective_registry (
  id UUID PRIMARY KEY,
  clr_type_name VARCHAR(500) NOT NULL,    -- "MyApp.Contracts.OrderData" (model type)
  table_name VARCHAR(255) NOT NULL,        -- "wh_per_order_data"
  schema_json JSONB NOT NULL,              -- Full column/index definition
  schema_hash VARCHAR(64) NOT NULL,        -- SHA-256 of canonical schema
  service_name VARCHAR(255) NOT NULL,      -- "MyApp.Api"
  created_at TIMESTAMPTZ NOT NULL,
  updated_at TIMESTAMPTZ NOT NULL,
  UNIQUE(clr_type_name, service_name)
);

Reconciliation Actions

When your application starts, the reconciliation function compares registered perspectives against the database and returns actions taken:

Action When It Occurs
inserted New perspective type registered for the first time
updated Existing type refreshed (timestamps updated)
renamed Table name changed - executes ALTER TABLE RENAME
drift_detected Schema hash differs from previous deployment

Example Output

Example Output

// Startup logs show reconciliation results
[DBG] Registered new perspective: MyApp.Contracts.OrderData  wh_per_order_data
[WRN] Renamed perspective table: MyApp.Contracts.CustomerData from wh_per_customer_data  wh_per_customer
[WRN] Schema drift detected for perspective: MyApp.Contracts.ProductData (wh_per_product_data)
[DBG] Perspective registry reconciliation complete: 1 inserted, 4 updated, 1 renamed, 1 drift warnings

Schema Drift Detection

Schema drift occurs when your C# perspective class changes but the database table wasn't updated. The registry detects this by comparing schema hashes.

What Causes Drift

  • Adding or removing physical field columns on your perspective model
  • Changing physical field column types (e.g., int to long)
  • Adding or removing indexes (e.g., via [PhysicalField(Indexed = true)])
  • Changing vector field dimensions

Handling Drift

Drift detection is informational: when the reconciliation function returns drift_detected, Whizbang logs a warning (Schema drift detected for perspective: ...) and continues startup. Reconciliation failures never abort initialization. When you see a drift warning you can:

  1. Run migrations to update the table schema
  2. Recreate the table if the changes are breaking
  3. Ignore if the changes are backward-compatible

There is no configuration knob for drift behavior at this commit -- the warning is always logged, and no automatic migration is attempted.

Automatic Table Renaming

Table names are generated from the perspective model type name (see Table Naming). When the generated table name changes -- for example, after changing suffix-stripping configuration -- the registry automatically handles the rename:

Before

Before

// Model type CustomerData with suffix stripping disabled
public class CustomerPerspective : IPerspectiveFor<CustomerData, CustomerCreatedEvent> {
  // Table: wh_per_customer_data
}

After

After

// "Data" added to WhizbangTableNameSuffixesToStrip in the project file
public class CustomerPerspective : IPerspectiveFor<CustomerData, CustomerCreatedEvent> {
  // Table: wh_per_customer
}

What Happens

  1. Application starts and calls reconcile_perspective_registry()
  2. Registry finds existing entry for the model type MyApp.Contracts.CustomerData
  3. Detects table name changed from wh_per_customer_data to wh_per_customer
  4. Executes: ALTER TABLE IF EXISTS wh_per_customer_data RENAME TO wh_per_customer
  5. Updates registry with new table name

This happens automatically - no manual migration required. If the rename fails (e.g., the old table no longer exists), the registry entry is still updated and the action is reported as updated.

Note that renaming the model type itself changes the registry key (clr_type_name), so the reconciler treats it as a brand-new perspective (inserted) rather than a rename -- the old table is left in place.

Multi-Service Scenarios

In microservice architectures, multiple services may define perspectives. The registry tracks which service owns each perspective via the service_name column.

Multi-Service Scenarios

// Service A: OrderService
public class OrderProjection : IPerspectiveFor<OrderData, OrderCreatedEvent> { }
// Registered as: clr_type_name = "OrderService.Contracts.OrderData",
//                table_name = "wh_per_order_data", service_name = "OrderService"

// Service B: AnalyticsService
public class OrderAnalyticsProjection : IPerspectiveFor<OrderAnalytics, OrderCreatedEvent> { }
// Registered as: clr_type_name = "AnalyticsService.Contracts.OrderAnalytics",
//                table_name = "wh_per_order_analytics", service_name = "AnalyticsService"

The unique constraint (clr_type_name, service_name) allows the same model type name in different services.

Schema JSON Format

The registry stores the full schema definition as JSON for debugging and migration tooling:

Schema JSON Format

{
  "columns": [
    {"isPrimaryKey": true, "name": "id", "type": "uuid"},
    {"name": "created_at", "type": "timestamptz"},
    {"name": "customer_id", "nullable": true, "type": "uuid"},
    {"name": "data", "type": "jsonb"},
    {"name": "metadata", "type": "jsonb"},
    {"name": "scope", "type": "jsonb"},
    {"name": "updated_at", "type": "timestamptz"},
    {"name": "version", "type": "integer"}
  ],
  "indexes": [
    {"columns": ["customer_id"], "name": "idx_customer_customer_id", "type": "btree"},
    {"columns": ["data"], "name": "idx_customer_data_gin", "type": "gin"}
  ]
}

Columns and indexes are sorted alphabetically by name, false/null values are omitted (e.g., nullable only appears for nullable columns), and vector columns carry isVector and vectorDimensions properties.

Schema Hash Algorithm

The schema hash is computed using:

  1. Serialize schema to canonical JSON (alphabetically sorted columns/indexes, camelCase keys, no whitespace, lowercase types, false/null omitted)
  2. Encode as UTF-8 bytes
  3. Compute SHA-256 hash
  4. Output as 64-character lowercase hex string

This ensures consistent hashes across deployments regardless of serialization order.

Querying the Registry

You can query the registry directly for debugging:

Querying the Registry

-- All perspectives for a service
SELECT clr_type_name, table_name, schema_hash, updated_at
FROM wh_perspective_registry
WHERE service_name = 'MyApp.Api'
ORDER BY table_name;

-- Find perspectives with schema drift (compare with application metadata)
SELECT clr_type_name, table_name, schema_hash
FROM wh_perspective_registry
WHERE schema_hash != 'expected_hash_from_app';

-- Recently updated perspectives
SELECT clr_type_name, table_name, updated_at
FROM wh_perspective_registry
WHERE updated_at > NOW() - INTERVAL '1 hour';

Configuration

The registry is automatically created as part of the Whizbang infrastructure schema. No additional configuration is required.

Configuration

// Registry is included in standard initialization
await dbContext.EnsureWhizbangDatabaseInitializedAsync();

See Also