Attribute Reference¶
The Integration Events module uses four attributes: one to mark topic classes, one to declare a publisher, one to connect handlers (subscribers), and one to assign stable wire names to event types.
[IntegrationEventTopic]¶
Marks a class as a topic — a container for [IntegrationEvent] records representing a bounded context's outbound event contract.
| Property | Type | Default | Description |
|---|---|---|---|
Name |
string? |
Auto-derived | Override the kebab-cased topic name (e.g., CatalogEvents → "catalog-events") |
[IntegrationEventTopic]
public sealed class CatalogEvents
{
[IntegrationEvent("catalog.product-price-changed")]
public sealed record ProductPriceChanged(CatalogItemId ItemId, decimal OldPrice, decimal NewPrice);
}
Types referenced by [IntegrationEvents(typeof(X))] or [IntegrationEventHandler<R>(typeof(X))] must carry this attribute. Omitting it produces DSEQ07.
[IntegrationEvents]¶
Marks a static partial class as a publisher for the integration events defined in a topic class. The generator emits Enqueue effect methods, an EventTypeRegistry, and DI registration.
| Property | Type | Default | Description |
|---|---|---|---|
TopicType |
Type |
(required, positional) | Class marked with [IntegrationEventTopic] containing nested [IntegrationEvent] types |
Capacity |
int |
10000 |
Channel capacity. 0 = unbounded. |
SingleReader |
bool |
true |
Optimize channel for single reader |
SingleWriter |
bool |
false |
Optimize channel for single writer |
Topic class convention
The topic type must be a sealed class marked with [IntegrationEventTopic] in a shared contracts project, containing nested event records. Each nested type must carry [IntegrationEvent("wire-name")].
[IntegrationEventHandler<TRuntime>]¶
Links a static partial class of handler methods to a topic (or queue name) and a runtime. Two constructors offer flexibility:
- Type constructor — compile-safe reference to a topic class in a shared contracts project (preferred)
- String constructor — for external topics not in the solution
| Property | Type | Description |
|---|---|---|
TopicType |
Type? |
Topic class reference (when using Type constructor) |
QueueName |
string? |
Queue name string (when using string constructor) |
MaxConcurrency |
int |
Max parallel handlers. 1 = sequential. Default: 1 |
TimeoutMilliseconds |
int |
Per-event handler timeout. 0 = no timeout. Default: 0 |
MaxRetries |
int |
Total retry attempts before dead-lettering. Default: 7 |
// Preferred: compile-safe topic reference
[IntegrationEventHandler<OrderingRuntime>(typeof(CatalogEvents))]
public static partial class OrderingCatalogEventHandlers
{
public static Eff<OrderingRuntime, Unit> Handle(CatalogEvents.OrderStockConfirmed evt) =>
OrderingDispatch.ConfirmOrderStock(evt.OrderId);
public static Eff<OrderingRuntime, Unit> Handle(CatalogEvents.OrderStockRejected evt) =>
OrderingDispatch.RejectOrderStock(evt.OrderId, evt.RejectedItems).AsUnit();
}
Handler method convention
Handler methods must be static, return Eff<TRuntime, Unit>, and accept a single event parameter. The parameter type determines which events are routed to that handler — and each parameter type must carry an [IntegrationEvent] attribute (enforced by DSEQ06).
[IntegrationEvent]¶
Marks an event type with a stable wire name for cross-transport serialization. The name is used as a type discriminator when events are serialized to external transports (e.g., Azure Service Bus) and must be unique within the system.
| Property | Type | Description |
|---|---|---|
Name |
string |
(required, positional) Stable wire name for this event type |
public sealed class CatalogEvents
{
[IntegrationEvent("catalog.product-price-changed")]
public sealed record ProductPriceChanged(
CatalogItemId ItemId,
decimal OldPrice,
decimal NewPrice);
[IntegrationEvent("catalog.order-stock-confirmed")]
public sealed record OrderStockConfirmed(OrderId OrderId);
}
Wire name stability
The name must never change once deployed to production. It is persisted in external transport messages and used for polymorphic deserialization via the generated EventTypeRegistry. The wire name manifest system enforces this at compile time — see below.
Naming convention
Follow the pattern {context}.{event-name} — e.g., "ordering.checkout-accepted", "inventory.stock-reserved". This scopes events to their bounded context and avoids collisions.
DSEQ06 enforcement
All event types handled by [IntegrationEventHandler] methods must carry [IntegrationEvent]. The analyzer raises DSEQ06 if any handler parameter type is missing it. See Diagnostics.
Wire Name Manifest¶
The wire name manifest system prevents accidental deletion or renaming of wire names. A wire-manifest.json file in your project root tracks every wire name that has ever been registered. The Deepstaging CLI auto-updates this file on build, and a Roslyn analyzer flags regressions.
How it works¶
deepstaging sync(run automatically after build via MSBuild target, or continuously viadeepstaging watch) opens the project and queries the projection layer- The same
IntegrationEvents.Publisher()query the generator uses discovers all wire names - New wire names are appended to
wire-manifest.json; existing entries are preserved - The DSEQ11 analyzer reads
wire-manifest.jsonand reports an error if any active wire name is missing from the current compilation
The manifest file¶
Commit wire-manifest.json to source control. It is append-only — entries are never deleted, only retired:
{
"version": 1,
"topics": {
"ordering-events": {
"entries": [
{
"wireName": "ordering.order-placed",
"clrType": "Ordering.Events.OrderPlaced",
"registered": "2026-01-15",
"status": "active"
}
]
}
}
}
New wire names are added automatically when you build. Changes to the manifest appear in your PR diff — reviewers can see exactly which wire names were added.
Retiring a wire name¶
When you intentionally remove an event type, the build fails with DSEQ11. To acknowledge the removal, edit wire-manifest.json and set the entry's status to "retired":
{
"wireName": "ordering.old-event",
"clrType": "Ordering.Events.OldEvent",
"registered": "2025-06-01",
"retired": "2026-03-23",
"status": "retired"
}
Retired entries stay in the manifest permanently — they document that the wire name existed and must never be reused for a different type.
Diagnostics¶
| ID | Severity | Description |
|---|---|---|
| DSEQ08 | Error | Two [IntegrationEvent] attributes use the same wire name string for different types |
| DSEQ11 | Error | An active wire name in wire-manifest.json has no corresponding [IntegrationEvent] in the compilation |