Skip to content

Messaging Overview

Modulus provides an in-house messaging transport layer for cross-module communication via integration events. It supports InMemory, RabbitMQ, and Azure Service Bus transports, and includes built-in transactional outbox and inbox patterns for reliable, exactly-once message delivery. There is no MassTransit (or other framework) dependency -- the broker transports are built directly on the official RabbitMQ.Client and Azure.Messaging.ServiceBus clients.

Why an In-House Transport Layer?

ConcernModulus Messaging
Swap transports freelySwitch between InMemory, RabbitMQ, and Azure Service Bus by changing a single configuration value. No handler code changes required.
Clean handler interfaceImplement IIntegrationEventHandler<TEvent> -- no broker-specific consumer types or context ceremony.
Automatic idempotencyThe consumer pipeline enforces at-most-once execution per (EventId, handlerName) when the inbox store is registered.
Transactional outbox built-inPersist events as database rows and let a background processor publish them reliably to the broker. Map the outbox into your own DbContext for same-transaction atomicity with domain state.
Convention-based discoveryHandlers are auto-discovered from the assemblies you specify -- no manual consumer registration.
No licensed dependenciesThe whole stack is MIT-licensed, built on the official broker client libraries.

Installation

If you scaffolded your solution with the Modulus CLI, the messaging package is already referenced. To add it manually:

bash
dotnet add package ModulusKit.Messaging

The core package includes the in-memory transport. RabbitMQ and Azure Service Bus ship as separate transport packages -- ModulusKit.Messaging.RabbitMq and ModulusKit.Messaging.AzureServiceBus -- each activated with a one-line registration. See Transports.

Quick Setup

Register messaging in your host project's Program.cs or composition root. The recommended way is to bind the Messaging section from configuration — this is the section modulus init --transport scaffolds into appsettings.json:

json
// appsettings.json
{
  "Messaging": {
    "Transport": "RabbitMq",
    "ConnectionString": "amqp://guest:guest@localhost:5672"
  }
}
csharp
using Modulus.Messaging;
using Modulus.Messaging.RabbitMq;

var builder = WebApplication.CreateBuilder(args);

// One line per broker transport (from ModulusKit.Messaging.RabbitMq);
// not needed for the built-in InMemory transport.
builder.Services.AddModulusRabbitMqTransport();

builder.Services.AddModulusMessaging(builder.Configuration, options =>
{
    options.Assemblies.Add(typeof(Program).Assembly);
});

The callback supplies values that cannot be bound from configuration — the handler assemblies and an optional Azure TokenCredential — and runs after binding, so it can also override any bound value.

You can add multiple assemblies to scan for handlers across all your modules:

csharp
builder.Services.AddModulusMessaging(builder.Configuration, options =>
{
    options.Assemblies.Add(typeof(CatalogModule).Assembly);
    options.Assemblies.Add(typeof(OrdersModule).Assembly);
    options.Assemblies.Add(typeof(PaymentModule).Assembly);
});

Or configure everything imperatively without a configuration section:

csharp
builder.Services.AddModulusRabbitMqTransport();
builder.Services.AddModulusMessaging(options =>
{
    options.Transport = Transport.RabbitMq;
    options.ConnectionString = builder.Configuration.GetConnectionString("RabbitMq");
    options.Assemblies.Add(typeof(Program).Assembly);
});

MessagingOptions Reference

PropertyTypeDefaultDescription
TransportTransportInMemoryThe message broker transport to use. One of InMemory, RabbitMq, or AzureServiceBus.
ConnectionStringstring--Connection string for the selected transport. Not required for InMemory.
FullyQualifiedNamespacestring--Azure Service Bus namespace (e.g. myns.servicebus.windows.net); required when Credential is set instead of a connection string.
CredentialTokenCredential--Azure credential for managed-identity authentication. Set in the callback (cannot be bound from configuration).
AssembliesList<Assembly>EmptyAssemblies to scan for IIntegrationEventHandler<T> implementations.
EndpointNamestringSanitized entry assembly nameQueue (RabbitMQ) / subscription (Azure Service Bus) identity of this host. Replicas sharing it compete for messages.
PrefetchCountint10Messages the broker delivers ahead of acknowledgement. Valid range: 1–1000.
AutoProvisionbooltrueWhether the transport declares exchanges/queues/topics/subscriptions itself. Set false with pre-created entities for least privilege.
OutboxPollIntervalTimeSpan5 secondsFallback sweep interval for the OutboxProcessor; new rows saved through wired-up contexts dispatch immediately via a wake signal (see Outbox Pattern). Minimum: 1 second.
OutboxBatchSizeint100Maximum number of outbox messages to process per polling cycle. Valid range: 1–1000.
RetryPolicyRetryPolicyOptions5 attemptsOutbox dispatch retry: how many times the OutboxProcessor re-publishes a message before dead-lettering it.
ConsumerRetryRetryPolicyOptions5 attemptsIn-process consumer retry: how many times a handler is re-executed on failure before the message is dead-lettered on the transport. Independent of RetryPolicy.

Both retry options are RetryPolicyOptions with MaxAttempts (≥ 1), InitialInterval, MaxInterval, and IntervalIncrement (MaxIntervalInitialInterval). Bind them under Messaging:RetryPolicy:* and Messaging:ConsumerRetry:*.

Handler auto-discovery

AddModulusMessaging scans the provided assemblies for all IIntegrationEventHandler<TEvent> implementations, registers them as scoped services, and subscribes the transport to each handled event type. No manual consumer registration is needed. When a message arrives, all registered handlers for that event type are invoked.

How It Works

At a high level, the messaging system works as follows:

  1. Module A publishes an integration event via IMessageBus.
  2. The transport layer serializes the event (System.Text.Json) and publishes it to the configured broker.
  3. The broker (RabbitMQ, Azure Service Bus, or in-memory) routes the message to subscribed endpoints.
  4. The ConsumerDispatcher deserializes the message, applies inbox idempotency (when an inbox store is registered) and in-process retry, then invokes every registered handler.
  5. Your IIntegrationEventHandler receives and processes the event.

What's Next

Dive into the specific concepts:

Released under the MIT License.