Skip to content

Azure Storage Queues

Cheap, durable and simple. Commands only — no pub/sub — but it is the transport with the best support for long-running work, and the package supplies the attachment provider, saga store and singleton lock manager that the rest of KnightBus builds on. Those three are host-wide features and work with any transport; they need UseBlobStorage(...) for the storage account, not UseTransport<StorageTransport>().

dotnet add package KnightBus.Azure.Storage
dotnet add package KnightBus.Azure.Storage.Messages

Registration

services
    .UseBlobStorage(connectionString)
    .RegisterProcessors()
    .UseTransport<StorageTransport>();
services
    .UseBlobStorage("mystorageaccount", new ManagedIdentityCredential())
    .RegisterProcessors()
    .UseTransport<StorageTransport>();

The package name says "blob storage" because the same configuration covers queues, the attachment blobs and the saga blobs.

Messages

Only one marker interface — this transport has no events:

public class ResizeImage : IStorageQueueCommand
{
    public string ImageId { get; set; }
}

public class ResizeImageMapping : IMessageMapping<ResizeImage>
{
    public string QueueName => "resize-image";
}

Client

var bus = scope.ServiceProvider.GetRequiredService<IStorageBus>();

await bus.SendAsync(new ResizeImage { ImageId = "1" });
await bus.ScheduleAsync(new ResizeImage { ImageId = "2" }, TimeSpan.FromMinutes(30));

ScheduleAsync sets the message's visibility delay. There is no batch overload and no way to cancel a deferred message — for that, use Service Bus.

What gets created

One logical KnightBus queue is three storage objects:

Object Purpose
Queue {name} The queue itself, holding message properties.
Queue {name}-dl Dead letters.
Blob container {name} Message payloads and attachments.

The payload lives in a blob and the queue message only references it, which is how this transport sidesteps the 64 KB queue message limit entirely. It also means a queue message whose payload blob is missing is deleted on read as unrecoverable.

Dead letter queues are hidden from the management API listing, so they do not appear as queues in their own right.

Long-running work

This is the only transport that can extend a message lock while your handler runs, which makes it the right choice for work measured in minutes or hours. Implement IExtendMessageLockTimeout on your settings and register the middleware:

public class LongRunningSettings : IProcessingSettings, IExtendMessageLockTimeout
{
    public int MaxConcurrentCalls => 1;
    public int PrefetchCount => 0;
    public TimeSpan MessageLockTimeout => TimeSpan.FromHours(4);   // total budget
    public int DeadLetterDeliveryLimit => 2;

    public TimeSpan ExtensionDuration => TimeSpan.FromMinutes(5);  // lock actually held
    public TimeSpan ExtensionInterval => TimeSpan.FromMinutes(1);  // renewal cadence
}
services.AddMiddleware<ExtendMessageLockDurationMiddleware>();

The benefit over one enormous MessageLockTimeout is recovery time: if the host crashes, the message becomes visible again after ExtensionDuration rather than after the full four hours. See extending the lock.

The middleware itself is a KnightBus.Core type rather than a Storage Queues one: it is registered per host and runs on every listener. What it cannot do elsewhere is renew — that needs the message state handler to implement IMessageLockHandler<T>, which today is this transport alone. On other transports it passes messages through untouched, with one exception: on PostgreSQL, implementing IExtendMessageLockTimeout shortens the fetch lock with nothing able to renew it, which causes duplicate processing.

Attachments

The Blob Storage attachment provider is the one most applications use, whatever transport carries the messages:

services
    .UseBlobStorage(connectionString)
    .UseBlobStorageAttachments();

Optional Brotli compression:

services.UseBlobStorageAttachments(options =>
{
    options.EnableCompression = true;
    options.CompressionLevel = CompressionLevel.Optimal;
});

Compression is off by default and safe to enable on an existing store — whether a blob is compressed is recorded in its name, so old attachments keep working. See attachments.

Saga store

services.UseBlobStorageSagas();

State is stored in the knightbus-sagas container with the blob ETag providing optimistic concurrency, so concurrent writes to the same saga are detected — of the shipped stores only this one and Redis do that, whatever transport carries the saga's messages. Expiry is evaluated on read, so expired blobs are reported as not found but are not deleted for you. See sagas.

Singleton lock manager

Blob leases are KnightBus' only shipped distributed lock implementation, so this package is what enables singleton processing and cron scheduling — even for applications whose messages travel over another transport entirely.

services
    .UseBlobStorage(connectionString)
    .UseBlobStorageLockManager();

Locks are blobs under knight-data/locks. To change that, implement IBlobLockScheme:

public class MyLockScheme : IBlobLockScheme
{
    public string ContainerName => "my-container";
    public string Directory => "my-locks";
}

services.UseBlobStorageLockManager(new MyLockScheme());

Management

services.UseBlobStorageManagement(connectionString);

PeekScheduled is not supported. Note that MoveDeadLetters returns the number of messages you asked it to move rather than the number it actually moved, and that it removes messages from the dead letter queue even when a requeue predicate rejects them.

Serialization

Defaults to NewtonsoftSerializer. Queue messages are Base64-encoded by default for compatibility with older storage clients; changing MessageEncoding requires passing a constructed StorageBusConfiguration rather than using the configuration callback, since the property is read-only on the interface.

Example

KnightBus.Samples.Azure.Storage shows attachments, the blob lock manager, the blob saga store and distributed tracing together. Run it against Azurite with UseDevelopmentStorage=true.