DEV Community

Alex
Alex

Posted on

.NET Learning Notes: Implementing EventBus with CAP and RabbitMQ in Microservices

CAP Doc

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
Enter fullscreen mode Exit fullscreen mode

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"];
    });
});
Enter fullscreen mode Exit fullscreen mode

2.3 Configuration File

You can store the CAP configuration in appsettings.json:

"CAP": {
  "DefaultGroup": "order.service",
  "RabbitMQ": {
    "HostName": "localhost",
    "UserName": "guest",
    "Password": "guest"
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
  • 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
Enter fullscreen mode Exit fullscreen mode

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

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 });
    }
}
Enter fullscreen mode Exit fullscreen mode

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}");
    }
}
Enter fullscreen mode Exit fullscreen mode

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 via ICapPublisher, 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] and ICapPublisher, 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)