Message Registry¶
Verified by tests
MessageRegistryGeneratorTests, MessageRegistryGeneratorCoverageTests — library CI run #31657041675 (2026-08-13)
The MessageRegistryGenerator discovers all messages, dispatchers, receptors, and perspectives at compile-time and generates a JSON registry file for the Whizbang VSCode Extension. This enables rich IDE features like CodeLens annotations, hover tooltips, and "Go to Handler" navigation.
VSCode Extension Integration¶
IDE Features Enabled¶
| Feature | Description | Example |
|---|---|---|
| CodeLens | Inline annotations showing handler counts | CreateOrder → 3 dispatchers, 1 receptor |
| Hover Tooltip | Rich markdown with handler locations | Mouse over message → see all handlers |
| Go to Handler | Navigate from message to implementation | Click CodeLens → jump to receptor |
| Find References | List all dispatchers for a message | Right-click → Find dispatchers |
Visual Example: IDE Features Enabled
// In your code editor:
public record CreateOrder(Guid CustomerId, OrderItem[] Items) : ICommand;
↑↑↑↑↑↑↑↑↑↑↑
[3 dispatchers] [1 receptor] [0 perspectives] ← CodeLens
// Click [1 receptor] → Jump to OrderReceptor.HandleAsync()
How It Works¶
1. Compile-Time Discovery¶
flowchart TD
Generator["MessageRegistryGenerator (Roslyn)<br/><br/>Discovers 4 types of constructs:<br/><br/>1. Messages (ICommand, IEvent)<br/>2. Dispatchers (IDispatcher SendAsync/PublishAsync calls)<br/>3. Receptors (IReceptor implementations)<br/>4. Perspectives (IPerspectiveFor implementations)"]
Generated["Generated: MessageRegistry.g.cs<br/><br/>Contains:<br/>- Embedded JSON with message-to-handler mapping<br/>- File paths and line numbers for navigation<br/>- docsUrl + tests enrichment, package versions"]
Build["Shipped MSBuild target extracts JSON to<br/>.whizbang/cache/message-registry.json<br/>— Used by VSCode extension for tooling"]
Generator --> Generated
Generated --> Build
class Generator layer-infrastructure
class Generated,Build layer-core
2. Generated File¶
MessageRegistry.g.cs (simplified): Generated File
// <auto-generated/>
// Generated by Whizbang.Generators.MessageRegistryGenerator
#nullable enable
namespace Whizbang.Generated;
/// <summary>
/// Auto-generated message registry containing metadata about all discovered messages,
/// dispatchers, receptors, and perspectives in the application.
/// </summary>
internal static class MessageRegistry {
internal const string Json = @"{
""messages"": [
{
""type"": ""MyApp.Commands.CreateOrder"",
""isCommand"": true,
""isEvent"": false,
""filePath"": ""src/Commands/CreateOrder.cs"",
""lineNumber"": 5,
""docsUrl"": """",
""tests"": [],
""dispatchers"": [
{
""class"": ""MyApp.Controllers.OrderController"",
""method"": ""CreateAsync"",
""filePath"": ""src/Controllers/OrderController.cs"",
""lineNumber"": 42,
""docsUrl"": """",
""tests"": []
}
],
""receptors"": [
{
""class"": ""MyApp.Receptors.OrderReceptor"",
""method"": ""HandleAsync"",
""filePath"": ""src/Receptors/OrderReceptor.cs"",
""lineNumber"": 12,
""docsUrl"": """",
""tests"": []
}
],
""perspectives"": []
},
{
""type"": ""MyApp.Events.OrderCreated"",
""isCommand"": false,
""isEvent"": true,
""filePath"": ""src/Events/OrderCreated.cs"",
""lineNumber"": 3,
""docsUrl"": """",
""tests"": [],
""dispatchers"": [],
""receptors"": [],
""perspectives"": [
{
""class"": ""MyApp.Perspectives.OrderSummaryPerspective"",
""method"": ""Update"",
""filePath"": ""src/Perspectives/OrderSummaryPerspective.cs"",
""lineNumber"": 8,
""docsUrl"": """",
""tests"": []
}
]
}
],
""whizbangPackages"": [
{ ""id"": ""SoftwareExtravaganza.Whizbang.Core"", ""versionPrefix"": ""0.500.1"" }
]
}";
}
Key Information:
- Message type (fully qualified name)
- Message kind (command vs event)
- Definition location (file path + line number)
- All dispatchers calling this message
- All receptors handling this message
- All perspectives listening to this event
- docsUrl/tests enrichment (from the docs repo's code-docs-map.json / code-tests-map.json when found — WHIZ053/WHIZ054 report when they can't be loaded)
- whizbangPackages: referenced Whizbang assembly versions, so the extension can match NuGet folders by prefix
Discovery Patterns¶
Pattern 1: Message Discovery¶
Discovers ICommand and IEvent implementations:
Pattern 1: Message Discovery
// Command
public record CreateOrder(
Guid CustomerId,
OrderItem[] Items
) : ICommand; // ← Discovered
// Event
public record OrderCreated(
Guid OrderId,
Guid CustomerId,
decimal Total,
DateTimeOffset CreatedAt
) : IEvent; // ← Discovered
Generator finds:
- Type name: MyApp.Commands.CreateOrder
- File: src/Commands/CreateOrder.cs
- Line: 5
- IsCommand: true
- IsEvent: false
Pattern 2: Dispatcher Discovery¶
Discovers SendAsync and PublishAsync call sites — but only when the containing type is or implements Whizbang.Core.IDispatcher. Same-named methods on mocks (Rocks/Moq expectations) or unrelated wrappers are skipped:
Pattern 2: Dispatcher Discovery
public class OrderController : ControllerBase {
private readonly IDispatcher _dispatcher;
public async Task<IActionResult> CreateAsync(CreateOrderRequest request) {
var command = new CreateOrder(request.CustomerId, request.Items);
// ← Dispatcher call discovered
var @event = await _dispatcher.SendAsync(command);
return Ok(@event);
}
}
Generator finds:
- Message type: CreateOrder
- Class: OrderController
- Method: CreateAsync
- File: src/Controllers/OrderController.cs
- Line: 42
Also discovers PublishAsync: Pattern 2: Dispatcher Discovery (2)
// Inside OrderReceptor
var @event = new OrderCreated(/* ... */);
await _dispatcher.PublishAsync(@event); // ← Discovered
Pattern 3: Receptor Discovery¶
Discovers IReceptor
Pattern 3: Receptor Discovery
public class OrderReceptor : IReceptor<CreateOrder, OrderCreated> {
public async ValueTask<OrderCreated> HandleAsync( // ← Method discovered
CreateOrder message,
CancellationToken ct = default) {
// Business logic...
return new OrderCreated(/* ... */);
}
}
Generator finds:
- Message type: CreateOrder
- Class: OrderReceptor
- Method: HandleAsync
- File: src/Receptors/OrderReceptor.cs
- Line: 12 (HandleAsync method location)
Pattern 4: Perspective Discovery¶
Discovers IPerspectiveForIPerspectiveWithActionsFor) implementations — event types are the type arguments after the model:
Pattern 4: Perspective Discovery
public class OrderSummaryPerspective :
IPerspectiveFor<OrderSummary, OrderCreated, OrderShipped, OrderCancelled> {
// ↑ Model first, then all discovered event types
public OrderSummary Apply(OrderSummary currentData, OrderCreated eventData) { /* ... */ }
public OrderSummary Apply(OrderSummary currentData, OrderShipped eventData) { /* ... */ }
public OrderSummary Apply(OrderSummary currentData, OrderCancelled eventData) { /* ... */ }
}
Generator finds:
- Event types: OrderCreated, OrderShipped, OrderCancelled
- Class: OrderSummaryPerspective
- File: src/Perspectives/OrderSummaryPerspective.cs
- Line: 8 (class declaration)
Result: One perspective, three event type mappings.
JSON Structure¶
Complete Example¶
Complete Example
{
"messages": [
{
"type": "MyApp.Commands.CreateOrder",
"isCommand": true,
"isEvent": false,
"filePath": "src/Commands/CreateOrder.cs",
"lineNumber": 5,
"docsUrl": "",
"tests": [
{
"testFile": "tests/MyApp.Tests/CreateOrderTests.cs",
"testMethod": "CreateOrder_Valid_SucceedsAsync",
"testLine": 27,
"testClass": "CreateOrderTests"
}
],
"dispatchers": [
{
"class": "MyApp.Controllers.OrderController",
"method": "CreateAsync",
"filePath": "src/Controllers/OrderController.cs",
"lineNumber": 42,
"docsUrl": "",
"tests": []
}
],
"receptors": [
{
"class": "MyApp.Receptors.OrderReceptor",
"method": "HandleAsync",
"filePath": "src/Receptors/OrderReceptor.cs",
"lineNumber": 12,
"docsUrl": "",
"tests": []
}
],
"perspectives": []
},
{
"type": "MyApp.Events.OrderCreated",
"isCommand": false,
"isEvent": true,
"filePath": "src/Events/OrderCreated.cs",
"lineNumber": 3,
"docsUrl": "",
"tests": [],
"dispatchers": [],
"receptors": [],
"perspectives": [
{
"class": "MyApp.Perspectives.OrderSummaryPerspective",
"method": "Update",
"filePath": "src/Perspectives/OrderSummaryPerspective.cs",
"lineNumber": 8,
"docsUrl": "",
"tests": []
}
]
}
],
"whizbangPackages": [
{ "id": "SoftwareExtravaganza.Whizbang.Core", "versionPrefix": "0.500.1" }
]
}
Field Descriptions¶
| Field | Type | Description |
|---|---|---|
type |
string | Fully qualified message type name |
isCommand |
boolean | True if implements ICommand (inferred from receptors for referenced-assembly messages) |
isEvent |
boolean | True if implements IEvent (inferred from perspectives/dispatchers for referenced-assembly messages) |
filePath |
string | Source path (empty for messages defined in referenced assemblies) |
lineNumber |
number | Line number (1-based; 0 for referenced-assembly messages) |
docsUrl |
string | Documentation URL enriched from code-docs-map.json (empty if unavailable) |
tests |
array | Linked tests enriched from code-tests-map.json (testFile, testMethod, testLine, testClass) |
dispatchers |
array | All IDispatcher SendAsync/PublishAsync call sites (class, method, filePath, lineNumber, docsUrl, tests) |
receptors |
array | All IReceptor implementations (same entry shape) |
perspectives |
array | All IPerspectiveFor implementations (method is always "Update") |
whizbangPackages |
array | Referenced Whizbang assemblies (id = SoftwareExtravaganza.* package id, versionPrefix = Major.Minor.Build) |
VSCode Extension Usage¶
Installing the Extension¶
Installing the Extension
# From VSCode Extensions panel:
# Search: "Whizbang"
# Install: Whizbang Message Flow Visualizer
# Or from command line:
code --install-extension whizbang.whizbang-vscode
Extension Features¶
1. CodeLens Annotations¶
Inline annotations above message types:
CodeLens Annotations
// [3 dispatchers] [1 receptor] [0 perspectives]
public record CreateOrder(Guid CustomerId, OrderItem[] Items) : ICommand;
Click counts to navigate:
- [3 dispatchers] → List of dispatcher locations
- [1 receptor] → Jump to receptor HandleAsync
- [0 perspectives] → No perspectives for this command
2. Hover Tooltips¶
Rich markdown tooltips on hover:
CreateOrder
Type: Command
Handlers: 1 receptor
Dispatchers (3):
• OrderController.CreateAsync (Controllers/OrderController.cs:42)
• OrderSaga.ProcessAsync (Sagas/OrderSaga.cs:18)
• OrderScheduler.ScheduleAsync (Schedulers/OrderScheduler.cs:105)
Receptors (1):
• OrderReceptor.HandleAsync (Receptors/OrderReceptor.cs:12)
Returns: OrderCreated
3. Go to Handler Command¶
Right-click message → "Go to Whizbang Handler":
4. Message Flow Visualization¶
Command palette → "Whizbang: Show Message Flow":
CreateOrder
├─ Dispatcher: OrderController.CreateAsync
│ └─ Receptor: OrderReceptor.HandleAsync
│ └─ Publishes: OrderCreated
│ ├─ Perspective: OrderSummaryPerspective
│ └─ Perspective: CustomerStatisticsPerspective
└─ Dispatcher: OrderSaga.ProcessAsync
└─ (Remote via Outbox)
Build Integration¶
Shipped MSBuild Targets (No Custom Setup Needed)¶
The SoftwareExtravaganza.Whizbang.Generators NuGet package ships .props/.targets files that automate the whole flow — you do not write your own target or extraction script:
SoftwareExtravaganza.Whizbang.Generators.propsdefaultsWhizbangEmitMessageRegistrytotrue, enablesEmitCompilerGeneratedFiles, and pointsCompilerGeneratedFilesOutputPathat$(MSBuildProjectDirectory)/.whizbang/cache(unless you've set these yourself).SoftwareExtravaganza.Whizbang.Generators.targetsdefines theWhizbangExtractMessageRegistrytarget (AfterTargets="Build"), which extracts the embedded JSON fromMessageRegistry.g.csand writes it to.whizbang/cache/message-registry.json— skipping the write when the content hash is unchanged, to avoid file-watcher churn.
Opting Out
<!-- MyApp.csproj — only needed to OPT OUT -->
<PropertyGroup>
<WhizbangEmitMessageRegistry>false</WhizbangEmitMessageRegistry>
</PropertyGroup>
Run a build: Build Writes the Registry
The .whizbang/ Folder Convention¶
.whizbang/cache/— regenerable artifacts (generated.cs,message-registry.json); add**/.whizbang/cache/to.gitignore.whizbang/root — committed files (e.g.,pinned-type-ledger.json, the folder README); commit these
Generator Performance¶
Multi-Pipeline Architecture¶
Generator uses 4 independent pipelines for optimal caching:
Multi-Pipeline Architecture
// Pipeline 1: Messages
var messageTypes = context.SyntaxProvider.CreateSyntaxProvider(
predicate: static (node, _) => node is RecordDeclarationSyntax { BaseList.Types.Count: > 0 },
transform: static (ctx, ct) => ExtractMessageType(ctx, ct)
);
// Pipeline 2: Dispatchers
var dispatchers = context.SyntaxProvider.CreateSyntaxProvider(
predicate: static (node, _) => node is InvocationExpressionSyntax { ... },
transform: static (ctx, ct) => ExtractDispatcher(ctx, ct)
);
// Pipeline 3: Receptors
var receptors = context.SyntaxProvider.CreateSyntaxProvider(
predicate: static (node, _) => node is ClassDeclarationSyntax { BaseList.Types.Count: > 0 },
transform: static (ctx, ct) => ExtractReceptor(ctx, ct)
);
// Pipeline 4: Perspectives
var perspectives = context.SyntaxProvider.CreateSyntaxProvider(
predicate: static (node, _) => node is ClassDeclarationSyntax { BaseList.Types.Count: > 0 },
transform: static (ctx, ct) => ExtractPerspective(ctx, ct)
);
// Combine at the end
var allData = messageTypes.Collect()
.Combine(dispatchers.Collect())
.Combine(receptors.Collect())
.Combine(perspectives.Collect());
Why 4 pipelines? - Independent caching: Changing a dispatcher doesn't invalidate message cache - Optimized predicates: Each pipeline filters for its specific construct - Parallel execution: Roslyn runs pipelines concurrently
Performance Characteristics¶
| Phase | First compilation (100 messages, 200 handlers) | Incremental compilation (change 1 receptor) |
|---|---|---|
| Message discovery | 30ms | 0ms (cache, unchanged) |
| Dispatcher discovery | 40ms | 0ms (cache, unchanged) |
| Receptor discovery | 20ms | 20ms (changed) |
| Perspective discovery | 15ms | 0ms (cache, unchanged) |
| JSON generation | 5ms | 5ms |
| Total | 110ms | 25ms (85ms saved!) |
Cross-Assembly Messages¶
Generator handles messages from referenced assemblies:
Cross-Assembly Messages
// In Whizbang.Core (referenced assembly)
public interface ICommand { }
// In MyApp (your project)
public record CreateOrder(Guid CustomerId) : ICommand; // ← Discovered
// Also in MyApp
public class OrderReceptor : IReceptor<CreateOrder, OrderCreated> {
// ← Receptor discovered, links to CreateOrder
}
Result: CreateOrder appears in registry even though ICommand is external.
External Messages: Cross-Assembly Messages - CustomerCreated
// In SharedMessages (referenced assembly)
public record CustomerCreated([property: StreamId] Guid CustomerId) : IEvent;
// In MyApp
public class CustomerStatsPerspective : IPerspectiveFor<CustomerStats, CustomerCreated> {
// ← Perspective discovered, links to CustomerCreated
public CustomerStats Apply(CustomerStats currentData, CustomerCreated eventData) { /* ... */ }
}
JSON output (message kind inferred from usage; empty location): Cross-Assembly Messages (3)
{
"type": "SharedMessages.CustomerCreated",
"isCommand": false,
"isEvent": true,
"filePath": "",
"lineNumber": 0,
"docsUrl": "",
"tests": [],
"dispatchers": [],
"receptors": [],
"perspectives": [
{
"class": "MyApp.Perspectives.CustomerStatsPerspective",
"method": "Update",
"filePath": "src/Perspectives/CustomerStatsPerspective.cs",
"lineNumber": 8,
"docsUrl": "",
"tests": []
}
]
}
Benefit: Complete message flow visualization across assemblies.
Debugging¶
View Generated File¶
Or configured output: View Generated File
<PropertyGroup>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>.whizbang/cache</CompilerGeneratedFilesOutputPath>
</PropertyGroup>
Validate JSON¶
Validate JSON
# The registry JSON is extracted automatically by the shipped MSBuild target
dotnet build
# Validate JSON
cat .whizbang/cache/message-registry.json | jq .
# Output:
{
"messages": [
{
"type": "MyApp.Commands.CreateOrder",
"isCommand": true,
...
}
]
}
VSCode Extension Logs¶
View extension logs: 1. View → Output 2. Select "Whizbang" from dropdown 3. See message registry loading and parsing
[Whizbang] Loading message registry from .whizbang/cache/message-registry.json
[Whizbang] Found 15 messages
[Whizbang] Registered 42 CodeLens providers
[Whizbang] Registry loaded successfully
Best Practices¶
DO ✅¶
- ✅ Use ICommand and IEvent markers for all messages
- ✅ Keep messages in dedicated folders (Commands/, Events/)
- ✅ Use descriptive names (CreateOrder, not Order1)
- ✅ Commit .whizbang/cache/message-registry.json to source control (helps team)
- ✅ Rebuild after adding new messages (F5 in VSCode to reload extension)
DON'T ❌¶
- ❌ Modify MessageRegistry.g.cs manually (regenerated on build)
- ❌ Use abstract message types (can't be instantiated)
- ❌ Mix commands and events (implement one interface only)
- ❌ Delete .whizbang/ folder (VSCode extension needs it)
Troubleshooting¶
Problem: CodeLens Not Showing¶
Symptoms: No [X dispatchers] annotations above messages.
Causes: 1. Extension not installed 2. Message registry not generated 3. Extension not loaded
Solution: Problem: CodeLens Not Showing
# 1. Verify extension installed
code --list-extensions | grep whizbang
# 2. Rebuild project (generates registry)
dotnet build
# 3. Check .whizbang/cache/message-registry.json exists
ls -la .whizbang/
# 4. Reload VSCode window
Ctrl+Shift+P → "Developer: Reload Window"
Problem: Wrong Handler Count¶
Symptoms: CodeLens shows [1 receptor] but you have 2 receptors.
Causes: 1. Stale message-registry.json 2. Receptor not discovered (missing IReceptor interface)
Solution: Problem: Wrong Handler Count
# Clean and rebuild
dotnet clean && dotnet build
# Check generated file
cat .whizbang/cache/message-registry.json | jq '.messages[] | select(.type == "MyApp.Commands.CreateOrder")'
# Verify receptor implements interface correctly
public class OrderReceptor : IReceptor<CreateOrder, OrderCreated> {
// Must have HandleAsync method
}
Problem: External Messages Not Found¶
Symptoms: Messages from referenced assemblies don't appear in registry.
Causes: 1. Message not used in any dispatcher/receptor/perspective 2. Assembly reference missing
Solution: Generator only includes messages that are actually used in the project. If you want external messages in registry, add at least one handler:
Problem: External Messages Not Found
// Add a perspective for external event
public class ExternalEventPerspective : IPerspectiveOf<SharedMessages.CustomerCreated> {
public async Task UpdateAsync(CustomerCreated @event, CancellationToken ct) {
// Now CustomerCreated appears in registry!
}
}
Further Reading¶
Source Generators: - Receptor Discovery - Compile-time receptor discovery - Perspective Discovery - Compile-time perspective discovery - Aggregate IDs - UUIDv7 generation for identity value objects - JSON Contexts - AOT-compatible JSON serialization
Core Concepts: - Dispatcher - Message routing patterns - Receptors - Message handlers - Perspectives - Event-driven read models
Tools: - VSCode Extension - IDE integration features
Version 1.0.0 - Foundation Release | Last Updated: 2024-12-12