Skip to content

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
[IntegrationEvents(typeof(OrderingEvents))]
public static partial class OrderingIntegrationEvents;

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")].

[IntegrationEventTopic]
public sealed class OrderingEvents
{
    [IntegrationEvent("ordering.order-placed")]
    public sealed record OrderPlaced(string OrderId, string CustomerId);
}

[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

  1. deepstaging sync (run automatically after build via MSBuild target, or continuously via deepstaging watch) opens the project and queries the projection layer
  2. The same IntegrationEvents.Publisher() query the generator uses discovers all wire names
  3. New wire names are appended to wire-manifest.json; existing entries are preserved
  4. The DSEQ11 analyzer reads wire-manifest.json and 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