Skip to main content

IBM MQ Transport

Excalibur.Dispatch.Transport.IbmMq registers the transport primitives for IBM MQ: a keyed ITransportSender and ITransportReceiver that send, receive, and acknowledge messages against an IBM MQ queue manager over the IBM MQ managed .NET client.

Scope of this package

This package provides the low-level transport primitives only — the keyed sender and receiver you resolve by transport name to move bytes directly. High-level integration into the dispatch pipeline (a transport adapter that publishes and consumes typed dispatch messages end-to-end, participating in middleware, serialization, and routing) is not part of this package and is provided separately.

If you need full pipeline-integrated messaging today, use one of the pipeline-integrated transports (Kafka, RabbitMQ, Azure Service Bus, AWS SQS, Google Pub/Sub).

Before You Start

  • .NET 10.0
  • A reachable IBM MQ queue manager (local or hosted) with a server-connection channel and queue
  • Familiarity with transports

Installation

dotnet add package Excalibur.Dispatch.Transport.IbmMq

Dependencies: Excalibur.Dispatch.Abstractions, Excalibur.Dispatch.Transport.Abstractions, IBMMQDotnetClient

Registration

Register a named transport with AddIbmMqTransport. The name is used for multi-transport routing and to resolve the keyed sender/receiver.

using Microsoft.Extensions.DependencyInjection;

services.AddIbmMqTransport("orders", ibmmq =>
{
ibmmq.QueueManager = "QM1";
ibmmq.Host = "localhost";
ibmmq.Port = 1414;
ibmmq.Channel = "DEV.APP.SVRCONN";
ibmmq.QueueName = "DEV.QUEUE.1";

// Optional
ibmmq.ReplyToQueue = "DEV.REPLY.1";
ibmmq.UserId = "app"; // source credentials from configuration or a secret manager
ibmmq.Password = "<from-secret-store>";

// Receive tuning
ibmmq.Receive.MaxBatchSize = 10;
ibmmq.Receive.WaitIntervalMilliseconds = 1000;
ibmmq.Receive.MaxPayloadBytes = 1_048_576;
});

Options

PropertyTypeDefaultPurpose
QueueManagerstring"" (required)The name of the target queue manager.
Hoststring"" (required)The host name of the queue manager listener.
Portint1414The TCP port of the queue manager listener; must be in 1..65535.
Channelstring"" (required)The server-connection channel name (commonly DEV.APP.SVRCONN on developer queue managers).
QueueNamestring"" (required)The queue that messages are sent to and received from.
ReplyToQueuestring?nullThe reply-to queue for the native request/reply pattern, or null when unused.
UserIdstring?nullThe user id for client authentication, or null for none.
Passwordstring?nullThe password for client authentication, or null for none.
ReceiveIbmMqReceiveTuningOptions(see below)Receive-side tuning options.

Receive tuning (Receive)

PropertyTypeDefaultPurpose
MaxBatchSizeint10The maximum number of messages to drain per receive call; must be in 1..256. Because the receiver opens one queue-manager connection per in-flight message, this bounds the concurrent connections a single receive can open (MaxBatchSizeCeiling = 256).
WaitIntervalMillisecondsint1000The get-wait interval before a receive call returns empty; must be non-negative. Only the first get in a batch waits — the rest return immediately.
MaxPayloadBytesint?nullThe maximum accepted inbound payload size in bytes, or null to opt out of the limit.

Options are validated at startup (ValidateOnStart), so a missing queue manager, host, channel, or queue name — or a port or batch size out of range — fails fast with OptionsValidationException.

Using the sender and receiver

The sender and receiver are registered as keyed singletons under the transport name. Resolve them with GetRequiredKeyedService:

using Excalibur.Dispatch.Transport;

var sender = provider.GetRequiredKeyedService<ITransportSender>("orders");
var receiver = provider.GetRequiredKeyedService<ITransportReceiver>("orders");

// Send
await sender.SendAsync(
new TransportMessage
{
Body = JsonSerializer.SerializeToUtf8Bytes(order),
ContentType = "application/json",
MessageType = "OrderPlaced",
},
cancellationToken);

// Receive and acknowledge
var messages = await receiver.ReceiveAsync(maxMessages: 10, cancellationToken);
foreach (var message in messages)
{
Process(message);
await receiver.AcknowledgeAsync(message, cancellationToken);
}

Unit of work per message

Each received message holds its own queue-manager connection and syncpoint. AcknowledgeAsync commits (removes) exactly that message, and RejectAsync backs it out so the queue manager redelivers it — true per-message acknowledge and reject in any order. Outstanding units of work are bounded by the caller's maxMessages and are always committed or backed out (never leaked), including on disposal and cancellation. On the send side, each send opens a connection, puts the message under a unit of work, commits, and disconnects, so a failed put never leaves an uncommitted message.

Payload-size guard

The receiver enforces an inbound payload-size limit measured before the message is delivered, tuned via IbmMqOptions.Receive.MaxPayloadBytes — set it to null to opt out. An oversized message is discarded (committed under syncpoint) rather than backed out, so the queue manager does not loop redelivering an unprocessable payload.

What's Next