Skip to the content.

SQL transport provider for SlimMessageBus

Please read the Introduction before reading this provider documentation.

About

The SQL transport provider allows to leverage a single shared SQL database instance as a messaging broker for all the collaborating producers and consumers.

This transport might be optimal for simpler applications that do not have a dedicated messaging infrastructure available, do not have high throughput needs, or want to target a simplistic deployment model.

When the application grows over time, and given that SMB is an abstraction, the migration from SQL towards a dedicated messaging system should be super easy.

SQL Compatibility

This transport targets SQL Server / Azure SQL (T-SQL).

Configuration

Install the transport package:

dotnet add package SlimMessageBus.Host.Sql

The configuration is arranged via the .WithProviderSql(cfg => {}) method on the message bus builder.

using SlimMessageBus.Host.Sql;

services.AddSlimMessageBus(mbb =>
{
    mbb.WithProviderSql(cfg =>
    {
       cfg.ConnectionString = "...";
       cfg.DatabaseSchemaName = "smb";
       cfg.DatabaseTableName = "Messages";
       cfg.PollDelay = TimeSpan.FromMilliseconds(250);
       cfg.PollBatchSize = 10;
       cfg.LockDuration = TimeSpan.FromSeconds(30);
       cfg.MaxDeliveryAttempts = 10;
    });

    mbb.Produce<PingMessage>(x => x.DefaultQueue("ping-queue"));
    mbb.Consume<PingMessage>(x => x.Queue("ping-queue"));

    mbb.Produce<OrderSubmitted>(x => x.DefaultTopic("orders").ToTopic());
    mbb.Consume<OrderSubmitted>(x => x.Topic("orders", "billing"));
    mbb.Consume<OrderSubmitted>(x => x.Topic("orders", "shipping"));

    mbb.AddServicesFromAssemblyContaining<PingConsumer>();
    mbb.AddJsonSerializer();
});

Provider settings

The most commonly configured settings are:

Queues, topics, and request/response

Use DefaultQueue() and Queue() for competing-consumer queues:

mbb.Produce<PingMessage>(x => x.DefaultQueue("ping-queue"));
mbb.Consume<PingMessage>(x => x.Queue("ping-queue"));

Use DefaultTopic().ToTopic() and Topic(topic, subscriptionName) for durable pub/sub:

mbb.Produce<OrderSubmitted>(x => x.DefaultTopic("orders").ToTopic());
mbb.Consume<OrderSubmitted>(x => x.Topic("orders", "billing"));
mbb.Consume<OrderSubmitted>(x => x.Topic("orders", "shipping"));

Request/response endpoints can also use SQL queues or topics:

mbb.Handle<PingRequest, PingResponse>(x => x.Queue("ping-handler"));
mbb.ExpectRequestResponses(x => x.ReplyToQueue("replies"));

Message id generation

The transport stores messages with two identifiers:

By default, SQL uses SqlMessageIdGenerationMode.ClientGuidGenerator with SqlSequentialGuidGenerator, which creates sequential-ish GUIDs client-side for better index locality than random GUIDs.

You can change the id strategy:

mbb.WithProviderSql(cfg =>
{
    cfg.ConnectionString = "...";
    cfg.IdGeneration.Mode = SqlMessageIdGenerationMode.DatabaseGeneratedSequentialGuid;
});

Available modes:

How it works

The same SQL database instance is required for all the producers and consumers to collaborate. Therefore ensure all of the service instances point to the same database cluster.

Polling and locking

Consumers poll the shared message table in batches. SQL Server locking hints (ROWLOCK, UPDLOCK, READPAST) are used so competing consumers can skip rows already locked by another instance.

When a consumer locks a row, the transport stores the consumer instance id and lock expiration. If the process stops before completing the message, the row becomes visible again after LockDuration.

Retries and failed messages

Successful processing marks the row as complete. Failed processing increments DeliveryAttempt, clears the lock, and makes the row available for another attempt. Once MaxDeliveryAttempts is reached, the row is marked aborted and will no longer be delivered.

The transport retries transient SQL errors around schema provisioning and operations according to SchemaCreationRetry and OperationRetry.

Schema provisioning

The provider provisions the required message, subscription, and migration tables during bus startup. All cooperating services should use the same database, schema, and table names.

Testing locally

The integration tests use Testcontainers and require Docker to be running:

dotnet test src/Tests/SlimMessageBus.Host.Sql.Test/SlimMessageBus.Host.Sql.Test.csproj --filter "Category=Integration"

The repository also contains infrastructure.ps1 for standing up shared development infrastructure used by broader integration test runs.