Relays changes to a legacy SQL Server database's tables to message broker infrastructure as integration events, so systems that can't publish events themselves can take part in an event-driven architecture.
Your legacy system writes to a SQL Server database. It can’t create or publish integration events: its as-is architecture, its technical debt, or an inability to integrate with message broker infrastructure keeps it out of an event-driven architecture.
Think of a monolithic desktop app (WinForms or Swing), a VBA Excel app, or people editing tables directly. The data changes. Nobody downstream hears about it.
One requirement. The legacy system’s backend is SQL Server. The relay is tested against SQL Server 2019 and 2022.
On the other side, systems publish and subscribe to events through message broker infrastructure. The relay publishes integration events through any Chatter message broker infrastructure implementation, such as Azure Service Bus.
An integration event is the message the relay publishes. An attribute names the topic it goes to, and consumers subscribe to that topic.
OrderShippedEvent, from the README
[BrokeredMessage("order-shipped-event")]
public class OrderShippedEvent : IEvent
The bridge is a change source. It watches your tables for row changes. Each row change becomes a source event: the row’s values, typed by the mapping’s source event type.
Two change sources are available, and the stage draws them differently on purpose: SQL Service Broker is the dashed teal bridge with square packets, and SQL Server CDC is the solid rust bridge with round packets. One relay can use both, but each table uses only one.
Our running example is an Orders table. When OrderStatus changes to Shipped, the relay should publish an OrderShippedEvent. The relay runs in a .NET Worker Service, and it checks its configuration when the host starts and stops with a list of problems.
Row change
An insert, update or delete of one row.
Source event
The row’s values, typed by the mapping’s source event type.
Change source
What watches the tables: SQL Service Broker or CDC.
Source
A named, configured instance of a change source. Each mapping names one.
A mapping in configuration pairs a table and a change type with a source event type and an integration event type. Each mapping has exactly one mapper.
The mapper is your code. It decides whether the source event becomes an integration event and builds it. Return null and nothing is published. For updates the mapper sees both OldValue and NewValue.
The mapper’s test, from the README
// mappingData.OldValue only has a value for an update
if (mappingData.OldValue?.OrderStatus != "Shipped" && mappingData.NewValue?.OrderStatus == "Shipped")
In the example, OrderStatus changing to Shipped makes the mapper publish OrderShippedEvent to the order-shipped-event topic.
Mapping
Pairs a table and change type with a source event type and an integration event type.
Mapper
Your code: decides whether the source event becomes an integration event, and builds it.
Integration event
The message the relay publishes.
5
Chapter 5 of 9
Publish
The relay hands the integration event to the message broker infrastructure you configured. The topic named by [BrokeredMessage("order-shipped-event")] receives it, and consumers subscribe to that topic.
The legacy app never changed. It wrote one row, and the event-driven side heard an event.
Relay log (illustrative)
Processing Update of 'OrderChangedEvent' from source 'orders-cdc' with 1 mapping(s)
Published integration event 'OrderShippedEvent' to messaging infrastructure
Illustrative: the entity names and the count are made up. Only the message templates are the relay's.
Both bridges end at the same relay and the same mappers. They differ in how they see a change, and in what they promise. Pick the one that fits the table.
SQL Service Broker
Dashed, teal, square packets
Capture
A trigger on each watched table, in the writing transaction, adds modified rows to a Service Broker queue. The Worker consumes it.
Needs
A SQL Server with Service Broker. Most PaaS offerings don’t support it.
Latency
No CDC capture latency.
Order
Not guaranteed.
Message id
Generated by Chatter.
Capabilities
None.
After downtime
Changes wait in the queue.
SQL Server CDC
Solid, rust, round packets
Capture
The Worker polls the change tables that CDC’s capture job fills from the transaction log. No trigger on the table. The capture job scans the log every 5 seconds by default.
Needs
SQL Server Agent, sysadmin to enable CDC on the database, db_owner per table.
Order
Commit order per table (capture instance), no order across tables. Keeps the before-image of updates.
Message id
Derived from the source name, the change’s position and the integration event type.
Resumes from a checkpoint while the changes are still in the change table (retention default 3 days).
Use CDC when
mappers compare old and new values or depend on the order of changes (for example soft delete followed by restore), since CDC delivers each table’s changes in commit order and keeps the before-image of every update;
you can’t, or don’t want to, add triggers to the legacy system’s tables, or the writes must not pay for a trigger and a Service Broker send;
the relay must resume where it stopped after downtime, from changes still in the change table.
Use SQL Service Broker when
SQL Server Agent isn’t available (CDC’s capture job needs it), or CDC can’t be enabled on the database;
changes must reach the relay without CDC’s capture latency (the capture job scans the log every few seconds by default);
the table is already relayed this way and works as needed.
A mapping can require capabilities with RequiredCapabilities. The relay stops at startup if the mapping’s source doesn’t provide them.
Both change sources deliver at least once. That is a promise to plan around, so here is what keeps your place and what to do about repeats.
Checkpoint and same-id replay
With CDC, a batch is checkpointed only after every change in it is published. After a crash the batch is republished with the same message ids, derived from the source name, the change’s position and the integration event type. Enable duplicate detection on the topics (Azure Service Bus) or make consumers idempotent.
With SQL Service Broker, Chatter acknowledges by committing its receive transaction once processing succeeds, and Chatter’s recovery options retry failures.
Lease
Several relay instances can run with the same configuration. A lease (a SQL Server application lock) lets one of them poll each capture instance while the others stand by.
When a change keeps failing
Transient failures are retried with exponential backoff and never parked. For anything else you choose an error policy:
Halt (default) stops at the change and keeps retrying. Nothing after it is published, so order holds.
ParkRow parks the change in <Schema>.Chatter_RelayParkedChanges after MaxAttemptsBeforePark attempts. Later changes to the same row park behind it, and other rows carry on. On request, a replay republishes the row’s parked changes in order.
Request a replay, from the README
UPDATE Chatter.Chatter_RelayParkedChanges SET ReplayRequestedAt = SYSUTCDATETIME()
WHERE SourceName = N'orders-cdc' AND CaptureInstance = N'dbo_Orders' AND RowKey = N'<row key, e.g. [42]>';
Watching it
Metrics are published on the meter Chatter.IntegrationEventRelay.Cdc: chatter.relay.cdc.changes, chatter.relay.cdc.parked, chatter.relay.cdc.batch.failures, chatter.relay.cdc.batch.duration, chatter.relay.cdc.lag and chatter.relay.cdc.parked.current.
You can move a table from one bridge to the other. Two scripts do it: .scripts/cdc/switch-to-cdc.sql and .scripts/cdc/switch-to-service-broker.sql.
Each script picks a switch point. The source you switch away from relays every change committed before it, and the source you switch to relays every change committed after it. Every change is relayed by only one of the two sources, and each source is still at least once.
The switch commits in one transaction, or the script rolls all of it back and changes nothing.
The script locks the table, so writes to it pause until the script ends, normally a few seconds. Its waits for the relay, the capture job and the CDC sources end within about @timeoutSeconds (default 300); past it, the script rolls back and changes nothing. Some reads, the checks before the lock and, switching back, SqlChangeFeed’s install procedures and the table’s triggers aren’t bounded by @timeoutSeconds. If the script doesn’t finish, cancel it: that rolls it back too.
Mappers don’t change.
Integration tests run both scripts in both directions.
Three NuGet packages. The core maps row changes to integration events and publishes them, and a change source package captures the changes.
Chatter.IntegrationEventRelay
Core: mapping configuration, mapper interfaces, and the processor that maps row changes to integration events and publishes them. It doesn’t capture changes itself.
The SQL Service Broker change source. It uses Chatter.SqlChangeFeed, which installs a trigger on each watched table that sends changed rows to a SQL Service Broker queue.
The CDC change source. It polls each capture instance’s change table, hands changes to the processor in commit order, and saves a checkpoint in the source database once a batch is published.
Wire it up with SQL Service Broker (Program.cs), from the README
// Wires up the mapper of each mapping in configuration
.AddIntegrationEventRelay()
// Captures changes to the mapped tables with triggers and SQL Service Broker
.UseServiceBrokerChangeSource();