1. What Is CAP and Why Do We Need It?
In microservices architectures, services often need to communicate with each other asynchronously to remain decoupled, scalable, and fault-tolerant. Event-driven communication using message queues like RabbitMQ or Kafka is a popular solution — but it comes with its own set of challenges, especially around reliability and distributed transaction consistency.
This is where CAP (Distributed Transaction Solution in .NET Core) comes in.
CAP is an open-source .NET library designed to help developers manage reliable event publishing and consumption across services, while also handling eventual consistency in database transactions. It follows the Outbox Pattern, allowing you to store events in your local database as part of a transaction and forward them to a message queue only after the transaction succeeds.
Problems CAP Solves
- Distributed Transaction Complexity: Ensures that data changes and event publishing happen atomically, without requiring a distributed transaction coordinator.
- Message Loss Prevention: Persist events locally before sending, so that messages are not lost even if the broker or consumer service is temporarily down.
- Retry & Monitoring: Built-in retry logic and a web dashboard for tracking sent and received messages.
With just a few lines of configuration, CAP lets your services publish and consume events reliably, while abstracting away the complexities of queue connection, message delivery, error handling, and storage.
2. How to Integrate CAP in a .NET Microservice
In this section, we'll cover how to add CAP to your .NET microservice, including the required NuGet packages, how to configure RabbitMQ and the database, and the meaning of the DefaultGroup
setting for subscribers.
2.1 Install Required Packages
You need to install the following NuGet packages:
For EF Core + RabbitMQ setup:
dotnet add package DotNetCore.CAP
dotnet add package DotNetCore.CAP.RabbitMQ
dotnet add package DotNetCore.CAP.EntityFrameworkCore
2.2 Configure CAP in Program.cs
First, make sure your DbContext
is already registered with EF Core:
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseMySql(builder.Configuration.GetConnectionString("DefaultConnection"),
ServerVersion.AutoDetect(builder.Configuration.GetConnectionString("DefaultConnection"))));
builder.Services.AddCap(x =>
{
x.UseEntityFramework<AppDbContext>(); // Use EF Core to store CAP events
x.UseRabbitMQ(cfg =>
{
cfg.HostName = builder.Configuration["CAP:RabbitMQ:HostName"];
cfg.UserName = builder.Configuration["CAP:RabbitMQ:UserName"];
cfg.Password = builder.Configuration["CAP:RabbitMQ:Password"];
});
});
2.3 Configuration File
You can store the CAP configuration in appsettings.json
:
"CAP": {
"DefaultGroup": "order.service",
"RabbitMQ": {
"HostName": "localhost",
"UserName": "guest",
"Password": "guest"
}
}
2.4 What Is DefaultGroup?
The DefaultGroup
defines the consumer group name for this service. It's used to distinguish multiple subscribers of the same topic in different services. CAP ensures that only one subscriber within the same group will consume the message.
- Services with different group names can receive the same event (fan-out).
- Services with the same group name will compete to consume the event (load balancing).
2.5 Do Publisher and Subscriber Use the Same Configuration?
Yes, both the sender and receiver services use the same basic CAP configuration. The only difference is that:
- The publisher uses
ICapPublisher
to send messages. - The subscriber uses the
[CapSubscribe]
attribute to receive messages.
Both sides must be properly configured with the same transport (e.g., RabbitMQ) and the same storage (e.g., MySQL via EF Core) to work together.
3. Running RabbitMQ with Docker (Local Setup)
To enable CAP to work properly, we need a running instance of a message broker. RabbitMQ is one of the supported brokers and works well for local development and testing.
Step 1: Pull the Official RabbitMQ Image
docker pull rabbitmq:3-management
- This image includes the RabbitMQ server and the Management UI plugin.
Step 2: Run the RabbitMQ Container
docker run -d \
--hostname rabbitmq \
--name rabbitmq \
-p 5672:5672 \
-p 15672:15672 \
rabbitmq:3-management
Explanation:
-
--name rabbitmq
: Names the container so you can manage it easily. -
--hostname rabbitmq
: Sets the hostname inside the container (optional). -
-p 5672:5672
: Exposes the RabbitMQ AMQP port. -
-p 15672:15672
: Exposes the RabbitMQ Management UI port. -
-d
: Runs the container in detached mode.
Step 3: Access the RabbitMQ Management UI
- Open your browser and go to: http://localhost:15672
-
Default login credentials:
-
Username:
guest
-
Password:
guest
-
Username:
Summary
With just two commands (pull
and run
), you’ll have a fully functional RabbitMQ server running locally, along with a powerful UI for inspecting exchanges, queues, and messages — ideal for working with CAP.
4. Publish and Subscribe with CAP
In the sender service, inject ICapPublisher
and use it to publish an event:
public class OrderService
{
private readonly ICapPublisher _capBus;
public OrderService(ICapPublisher capBus)
{
_capBus = capBus;
}
public async Task PlaceOrderAsync()
{
await _capBus.PublishAsync("order.created", new { OrderId = 123, Amount = 99.9 });
}
}
In the receiver service, add a method with [CapSubscribe]
to handle the event:
public class OrderEventHandler
{
[CapSubscribe("order.created")]
public void HandleOrderCreated(dynamic data)
{
Console.WriteLine($"Order received: {data.OrderId}, Amount: {data.Amount}");
}
}
Both services need to be connected to the same message broker and configured with CAP.
5. CAP Message Tables
CAP relies on two key tables in your database to ensure message reliability and traceability:
cap.published
: This table stores all the messages that have been published. When a message is sent viaICapPublisher
, it first gets recorded here. CAP will attempt to deliver this message based on the retry policy if the subscriber is not ready.cap.received
: Once a message is successfully consumed by a subscriber, it will be recorded in this table. This acts as a log of all successfully handled messages.
If you're using in-memory storage instead of a real database, these records will not be persisted after a service restart.
CAP ensures that messages are reliably transferred and tracked between microservices using this storage mechanism.
6. Why CAP Is Powerful
One of the most powerful features of CAP is its simplicity.
-
You only need to configure the database and message broker once, and CAP will automatically handle:
- Reliable message delivery
- Retry mechanisms
- Failure tracking
- Distributed transaction support (Outbox pattern)
No extra queue management or manual retry logic is needed.
With just
[CapSubscribe]
andICapPublisher
, microservices can communicate reliably and efficiently.
CAP abstracts away much of the complexity of building a robust EventBus system. Once configured, your services can send and receive events without worrying about infrastructure details.
Top comments (0)