Skip to content
bitzorcas
中EN

Reference

Eventing Building Block

Reference for CAP, RabbitMQ, SQL Server Outbox, topic contracts, consumer idempotency, and failed-message operations.

Last updated

BitzOrcas uses DotNetCore.CAP to connect a SQL Server Outbox to RabbitMQ. This solves the dual-write decision between committing a database transaction and publishing a message. It does not turn distributed messaging into exactly-once delivery. Producers get a transactional Outbox; consumers still design for at-least-once delivery.

Critical path diagram

Follow the main path to identify responsibility handoffs, then use the prose to inspect failure branches and evidence.

Domain event

Transaction commit

Outbox / CAP publish

Integration event

Idempotent consumer

Producer path

CapSqlSugarUnitOfWork opens a CAP-aware transaction on the shared DbConnection and injects the same DbTransaction into SqlSugar. Business writes and the CAP Outbox INSERT commit or roll back together.

Command
↓
CAP-aware SQL transaction
├─ business rows
└─ CAP Outbox row
↓ commit
CAP dispatcher → RabbitMQ → consumer

The atomic dual-write guarantee applies only when CAP publish occurs while this transaction is open. The post-commit domain-event bridge is best effort; see Eventing Abstractions for that boundary.

Topics and contracts

The publisher owns the topic and payload contract. A typed event can pin its topic with [IntegrationTopic("tickets.ticket.opened")]. Without an explicit topic, the CAP adapter falls back to the type’s full name. Long-lived public topics should be explicit and have a version policy.

Contract evolution rules:

  • Prefer additive optional fields; do not remove fields or change existing meaning.
  • Do not publish database entities or internal enum ordinals.
  • Event ID, tenant ID, and occurrence time support diagnosis and idempotency.
  • Use a new topic or version for breaking change and allow old and new consumers to migrate in parallel.

Consumer modules expose subscribed-topic constants from Contracts or Application for module governance reports. Runtime consumers implement ICapSubscribe with [CapSubscribe] in Infrastructure. The reported catalog and runtime attributes must remain aligned.

Consumer rules

After validating contract and tenant identity, a consumer invokes an owner-local use case. Common idempotency patterns include:

  1. A unique processed record keyed by eventId + consumer.
  2. A conditional state transition on the target aggregate.
  3. An upsert using a business unique key for projections.

Commit the business effect and idempotency marker in the same transaction where possible. Do not mark a message handled before the side effect, and do not use an in-process HashSet as durable deduplication.

A consumer should throw on failure so CAP records and retries it. The current CAP configuration attempts failed delivery five times. Global CapConsumerAuditFilter writes execution audit, while the operations module can query and perform controlled retries of failed CAP messages.

Transactional publish use case

This use case keeps state persistence and event publication inside one Handler surrounded by the transaction behavior. TicketOpenedIntegrationEvent is a stable contract owned by Tickets Contracts.

public sealed class OpenTicketHandler(
ITicketStore tickets,
IIntegrationEventPublisher<TicketOpenedIntegrationEvent> events)
{
public async Task<Result> Handle(OpenTicket command, CancellationToken ct)
{
// The aggregate protects its transition. Expected failure returns and rolls back.
var ticket = await tickets.GetAsync(command.TicketId, ct);
var opened = ticket.Open(command.Subject);
if (opened.IsFailure) return opened;
// Save and CAP Publish run while the CAP-aware transaction is still open.
await tickets.SaveAsync(ticket, ct);
await events.PublishAsync(
TicketOpenedIntegrationEvent.From(ticket), ct);
return Result.Success();
}
}

The Handler does not call CommitAsync(). TransactionPipelineBehavior commits or rolls back from the Result and exception outcome. Moving PublishAsync into an after-commit callback would remove the atomic Outbox property used by this example.

Idempotent consumer use case

public sealed class TicketOpenedConsumer(
IProcessedEventStore processed,
IReportingTicketProjection projection)
{
[CapSubscribe(TicketOpenedIntegrationEvent.Topic)]
public async Task Consume(TicketOpenedIntegrationEvent message, CancellationToken ct)
{
// eventId + consumerName needs a database unique constraint; process cache is insufficient.
if (await processed.ExistsAsync(message.EventId, nameof(TicketOpenedConsumer), ct))
return;
// Commit the projection change and processed marker in one local transaction.
await projection.UpsertAsync(message.TenantId, message.TicketId, message.Subject, ct);
await processed.AddAsync(message.EventId, nameof(TicketOpenedConsumer), ct);
}
}

The unique constraint is the final defense when the same event arrives concurrently. If AddAsync conflicts, treat it as idempotent success only after proving the competing execution committed the projection; otherwise fail and let CAP retry.

Failure boundaries

FailureExpected behavior
Business transaction rolls backBusiness and Outbox rows both roll back
RabbitMQ is temporarily unavailableOutbox remains and dispatch resumes later
Consumer throwsCAP retries and records failure
Consumer succeeds but acknowledgement is lostDuplicate delivery is possible; business idempotency handles it
Post-commit best-effort publish failsBusiness already succeeded; alert and explicit compensation are required

Release gate

  • SQL Server, CAP tables, and RabbitMQ configuration pass health and readiness checks.
  • Topic catalog and runtime subscriptions agree; no orphan or misspelled topics remain.
  • Every consumer has duplicate, out-of-order, old-version, and poison-message tests.
  • Failed-message backlog, retry rate, consumer latency, and oldest-message age are monitored.
  • A critical flow can be traced from event ID and CorrelationId through producer and consumer audit.

Source and verification entry points

  • CAP-aware units of work: CapSqlSugarUnitOfWork and CapEfCoreUnitOfWork.
  • Typed publishing adapter: CapIntegrationEventPublisher<TEvent>.
  • Domain-event bridge: DomainEventDispatchPipelineBehavior and IIntegrationTopicRegistry.
  • Consumer audit: CapConsumerAuditFilter; failed-message operations live in OpsExtension.
  • Integration evidence covers business/Outbox commit and rollback, dispatch after broker recovery, duplicate consumption, and controlled retry.

See Eventing Abstractions for contract and port selection.

100%

Scroll or use controls to zoom · drag when enlarged · double-click for 100% / 200%