I'm always excited to take on new projects and collaborate with innovative minds.
A practical walkthrough of the DotNetCore.CAP event bus in .NET 6 — how the transactional outbox pattern saves your messages from the "record saved, message never sent" trap, complete with a real publish–consume pipeline you can run locally.
If you have ever built a system where a database write and a message publish need to happen together, you have likely encountered the dual-write problem.
Consider a typical scenario: you write an order to the database and then attempt to publish a NewOrder event to your message queue. If the publish step fails due to a network blip, an application crash, or expired credentials, your system ends up in an inconsistent state. The order exists in your database, but your message bus remains unaware of it. The record is saved, but downstream services are never notified.
This is the exact problem DotNetCore.CAP addresses. CAP is a distributed event bus for .NET that implements the Transactional Outbox Pattern. This article explores an end-to-end .NET 6 API implementation that demonstrates how CAP handles this pipeline cleanly.
In a typical e-commerce architecture, placing an order involves two main steps:
Because step two occurs after the database transaction commits, a failure during message publication leaves the database out of sync with the rest of the system.
Attempting to wrap both operations in a single distributed transaction (using mechanisms like MSDTC) introduces significant complexity, performance bottlenecks, and infrastructure constraints. Modern cloud environments and databases rarely handle distributed transactions well.
The Transactional Outbox Pattern resolves this by shifting when and where the message is written:
Because the outbox record is part of the local database transaction, data loss between state changes and message emission is eliminated. If message delivery fails, the background worker can safely retry until successful.
CAP automates this pattern. It integrates with your database connection, manages the underlying outbox tables, delivers messages to your chosen transport (Azure Service Bus, RabbitMQ, Kafka, AWS SQS), tracks execution status, and manages retries automatically.
The accompanying sample is an ASP.NET Core API demonstrating the complete publish, store, deliver, and consume lifecycle:
GET /WeatherForecast request is triggered.helloWorld message inside an active database transaction.[CapSubscribe("helloWorld")] receives and processes the payload.Publishing relies on injecting ICapPublisher into your controller or service. CAP integrates directly into your database transaction:
using(var connection = new MySqlConnection(_mySqlConnectionString))
{
using(var transaction = connection.BeginTransaction(_capPublisher))
{
await _capPublisher.PublishAsync("helloWorld", "TestDemo");
transaction.Commit();
}
}Passing _capPublisher into connection.BeginTransaction(...) binds the published event to the local database transaction.
transaction.Commit() succeeds, the outbox entry is saved.In production applications, the payload ("TestDemo") is typically a strongly typed DTO representing an event such as order.created or payment.succeeded.
On the consuming side, a background service implements ICapSubscribe and marks subscriber methods with the CapSubscribe attribute matching the event name:
public class ReceiverService : IReceiverService, ICapSubscribe
{
private readonly ILogger<ReceiverService> _logger;
public ReceiverService(ILogger<ReceiverService> logger)
{
_logger = logger;
}
[CapSubscribe("helloWorld")]
public void Receive(string value)
{
_logger.Log(LogLevel.Debug, value);
}
}
CAP handles topic subscription, payload deserialization, and method execution automatically without requiring custom polling logic or broker-specific client libraries.
Configuration occurs in Program.cs. Three primary extension methods configure CAP's storage, transport, and monitoring mechanisms:
builder.Services.AddCap(x =>
{
x.UseSqlServer(sqlServerConnectionString);
x.UseAzureServiceBus(azureServiceBusConnectionString);
x.UseDashboard();
});UseSqlServer: Specifies where CAP creates and manages its outbox and inbox tables.UseAzureServiceBus: Configures the transport layer responsible for message delivery.UseDashboard: Enables an integrated web dashboard to inspect real-time message states, execution logs, and failure retries.Running the application requires .NET 6, access to SQL Server, and an Azure Service Bus connection string (or local emulator).
Once running, two endpoints become accessible:
/swagger): Used to execute requests against GET /WeatherForecast./cap): Provides real-time visibility into message states (Published, Delivered, Received, or Failed), processing timestamps, and retry metrics.Executing requests via Swagger populates the CAP dashboard, illustrating the progression of messages through the outbox workflow.
CAP is well-suited for distributed systems using relational databases and message brokers where consistency across services is critical.
Key considerations when adopting this pattern include:
This demo illustrates how DotNetCore.CAP simplifies the Transactional Outbox Pattern in .NET applications. By coupling message publishing directly to database transactions and abstracting transport logic, CAP mitigates data inconsistency issues in distributed architectures.
Your email address will not be published. Required fields are marked *