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.,
inttolong) - 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:
- Run migrations to update the table schema
- Recreate the table if the changes are breaking
- 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¶
- Application starts and calls
reconcile_perspective_registry() - Registry finds existing entry for the model type
MyApp.Contracts.CustomerData - Detects table name changed from
wh_per_customer_datatowh_per_customer - Executes:
ALTER TABLE IF EXISTS wh_per_customer_data RENAME TO wh_per_customer - 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:
- Serialize schema to canonical JSON (alphabetically sorted columns/indexes, camelCase keys, no whitespace, lowercase types,
false/nullomitted) - Encode as UTF-8 bytes
- Compute SHA-256 hash
- 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¶
- Table Naming - Configure table name generation
- Schema Migration - Database schema management
- Temporal Perspectives - Append-only perspective pattern