Event schema documentation per service, cross-service event registry, naming conventions, versioning strategy, and catalogue generation from code. Trigger: event catalogue, event registry, event schema, event documentation.
From dotnet-ai-kitnpx claudepluginhub faysilalshareef/dotnet-ai-kit --plugin dotnet-ai-kitThis skill uses the workspace's default tool permissions.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Compares coding agents like Claude Code and Aider on custom YAML-defined codebase tasks using git worktrees, measuring pass rate, cost, time, and consistency.
Event<TData> types ARE the catalogue; documentation is generated, not hand-writtenVersion field; the catalogue tracks all active versionsOrderCreated, never CreateOrder)All event names follow a strict pattern: {Aggregate}{PastTenseVerb}.
OrderCreated -- aggregate creation
OrderUpdated -- aggregate field changes
OrderItemsAdded -- collection modification
OrderItemsRemoved -- collection modification
OrderCompleted -- state transition
OrderCancelled -- state transition
InvoiceGenerated -- derived aggregate creation
PaymentRefunded -- financial reversal
Rules:
Created, Updated, Cancelled, never Create, Update, Cancel)OrderCreated, not CreatedOrder)DataChanged, EntityUpdated) -- be specific about what changedOrderCompleted, not OrderFinished)OrderItemsAdded, not OrderModified)Use sealed records for event data types to prevent inheritance and guarantee immutability:
namespace Acme.Orders.Commands.Domain.Events.DataTypes;
public sealed record OrderCreatedData(
string CustomerName,
string CustomerEmail,
decimal Total,
OrderStatus Status,
List<OrderLineItem> LineItems
) : IEventData
{
public EventType Type => EventType.OrderCreated;
}
public sealed record OrderLineItem(
Guid ProductId,
string ProductName,
int Quantity,
decimal UnitPrice);
public sealed record OrderCancelledData(
string Reason,
Guid? CancelledByUserId
) : IEventData
{
public EventType Type => EventType.OrderCancelled;
}
Key details:
sealed prevents subclassing -- each event data type is a leaf in the hierarchyinit-only by defaultOrderLineItem) document composite payloads without implementing IEventDataEach event in the catalogue follows a standard documentation block:
## OrderCreated (v2)
| Field | Type | Required | Description |
|-----------------|-------------------|----------|--------------------------------------|
| CustomerName | string | Yes | Full name of the ordering customer |
| CustomerEmail | string? | No | Email address (added in v2) |
| Total | decimal | Yes | Order total including tax |
| Status | OrderStatus | Yes | Initial status (always Pending) |
| LineItems | List<OrderLineItem> | Yes | Products in the order |
**Aggregate:** Order
**Produced by:** Orders.Command
**Consumed by:** Orders.Query, Invoicing.Processor, Notifications.Processor
**Version history:** v1 (2024-01), v2 (2024-06 -- added CustomerEmail)
Each service maintains its own events.json manifest listing the events it produces:
{
"$schema": "event-catalogue/v1",
"service": "Orders.Command",
"domain": "Orders",
"events": [
{
"name": "OrderCreated",
"aggregate": "Order",
"version": 2,
"dataType": "OrderCreatedData",
"namespace": "Acme.Orders.Commands.Domain.Events.DataTypes",
"fields": [
{ "name": "CustomerName", "type": "string", "required": true },
{ "name": "CustomerEmail", "type": "string?", "required": false },
{ "name": "Total", "type": "decimal", "required": true },
{ "name": "Status", "type": "OrderStatus", "required": true },
{ "name": "LineItems", "type": "List<OrderLineItem>", "required": true }
],
"consumers": ["Orders.Query", "Invoicing.Processor", "Notifications.Processor"]
},
{
"name": "OrderCancelled",
"aggregate": "Order",
"version": 1,
"dataType": "OrderCancelledData",
"namespace": "Acme.Orders.Commands.Domain.Events.DataTypes",
"fields": [
{ "name": "Reason", "type": "string", "required": true },
{ "name": "CancelledByUserId", "type": "Guid?", "required": false }
],
"consumers": ["Orders.Query", "Invoicing.Processor"]
}
]
}
A single event-registry/ repository aggregates all per-service manifests. Use this when:
Generate the catalogue by reflecting over IEventData implementations at build time:
namespace Acme.Shared.EventCatalogue;
public sealed class EventCatalogueGenerator
{
public static List<EventSchemaEntry> GenerateFromAssembly(Assembly assembly)
{
var entries = new List<EventSchemaEntry>();
var eventDataTypes = assembly.GetTypes()
.Where(t => t.IsAssignableTo(typeof(IEventData))
&& t is { IsInterface: false, IsAbstract: false });
foreach (var type in eventDataTypes)
{
var instance = Activator.CreateInstance(type, nonPublic: true);
var eventType = type.GetProperty("Type")?.GetValue(instance);
var fields = type.GetConstructors()
.First()
.GetParameters()
.Select(p => new FieldEntry(
Name: p.Name!,
Type: FormatTypeName(p.ParameterType),
Required: !IsNullable(p)))
.ToList();
entries.Add(new EventSchemaEntry(
Name: eventType?.ToString() ?? type.Name,
DataType: type.Name,
Namespace: type.Namespace ?? string.Empty,
Fields: fields));
}
return entries;
}
private static string FormatTypeName(Type type)
{
if (Nullable.GetUnderlyingType(type) is { } underlying)
return $"{underlying.Name}?";
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>))
return $"List<{type.GenericTypeArguments[0].Name}>";
return type.Name;
}
private static bool IsNullable(ParameterInfo param) =>
Nullable.GetUnderlyingType(param.ParameterType) is not null
|| new NullabilityInfoContext().Create(param).WriteState == NullabilityState.Nullable;
}
public sealed record EventSchemaEntry(
string Name,
string DataType,
string Namespace,
List<FieldEntry> Fields);
public sealed record FieldEntry(
string Name,
string Type,
bool Required);
Usage in a build-time tool or test:
var assembly = typeof(OrderCreatedData).Assembly;
var catalogue = EventCatalogueGenerator.GenerateFromAssembly(assembly);
var json = JsonSerializer.Serialize(catalogue, new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
});
File.WriteAllText("events.json", json);
Generate a Mermaid diagram showing event producers, consumers, and the message broker:
flowchart LR
subgraph Producers
OC[Orders.Command]
IC[Invoicing.Command]
end
subgraph "Message Broker (Azure Service Bus)"
OT[orders-topic]
IT[invoicing-topic]
end
subgraph Consumers
OQ[Orders.Query]
IP[Invoicing.Processor]
NP[Notifications.Processor]
end
OC -- "OrderCreated\nOrderUpdated\nOrderCancelled" --> OT
IC -- "InvoiceGenerated\nPaymentRefunded" --> IT
OT --> OQ
OT --> IP
OT --> NP
IT --> OQ
IT --> NP
Per-aggregate event lifecycle diagram:
stateDiagram-v2
[*] --> Pending : OrderCreated
Pending --> Confirmed : OrderConfirmed
Confirmed --> Shipped : OrderShipped
Shipped --> Delivered : OrderDelivered
Pending --> Cancelled : OrderCancelled
Confirmed --> Cancelled : OrderCancelled
Delivered --> [*]
Cancelled --> [*]
Add a test that verifies the catalogue stays in sync with the code:
public sealed class EventCatalogueTests
{
[Fact]
public void AllEventDataTypes_AreDocumentedInCatalogue()
{
var assembly = typeof(OrderCreatedData).Assembly;
var actual = EventCatalogueGenerator.GenerateFromAssembly(assembly);
var catalogueJson = File.ReadAllText("events.json");
var documented = JsonSerializer.Deserialize<List<EventSchemaEntry>>(catalogueJson)!;
var undocumented = actual
.Where(a => !documented.Any(d => d.DataType == a.DataType))
.ToList();
Assert.Empty(undocumented);
}
[Fact]
public void AllEventDataTypes_AreSealedRecords()
{
var assembly = typeof(OrderCreatedData).Assembly;
var eventDataTypes = assembly.GetTypes()
.Where(t => t.IsAssignableTo(typeof(IEventData))
&& t is { IsInterface: false, IsAbstract: false });
foreach (var type in eventDataTypes)
{
Assert.True(type.IsSealed,
$"{type.Name} must be sealed");
Assert.True(type.GetMethod("<Clone>$") is not null,
$"{type.Name} must be a record (missing Clone method)");
}
}
}
| Scenario | Approach | Reason |
|---|---|---|
| 2-3 services, same team | Per-service events.json checked into each repo | Low overhead, discoverable via code search |
| 5+ services, multiple teams | Centralized registry repo with CI validation | Single portal, contract-breaking detection |
| Strict governance required | Generated catalogue + build-time validation tests | Prevents undocumented events from shipping |
| Rapid prototyping | Skip formal catalogue, use code as documentation | Avoid ceremony that slows iteration |
| AsyncAPI compliance needed | Generate AsyncAPI spec from events.json manifests | Industry-standard format for event docs |
| Event schemas rarely change | Per-service manifest, manual updates | Reflection-based generation is overkill |
| Event schemas change frequently | Build-time generation + test enforcement | Manual updates fall out of sync |
| Anti-Pattern | Correct Approach |
|---|---|
| Hand-writing event docs in a wiki | Generate from IEventData types; code is the source of truth |
Using present-tense event names (CreateOrder) | Past tense only (OrderCreated) -- events describe facts |
Generic event names (DataChanged, Updated) | Specific names scoped to aggregate (OrderItemsAdded) |
| Mutable classes for event data | Sealed records with positional syntax |
| No version tracking in catalogue | Include version number and version history per event |
| Centralized registry without CI checks | Validate consumer compatibility on every PR |
| Documenting events without listing consumers | Always track which services consume each event |
| Catalogue only in Confluence/wiki | Keep catalogue in the repo (generated or checked in) |
| Skipping nested type documentation | Document composite types like OrderLineItem alongside the parent |
One giant EventData class with optional fields | Separate sealed record per event type |
# Find all IEventData implementations
grep -r ": IEventData" --include="*.cs" src/
# Find existing event catalogue or registry files
find . -name "events.json" -o -name "event-catalogue*" -o -name "event-registry*"
# Find EventType enums across services
grep -r "enum EventType" --include="*.cs" src/
# Find sealed record event data types
grep -r "sealed record.*Data" --include="*.cs" src/
# Find existing Mermaid diagrams referencing events
grep -r "OrderCreated\|EventType\|event-topic" --include="*.md" docs/
grep -r ": IEventData" --include="*.cs" to find all event data typessealed record with positional syntaxEventCatalogueGenerator in a shared project or test projectevents.json by running the generator against each command service assembly{Aggregate}{PastTenseVerb} convention