From d147b6da0aaf8c8761797b4f82fdb5e74044d6bc Mon Sep 17 00:00:00 2001 From: Ramon Smits Date: Thu, 19 Feb 2026 09:43:25 +0000 Subject: [PATCH 01/19] =?UTF-8?q?=E2=9C=A8=20Add=20IBM=20MQ=20transport=20?= =?UTF-8?q?documentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds comprehensive transport guidance for the IBM MQ transport, following existing patterns from RabbitMQ, PostgreSQL, and other transport documentation. New pages: - Transport overview with features, advantages, and configuration - Connection settings (basic, HA, SSL/TLS, message processing) - Publish/subscribe topology (topic-per-event, polymorphism, naming) - Transaction support (SendsAtomicWithReceive, ReceiveOnly, None) - Native integration (MQMD/MQRFH2 message structure) - Operations scripting (CLI tool and runmqsc commands) Infrastructure: - Registered IbmMq component in components.yaml - Added NuGet alias mapping in nugetAlias.txt - Added core-dependencies version mapping - Added IBM MQ section to navigation menu - Added IBM MQ to supported transports index - Added IBM MQ to transport selection guide --- components/components.yaml | 9 + .../NServiceBus.Transport.IbmMq.txt | 1 + components/nugetAlias.txt | 1 + menu/menu.yaml | 14 ++ transports/ibmmq/connection-settings.md | 188 ++++++++++++++++++ transports/ibmmq/index.md | 111 +++++++++++ transports/ibmmq/native-integration.md | 78 ++++++++ transports/ibmmq/operations-scripting.md | 138 +++++++++++++ transports/ibmmq/topology.md | 140 +++++++++++++ transports/ibmmq/transactions.md | 85 ++++++++ transports/index.md | 1 + transports/selecting.md | 28 +++ 12 files changed, 794 insertions(+) create mode 100644 components/core-dependencies/NServiceBus.Transport.IbmMq.txt create mode 100644 transports/ibmmq/connection-settings.md create mode 100644 transports/ibmmq/index.md create mode 100644 transports/ibmmq/native-integration.md create mode 100644 transports/ibmmq/operations-scripting.md create mode 100644 transports/ibmmq/topology.md create mode 100644 transports/ibmmq/transactions.md diff --git a/components/components.yaml b/components/components.yaml index 6f7368ea2fe..fe7189da6a4 100644 --- a/components/components.yaml +++ b/components/components.yaml @@ -53,6 +53,15 @@ NugetOrder: - NServiceBus.AmazonSQS +- Key: IbmMq + Name: IBM MQ Transport + DocsUrl: /transports/ibmmq + GitHubOwner: ParticularLabs + Category: Transport + ProjectUrl: https://github.com/ParticularLabs/NServiceBus.IBMMQ + NugetOrder: + - NServiceBus.Transport.IbmMq + - Key: ASB Name: Azure Service Bus Transport (Legacy) DocsUrl: /transports/azure-service-bus diff --git a/components/core-dependencies/NServiceBus.Transport.IbmMq.txt b/components/core-dependencies/NServiceBus.Transport.IbmMq.txt new file mode 100644 index 00000000000..cbc691394c1 --- /dev/null +++ b/components/core-dependencies/NServiceBus.Transport.IbmMq.txt @@ -0,0 +1 @@ +1.0 : 10 diff --git a/components/nugetAlias.txt b/components/nugetAlias.txt index 7a84c292981..c9478c139c2 100644 --- a/components/nugetAlias.txt +++ b/components/nugetAlias.txt @@ -30,6 +30,7 @@ Heartbeats4: ServiceControl.Plugin.Nsb4.Heartbeat Heartbeats5: ServiceControl.Plugin.Nsb5.Heartbeat Heartbeats6: ServiceControl.Plugin.Nsb6.Heartbeat Heartbeats: NServiceBus.Heartbeat +IbmMq: NServiceBus.Transport.IbmMq Host: NServiceBus.Host Log4Net: NServiceBus.Log4Net MessageInterfaces: NServiceBus.MessageInterfaces diff --git a/menu/menu.yaml b/menu/menu.yaml index af0580c5762..e16277a446e 100644 --- a/menu/menu.yaml +++ b/menu/menu.yaml @@ -1078,6 +1078,20 @@ Title: Scaling out - Url: transports/msmq/sender-side-distribution Title: Scaling out with Sender-Side Distribution + - Title: IBM MQ + Articles: + - Url: transports/ibmmq + Title: IBM MQ transport + - Url: transports/ibmmq/connection-settings + Title: Connection settings + - Url: transports/ibmmq/topology + Title: Publish/subscribe topology + - Url: transports/ibmmq/transactions + Title: Transaction support + - Url: transports/ibmmq/native-integration + Title: Native integration + - Url: transports/ibmmq/operations-scripting + Title: Scripting - Title: RabbitMQ Articles: - Url: transports/rabbitmq diff --git a/transports/ibmmq/connection-settings.md b/transports/ibmmq/connection-settings.md new file mode 100644 index 00000000000..05fe1de78fd --- /dev/null +++ b/transports/ibmmq/connection-settings.md @@ -0,0 +1,188 @@ +--- +title: Connection Settings +summary: Information about connection settings for the IBM MQ transport, including SSL/TLS and high availability +reviewed: 2026-02-19 +component: IbmMq +--- + +## Basic connection + +The transport connects to an IBM MQ queue manager using a host, port, and SVRCONN channel: + +```csharp +var transport = new IbmMqTransport(options => +{ + options.Host = "mq-server.example.com"; + options.Port = 1414; + options.Channel = "DEV.APP.SVRCONN"; + options.QueueManagerName = "QM1"; +}); +``` + +### Defaults + +|Setting|Default| +|:---|---| +|Host|`localhost`| +|Port|`1414`| +|Channel|`DEV.ADMIN.SVRCONN`| +|QueueManagerName|Empty (local default queue manager)| + +## Authentication + +User credentials can be provided to authenticate with the queue manager: + +```csharp +var transport = new IbmMqTransport(options => +{ + options.Host = "mq-server.example.com"; + options.QueueManagerName = "QM1"; + options.User = "app"; + options.Password = "passw0rd"; +}); +``` + +> [!NOTE] +> When no credentials are provided, the connection uses the operating system identity. This may be appropriate for local development but typically requires explicit credentials in production. + +## Application name + +The application name appears in IBM MQ monitoring tools (e.g., `DISPLAY CONN(*)` in runmqsc) and is useful for identifying connections: + +```csharp +var transport = new IbmMqTransport(options => +{ + // ... connection settings ... + options.ApplicationName = "OrderService"; +}); +``` + +If not specified, the application name defaults to the entry assembly name. + +## High availability + +For high availability scenarios with multi-instance queue managers or a set of candidate queue managers, provide a connection name list instead of a single host and port: + +```csharp +var transport = new IbmMqTransport(options => +{ + options.QueueManagerName = "QM1"; + options.Channel = "APP.SVRCONN"; + options.Connections.Add("mqhost1(1414)"); + options.Connections.Add("mqhost2(1414)"); +}); +``` + +When `Connections` is specified, the `Host` and `Port` properties are ignored. The IBM MQ client will attempt each connection in order, connecting to the first available queue manager. + +> [!NOTE] +> All entries in the connection name list must point to instances of the same queue manager (by name). This is not a load balancing mechanism; it provides failover to a standby instance. + +## SSL/TLS + +To enable encrypted communication with the queue manager, configure the SSL key repository and cipher specification. The cipher must match the `SSLCIPH` attribute on the SVRCONN channel. + +```csharp +var transport = new IbmMqTransport(options => +{ + options.Host = "mq-server.example.com"; + options.QueueManagerName = "QM1"; + options.SslKeyRepository = "*SYSTEM"; + options.CipherSpec = "TLS_RSA_WITH_AES_256_CBC_SHA256"; +}); +``` + +### Key repository options + +|Value|Description| +|:---|---| +|`*SYSTEM`|Use the Windows system certificate store| +|`*USER`|Use the Windows user certificate store| +|File path|Path to a key database file (without extension), e.g., `/var/mqm/ssl/key`| + +### Peer name verification + +For additional security, verify the queue manager's certificate distinguished name: + +```csharp +var transport = new IbmMqTransport(options => +{ + // ... connection settings ... + options.SslKeyRepository = "*SYSTEM"; + options.CipherSpec = "TLS_RSA_WITH_AES_256_CBC_SHA256"; + options.SslPeerName = "CN=MQSERVER01,O=MyCompany,C=US"; +}); +``` + +### Key reset count + +SSL key renegotiation can be configured to periodically reset the secret key after a specified number of bytes: + +```csharp +var transport = new IbmMqTransport(options => +{ + // ... connection and SSL settings ... + options.KeyResetCount = 40000; // Renegotiate every 40 KB +}); +``` + +The default value of `0` disables client-side key reset, deferring to the channel or queue manager setting. + +> [!NOTE] +> Both `SslKeyRepository` and `CipherSpec` must be specified together. Setting one without the other will cause a configuration validation error. + +## Message processing settings + +### Polling interval + +The transport polls queues for messages using the IBM MQ `MQGET` with wait. The wait interval controls how long each poll waits for a message before returning: + +```csharp +var transport = new IbmMqTransport(options => +{ + // ... connection settings ... + options.MessageWaitInterval = TimeSpan.FromMilliseconds(2000); +}); +``` + +|Setting|Default|Range| +|:---|---|---| +|MessageWaitInterval|5000 ms|100–30,000 ms| + +Shorter intervals improve responsiveness but increase CPU usage. Longer intervals reduce CPU usage but increase message processing latency. + +### Maximum message size + +The maximum message size the transport will accept. This should match or be less than the queue manager's `MAXMSGL` setting: + +```csharp +var transport = new IbmMqTransport(options => +{ + // ... connection settings ... + options.MaxMessageLength = 10 * 1024 * 1024; // 10 MB +}); +``` + +|Setting|Default|Range| +|:---|---|---| +|MaxMessageLength|4 MB (4,194,304 bytes)|1 KB – 100 MB| + +### Character set + +The Coded Character Set Identifier (CCSID) used for message text encoding: + +```csharp +var transport = new IbmMqTransport(options => +{ + // ... connection settings ... + options.CharacterSet = 1208; // UTF-8 (default) +}); +``` + +Common values: + +|CCSID|Encoding| +|:---|---| +|1208|UTF-8 (recommended)| +|819|ISO 8859-1 (Latin-1)| +|1252|Windows Latin-1| diff --git a/transports/ibmmq/index.md b/transports/ibmmq/index.md new file mode 100644 index 00000000000..a9e6223a705 --- /dev/null +++ b/transports/ibmmq/index.md @@ -0,0 +1,111 @@ +--- +title: IBM MQ Transport +summary: Integrate NServiceBus with IBM MQ for enterprise messaging on mainframe and distributed platforms +reviewed: 2026-02-19 +component: IbmMq +related: +redirects: +--- + +Provides support for sending messages over [IBM MQ](https://www.ibm.com/products/mq) using the [IBM MQ .NET client](https://www.nuget.org/packages/IBMMQDotnetClient/). + +## Broker compatibility + +The transport requires IBM MQ 9.0 or later. It uses the managed .NET client library (`IBMMQDotnetClient`) which communicates with queue managers via the client connection (SVRCONN) channel. + +The transport has been tested with: + +- IBM MQ on Linux and Windows +- IBM MQ on z/OS +- IBM MQ in containers (using the `icr.io/ibm-messaging/mq` image) +- IBM MQ as a Service on IBM Cloud + +## Transport at a glance + +|Feature | | +|:--- |--- +|Transactions |None, ReceiveOnly, SendsAtomicWithReceive +|Pub/Sub |Native (via IBM MQ topics and durable subscriptions) +|Timeouts |Not natively supported +|Large message bodies |Up to 100 MB (configurable, must match queue manager MAXMSGL) +|Scale-out |Competing consumer +|Scripted Deployment |Supported via [CLI tool](operations-scripting.md) and IBM MQ admin commands +|Installers |Optional +|Native integration |[Supported](native-integration.md) +|Case Sensitive |Yes + +## Configuring the endpoint + +To use IBM MQ as the underlying transport: + +```csharp +var endpointConfiguration = new EndpointConfiguration("MyEndpoint"); + +var transport = new IbmMqTransport(options => +{ + options.Host = "mq-server.example.com"; + options.Port = 1414; + options.Channel = "DEV.APP.SVRCONN"; + options.QueueManagerName = "QM1"; + options.User = "app"; + options.Password = "passw0rd"; +}); + +endpointConfiguration.UseTransport(transport); +``` + +See [connection settings](connection-settings.md) for all available connection and configuration options. + +## Advantages and disadvantages + +### Advantages + +- Enterprise-grade messaging platform with decades of proven reliability in mission-critical systems. +- Native publish-subscribe mechanism via IBM MQ topics and durable subscriptions; does not require NServiceBus persistence for storing event subscriptions. +- Supports atomic sends with receive using IBM MQ syncpoint, ensuring send and receive operations commit or roll back together. +- Integrates with mainframe and legacy systems that already use IBM MQ, bridging distributed .NET applications with z/OS, IBM i, and other platforms. +- Built-in high availability via multi-instance queue managers and connection name lists. +- Supports SSL/TLS encryption and certificate-based authentication for secure communication. +- Supports the [competing consumer](https://www.enterpriseintegrationpatterns.com/patterns/messaging/CompetingConsumers.html) pattern out of the box for horizontal scaling. + +### Disadvantages + +- Requires an IBM MQ license; IBM MQ is a commercial product with per-VPC or per-core licensing. +- IBM MQ queue and topic names are limited to 48 characters, which can require custom name sanitization for longer endpoint or event type names. +- Does not support native delayed delivery; delayed delivery requires an external timeout storage mechanism. +- Does not support `TransactionScope` mode (distributed transactions). +- Fewer .NET-focused community resources compared to RabbitMQ or cloud-native alternatives. +- Queue manager administration requires specialized IBM MQ knowledge (runmqsc, MQ Explorer, or equivalent tooling). + +## Queue naming constraints + +IBM MQ imposes strict naming rules on queues and topics: + +- Maximum 48 characters +- Allowed characters: `A-Z`, `a-z`, `0-9`, `.`, `_` +- No hyphens, spaces, or other special characters + +If endpoint or event type names exceed these constraints, configure a `ResourceNameSanitizer` to transform names: + +```csharp +var transport = new IbmMqTransport(options => +{ + // ... connection settings ... + options.ResourceNameSanitizer = name => + { + // Replace invalid characters and truncate + var sanitized = name.Replace("-", ".").Replace("/", "."); + return sanitized.Length > 48 ? sanitized[..48] : sanitized; + }; +}); +``` + +> [!WARNING] +> Ensure the sanitizer produces deterministic and unique names. Two different input names mapping to the same sanitized name will cause messages to be delivered to the wrong endpoint. + +## Message persistence + +By default, all messages are sent as persistent (`MQPER_PERSISTENT`), meaning they survive queue manager restarts. To send non-persistent messages for higher throughput at the expense of durability, mark messages with the `NonDurableMessage` header. + +> [!CAUTION] +> Non-persistent messages are lost if the queue manager restarts before they are consumed. diff --git a/transports/ibmmq/native-integration.md b/transports/ibmmq/native-integration.md new file mode 100644 index 00000000000..f0e9daebb16 --- /dev/null +++ b/transports/ibmmq/native-integration.md @@ -0,0 +1,78 @@ +--- +title: Native integration +summary: How to integrate NServiceBus endpoints with native IBM MQ applications +reviewed: 2026-02-19 +component: IbmMq +--- + +The IBM MQ transport stores NServiceBus messages using standard IBM MQ message structures, making it possible to send and receive messages between NServiceBus endpoints and native IBM MQ applications. + +## Message structure + +NServiceBus messages on IBM MQ consist of two parts: + +### MQMD (Message Descriptor) + +The MQMD is the standard IBM MQ message header. The transport maps the following fields: + +|MQMD field|NServiceBus usage| +|:---|---| +|MessageId|24-byte binary; set from the `NServiceBus.MessageId` header as a GUID padded to 24 bytes| +|CorrelationId|24-byte binary; set from the `NServiceBus.CorrelationId` header| +|MessageType|Always `MQMT_DATAGRAM` (8)| +|Persistence|`MQPER_PERSISTENT` by default; `MQPER_NOT_PERSISTENT` if `NonDurableMessage` header is set| +|Expiry|Set from `DiscardIfNotReceivedBefore` in tenths of seconds; `MQEI_UNLIMITED` if not specified| +|CharacterSet|UTF-8 (CCSID 1208) by default| +|ReplyToQueueName|Set from the `NServiceBus.ReplyToAddress` header| + +### MQRFH2 properties + +All NServiceBus headers are stored as MQRFH2 custom properties on the message. Property names are escaped to comply with IBM MQ naming rules: + +- Underscores are doubled: `_` becomes `__` +- Non-alphanumeric characters are encoded as `_xHHHH` where `HHHH` is the Unicode code point + +For example, the header `NServiceBus.MessageId` is stored as the property `NServiceBus_x002EMessageId`. + +Two additional manifest properties are included: + +|Property|Purpose| +|:---|---| +|`nsbhdrs`|Comma-separated list of all escaped header names| +|`nsbempty`|Comma-separated list of escaped header names with empty values| + +> [!NOTE] +> IBM MQ silently discards string properties with empty values. The manifest properties work around this limitation by tracking which headers exist and which have empty values. + +## Sending messages from native applications + +To send a message to an NServiceBus endpoint from a native IBM MQ application: + +1. Create an `MQMessage` targeting the endpoint's input queue. +2. Set the message body to a serialized representation of the message (e.g., JSON or XML). +3. Set MQRFH2 properties for the required NServiceBus headers. + +The minimum required headers are: + +|Header (escaped property name)|Description| +|:---|---| +|`NServiceBus_x002EEnclosedMessageTypes`|The fully qualified .NET type name of the message| +|`NServiceBus_x002EContentType`|The MIME type of the body serialization (e.g., `application/json`)| +|`NServiceBus_x002EMessageId`|A unique identifier for the message (typically a GUID)| + +> [!WARNING] +> If the `NServiceBus.EnclosedMessageTypes` header is missing, the endpoint will not be able to deserialize and route the message. + +## Receiving messages in native applications + +Native applications can consume messages from queues that NServiceBus publishes or sends to. The application should: + +1. Read the message body bytes from the `MQMessage`. +2. Read MQRFH2 properties to access NServiceBus headers. Use the `nsbhdrs` manifest to enumerate all headers, and `nsbempty` to identify headers with empty values. +3. Unescape property names by replacing `__` with `_` and `_xHHHH` with the corresponding character. + +## Interoperability considerations + +- NServiceBus always sets `MessageType` to `MQMT_DATAGRAM`. Native applications that rely on `MQMT_REQUEST`/`MQMT_REPLY` patterns should use a separate set of queues or translate at an integration boundary. +- The body encoding is determined by the NServiceBus serializer configuration, typically JSON. Native applications must use the same serialization format. +- Message expiry (time-to-be-received) is mapped to the MQMD `Expiry` field in tenths of seconds. diff --git a/transports/ibmmq/operations-scripting.md b/transports/ibmmq/operations-scripting.md new file mode 100644 index 00000000000..ecf7d3ae43d --- /dev/null +++ b/transports/ibmmq/operations-scripting.md @@ -0,0 +1,138 @@ +--- +title: IBM MQ Transport Scripting +summary: Command-line tool and scripts for managing IBM MQ transport infrastructure +reviewed: 2026-02-19 +component: IbmMq +related: + - nservicebus/operations +--- + +The IBM MQ transport includes a command-line tool for creating and managing transport infrastructure (queues, topics, and subscriptions) without writing code. + +## Command-line tool + +The `NServiceBus.Transport.IbmMq.CommandLine` package provides the `ibmmq-transport` CLI tool for managing IBM MQ resources. + +### Installation + +Install the tool globally: + +```bash +dotnet tool install -g NServiceBus.Transport.IbmMq.CommandLine +``` + +### Connection options + +All commands accept the following connection options. These can also be provided via environment variables: + +|Option|Environment variable|Default| +|:---|---|---| +|`--host`|`IBMMQ_HOST`|`localhost`| +|`--port`|`IBMMQ_PORT`|`1414`| +|`--channel`|`IBMMQ_CHANNEL`|`DEV.ADMIN.SVRCONN`| +|`--queue-manager`|`IBMMQ_QUEUE_MANAGER`|(empty)| +|`--user`|`IBMMQ_USER`|(none)| +|`--password`|`IBMMQ_PASSWORD`|(none)| + +### Create endpoint infrastructure + +Creates the input queue for an endpoint: + +```bash +ibmmq-transport endpoint create \ + --host mq-server.example.com \ + --queue-manager QM1 +``` + +### Create a queue + +Creates a local queue with a configurable maximum depth: + +```bash +ibmmq-transport queue create --max-depth 5000 +``` + +### Delete a queue + +Deletes a local queue: + +```bash +ibmmq-transport queue delete +``` + +> [!WARNING] +> Deleting a queue permanently removes it and any messages it contains. + +### Subscribe an endpoint to an event + +Creates the topic and durable subscription needed for an endpoint to receive a published event type: + +```bash +ibmmq-transport endpoint subscribe \ + --topic-prefix DEV +``` + +For example: + +```bash +ibmmq-transport endpoint subscribe OrderService \ + "MyCompany.Events.OrderPlaced" \ + --topic-prefix PROD +``` + +This command: + +1. Creates a topic object named `PROD.MYCOMPANY.EVENTS.ORDERPLACED` (if it does not exist). +2. Creates a durable subscription linking the topic to the `OrderService` input queue. + +### Unsubscribe an endpoint from an event + +Removes the durable subscription for an event type: + +```bash +ibmmq-transport endpoint unsubscribe \ + --topic-prefix DEV +``` + +### Using environment variables + +For repeated use, connection details can be provided via environment variables: + +```bash +export IBMMQ_HOST=mq-server.example.com +export IBMMQ_PORT=1414 +export IBMMQ_QUEUE_MANAGER=QM1 +export IBMMQ_USER=admin +export IBMMQ_PASSWORD=passw0rd + +ibmmq-transport endpoint create OrderService +ibmmq-transport endpoint subscribe OrderService "MyCompany.Events.OrderPlaced" +``` + +## IBM MQ native administration + +For operations not covered by the CLI tool, use native IBM MQ administration commands. + +### Using runmqsc + +``` +# Create a local queue +DEFINE QLOCAL(ORDERSERVICE) MAXDEPTH(5000) DEFPSIST(YES) + +# Display queue depth +DISPLAY QLOCAL(ORDERSERVICE) CURDEPTH + +# Display active connections +DISPLAY CONN(*) APPLTAG + +# Create a topic +DEFINE TOPIC(DEV.ORDERPLACED) TOPICSTR('dev/mycompany.events.orderplaced/') +``` + +### Using PCF commands + +The transport uses PCF (Programmable Command Format) commands internally for queue and topic creation. The same approach can be used in deployment scripts: + +- `MQCMD_CREATE_Q` — Create a local queue +- `MQCMD_CREATE_TOPIC` — Create a topic object +- `MQCMD_DELETE_SUBSCRIPTION` — Delete a durable subscription diff --git a/transports/ibmmq/topology.md b/transports/ibmmq/topology.md new file mode 100644 index 00000000000..79adfc5515c --- /dev/null +++ b/transports/ibmmq/topology.md @@ -0,0 +1,140 @@ +--- +title: Publish/subscribe topology +summary: How the IBM MQ transport implements publish/subscribe messaging using topics and durable subscriptions +reviewed: 2026-02-19 +component: IbmMq +--- + +The IBM MQ transport implements publish/subscribe messaging using IBM MQ's native topic and subscription infrastructure. This means event subscriptions do not require NServiceBus persistence. + +## Topic-per-event topology + +The default topology creates one IBM MQ topic object per concrete event type. When an event is published, the message is sent to the corresponding topic. Subscribers create durable subscriptions against the topic, with messages delivered to their input queue. + +```mermaid +graph LR + P[Publisher Endpoint] -->|publish| T[Topic: DEV.ORDERPLACED] + T -->|durable subscription| S1[Subscriber A Queue] + T -->|durable subscription| S2[Subscriber B Queue] +``` + +### Sending (unicast) + +Unicast messages (commands) are sent directly to the destination queue by name. No topics or exchanges are involved. + +```mermaid +graph LR + S[Sender Endpoint] -->|put message| Q[Destination Queue] + Q --> R[Receiver Endpoint] +``` + +### Publishing (multicast) + +When an endpoint publishes an event: + +1. The transport resolves the IBM MQ topic for the event type using the configured `TopicNaming` strategy. +2. If infrastructure setup is enabled, the topic object is created if it does not already exist. +3. The message is published to the topic. IBM MQ delivers a copy to every durable subscription on that topic. + +### Subscribing + +When an endpoint subscribes to an event type: + +1. The transport resolves all concrete types assignable to the subscribed type (supporting polymorphic subscriptions). +2. For each concrete type, a durable subscription is created linking the topic to the endpoint's input queue. +3. When the endpoint starts, existing subscriptions are resumed. + +### Unsubscribing + +When an endpoint unsubscribes from an event type, the corresponding durable subscriptions are deleted from the queue manager. + +## Polymorphism + +The transport supports polymorphic event subscriptions via subscriber-side fan-out. When subscribing to a base class or interface, the transport scans loaded assemblies for all concrete types that implement or extend the subscribed type and creates a separate durable subscription for each. + +For example, given: + +```csharp +public interface IOrderEvent { } +public class OrderPlaced : IOrderEvent { } +public class OrderCancelled : IOrderEvent { } +``` + +Subscribing to `IOrderEvent` creates durable subscriptions for both `OrderPlaced` and `OrderCancelled`. Each published event is delivered exactly once to the subscriber regardless of how many type hierarchies match. + +```mermaid +graph LR + subgraph "Subscribe to IOrderEvent" + T1[Topic: DEV.ORDERPLACED] -->|subscription| Q[OrderHandler Queue] + T2[Topic: DEV.ORDERCANCELLED] -->|subscription| Q + end +``` + +> [!NOTE] +> For polymorphic subscriptions to work correctly, all concrete event types must be loadable in the subscribing endpoint's AppDomain. If a concrete type is defined in an assembly that is not referenced, the subscription for that type will not be created. + +## Topic naming + +Topics are named using a configurable `TopicNaming` strategy. The default strategy uses a prefix (default: `DEV`) and the fully qualified type name: + +|Concept|Format|Example| +|:---|---|---| +|Topic name (admin object)|`PREFIX.FULL.TYPE.NAME` (uppercase)|`DEV.MYAPP.EVENTS.ORDERPLACED`| +|Topic string|`prefix/full.type.name/` (lowercase)|`dev/myapp.events.orderplaced/`| +|Subscription name|`endpointname:topicstring`|`OrderService:dev/myapp.events.orderplaced/`| + +> [!WARNING] +> IBM MQ topic admin names are limited to 48 characters. If the generated topic name exceeds this limit, the transport throws an `InvalidOperationException` at startup. Override `TopicNaming.GenerateTopicName` to implement a shortening strategy. + +### Custom topic naming + +To customize how event types map to topic names, subclass `TopicNaming`: + +```csharp +public class ShortTopicNaming() : TopicNaming("APP") +{ + public override string GenerateTopicName(Type eventType) + { + // Use only the type name, not the full namespace + return $"APP.{eventType.Name}".ToUpperInvariant(); + } +} +``` + +Then configure the transport: + +```csharp +var transport = new IbmMqTransport(options => +{ + // ... connection settings ... + options.TopicNaming = new ShortTopicNaming(); +}); +``` + +### Custom topic prefix + +To change just the prefix without subclassing: + +```csharp +var transport = new IbmMqTransport(options => +{ + // ... connection settings ... + options.TopicNaming = new TopicNaming("PROD"); +}); +``` + +This produces topic names like `PROD.MYAPP.EVENTS.ORDERPLACED` instead of the default `DEV.MYAPP.EVENTS.ORDERPLACED`. + +## Topology configuration + +The topology strategy is set via the `Topology` property: + +```csharp +var transport = new IbmMqTransport(options => +{ + // ... connection settings ... + options.Topology = TopicTopology.TopicPerEvent(); +}); +``` + +The `TopicPerEvent` topology is currently the only built-in topology. diff --git a/transports/ibmmq/transactions.md b/transports/ibmmq/transactions.md new file mode 100644 index 00000000000..638dc2e2843 --- /dev/null +++ b/transports/ibmmq/transactions.md @@ -0,0 +1,85 @@ +--- +title: Transaction support +summary: The design and implementation details of IBM MQ transport transaction support +reviewed: 2026-02-19 +component: IbmMq +--- + +The IBM MQ transport supports the following [transport transaction modes](/transports/transactions.md): + +- Transport transaction - Sends atomic with receive +- Transport transaction - Receive only (default) +- Unreliable (transactions disabled) + +`TransactionScope` mode is not supported because the IBM MQ .NET client does not participate in System.Transactions distributed transactions. + +> [!NOTE] +> `Exactly once` message processing without distributed transactions can be achieved with any transport using the [Outbox](/nservicebus/outbox/) feature. + +## Sends atomic with receive + +In `SendsAtomicWithReceive` mode, the message receive and all send/publish operations are performed under a single IBM MQ syncpoint. Either all operations succeed together or they all roll back. + +This is achieved by sharing the same `MQQueueManager` connection between the receiver and the dispatcher. Messages are put with `MQPMO_SYNCPOINT`, and the syncpoint is committed only when processing completes successfully. + +```mermaid +graph LR + subgraph "Single MQ Syncpoint" + R[Receive message] --> H[Handle message] + H --> S1[Send reply] + H --> S2[Publish event] + end + S1 --> C{Success?} + S2 --> C + C -->|Yes| CM[Commit all] + C -->|No| RB[Rollback all] +``` + +```csharp +var endpointConfiguration = new EndpointConfiguration("MyEndpoint"); +var transport = new IbmMqTransport(options => +{ + // ... connection settings ... +}); +transport.TransportTransactionMode = TransportTransactionMode.SendsAtomicWithReceive; +endpointConfiguration.UseTransport(transport); +``` + +> [!NOTE] +> Messages sent outside of a handler (e.g., via `IMessageSession`) are dispatched using an independent connection and are not included in the syncpoint. + +## Receive only + +In `ReceiveOnly` mode, each message is received under its own syncpoint. Successfully processed messages are committed; failed messages are backed out and returned to the queue. Send and publish operations use a separate connection and are not part of the receive transaction. + +This is the default transaction mode. + +```csharp +var endpointConfiguration = new EndpointConfiguration("MyEndpoint"); +var transport = new IbmMqTransport(options => +{ + // ... connection settings ... +}); +transport.TransportTransactionMode = TransportTransactionMode.ReceiveOnly; +endpointConfiguration.UseTransport(transport); +``` + +> [!WARNING] +> If the connection to the queue manager is lost after a message has been successfully processed but before the commit, the queue manager will back out the message and it will be redelivered. This can result in the endpoint processing the same message multiple times. Use the [Outbox](/nservicebus/outbox/) feature to guarantee exactly-once processing. + +## Unreliable (transactions disabled) + +In `None` mode, messages are consumed without any transactional guarantees. If processing fails, the message is lost. Send and publish operations are also non-transactional. + +```csharp +var endpointConfiguration = new EndpointConfiguration("MyEndpoint"); +var transport = new IbmMqTransport(options => +{ + // ... connection settings ... +}); +transport.TransportTransactionMode = TransportTransactionMode.None; +endpointConfiguration.UseTransport(transport); +``` + +> [!CAUTION] +> This mode should only be used when message loss is acceptable, such as for non-critical telemetry or logging messages. diff --git a/transports/index.md b/transports/index.md index b5df8035cdf..09609567060 100644 --- a/transports/index.md +++ b/transports/index.md @@ -25,5 +25,6 @@ Initially, it can be challenging to decide which queuing technology is best for - [RabbitMQ](/transports/rabbitmq/) - [SQL Server](/transports/sql/) - [PostgreSQL](/transports/postgresql/) +- [IBM MQ](/transports/ibmmq/) - [MSMQ](/transports/msmq) - [Learning](/transports/learning/) diff --git a/transports/selecting.md b/transports/selecting.md index b07fe4786f7..d30fa6c3b03 100644 --- a/transports/selecting.md +++ b/transports/selecting.md @@ -24,6 +24,7 @@ Each of the following sections describes the advantages and disadvantages of eac * [Azure Storage Queues](#azure-storage-queues) * [Amazon SQS](#amazon-sqs) * [RabbitMQ](#rabbitmq) +* [IBM MQ](#ibm-mq) * [SQL Server](#sql-server) * [PostgreSQL](#postgresql) * [MSMQ](#msmq) @@ -185,6 +186,33 @@ Similar to [SQL Server](#sql-server), the PostgreSQL transport uses relational d - For native integration with other platforms. - When RabbitMQ is already used in the organization and the benefit of introducing another queueing technology is outweighed by the cost of licenses, training, and ongoing maintenance. +## IBM MQ + +[IBM MQ](https://www.ibm.com/products/mq) is an enterprise messaging platform used across mainframe, distributed, and cloud environments. It is widely deployed in financial services, government, and other regulated industries. + +### Advantages + +- Enterprise-grade messaging with decades of proven reliability in mission-critical systems +- Native integration with mainframe (z/OS) and midrange (IBM i) systems already using IBM MQ +- Supports atomic sends with receive via IBM MQ syncpoint +- Native publish-subscribe via topics and durable subscriptions; does not require NServiceBus persistence for storing event subscriptions +- Built-in high availability with multi-instance queue managers +- SSL/TLS encryption and certificate-based authentication + +### Disadvantages + +- Commercial licensing costs (per-VPC or per-core) +- Queue and topic names are limited to 48 characters, which can require custom name sanitization +- Does not support native delayed delivery +- Queue manager administration requires specialized IBM MQ skills +- Fewer .NET community resources compared to RabbitMQ + +### When to select this transport + +- When the organization already uses IBM MQ and wants to integrate .NET applications with existing messaging infrastructure +- For systems that must communicate with mainframe or legacy applications over IBM MQ +- When enterprise-grade reliability and support contracts are required +- In regulated industries where IBM MQ is already an approved messaging platform ## Amazon SQS From 0e4c438161c983c024651ed5e5a999e00b523da7 Mon Sep 17 00:00:00 2001 From: Ramon Smits Date: Thu, 19 Feb 2026 10:14:51 +0000 Subject: [PATCH 02/19] =?UTF-8?q?=E2=9A=9C=EF=B8=8F=20Simplify=20IBM=20MQ?= =?UTF-8?q?=20transport=20pages,=20move=20technical=20details?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - index.md: Remove implementation details (ResourceNameSanitizer code, MQPER_PERSISTENT constants, character set lists). Keep as user-facing overview with links to detail pages. - transactions.md: Remove MQ-specific internals (MQPMO_SYNCPOINT, MQQueueManager connection sharing). Keep conceptual descriptions. - topology.md: Remove detailed implementation steps and topic naming customization code. Keep diagrams and user-facing concepts. Link to connection-settings for configuration details. - native-integration.md: Tone down MQ internals while keeping the essential interop information. - connection-settings.md: Absorb topic naming configuration, resource name sanitization, and message persistence from other pages. --- transports/ibmmq/connection-settings.md | 96 +++++++++++++++++-------- transports/ibmmq/index.md | 61 ++++------------ transports/ibmmq/native-integration.md | 66 ++++++----------- transports/ibmmq/topology.md | 89 ++++------------------- transports/ibmmq/transactions.md | 37 +++------- 5 files changed, 126 insertions(+), 223 deletions(-) diff --git a/transports/ibmmq/connection-settings.md b/transports/ibmmq/connection-settings.md index 05fe1de78fd..6cff134571d 100644 --- a/transports/ibmmq/connection-settings.md +++ b/transports/ibmmq/connection-settings.md @@ -1,6 +1,6 @@ --- title: Connection Settings -summary: Information about connection settings for the IBM MQ transport, including SSL/TLS and high availability +summary: Connection settings for the IBM MQ transport, including SSL/TLS, high availability, and advanced configuration reviewed: 2026-02-19 component: IbmMq --- @@ -47,7 +47,7 @@ var transport = new IbmMqTransport(options => ## Application name -The application name appears in IBM MQ monitoring tools (e.g., `DISPLAY CONN(*)` in runmqsc) and is useful for identifying connections: +The application name appears in IBM MQ monitoring tools and is useful for identifying connections: ```csharp var transport = new IbmMqTransport(options => @@ -61,7 +61,7 @@ If not specified, the application name defaults to the entry assembly name. ## High availability -For high availability scenarios with multi-instance queue managers or a set of candidate queue managers, provide a connection name list instead of a single host and port: +For high availability scenarios with multi-instance queue managers, provide a connection name list instead of a single host and port: ```csharp var transport = new IbmMqTransport(options => @@ -73,14 +73,14 @@ var transport = new IbmMqTransport(options => }); ``` -When `Connections` is specified, the `Host` and `Port` properties are ignored. The IBM MQ client will attempt each connection in order, connecting to the first available queue manager. +When `Connections` is specified, the `Host` and `Port` properties are ignored. The client will attempt each connection in order, connecting to the first available queue manager. > [!NOTE] -> All entries in the connection name list must point to instances of the same queue manager (by name). This is not a load balancing mechanism; it provides failover to a standby instance. +> All entries in the connection name list must point to instances of the same queue manager (by name). This provides failover to a standby instance, not load balancing. ## SSL/TLS -To enable encrypted communication with the queue manager, configure the SSL key repository and cipher specification. The cipher must match the `SSLCIPH` attribute on the SVRCONN channel. +To enable encrypted communication, configure the SSL key repository and cipher specification. The cipher must match the `SSLCIPH` attribute on the SVRCONN channel. ```csharp var transport = new IbmMqTransport(options => @@ -96,13 +96,13 @@ var transport = new IbmMqTransport(options => |Value|Description| |:---|---| -|`*SYSTEM`|Use the Windows system certificate store| -|`*USER`|Use the Windows user certificate store| +|`*SYSTEM`|Windows system certificate store| +|`*USER`|Windows user certificate store| |File path|Path to a key database file (without extension), e.g., `/var/mqm/ssl/key`| ### Peer name verification -For additional security, verify the queue manager's certificate distinguished name: +Verify the queue manager's certificate distinguished name for additional security: ```csharp var transport = new IbmMqTransport(options => @@ -114,28 +114,71 @@ var transport = new IbmMqTransport(options => }); ``` -### Key reset count +> [!NOTE] +> Both `SslKeyRepository` and `CipherSpec` must be specified together. Setting one without the other will cause a configuration validation error. + +## Topic naming + +Topics are named using a configurable `TopicNaming` strategy. The default uses a prefix of `DEV` and the fully qualified type name. -SSL key renegotiation can be configured to periodically reset the secret key after a specified number of bytes: +### Custom topic prefix + +To change the prefix: ```csharp var transport = new IbmMqTransport(options => { - // ... connection and SSL settings ... - options.KeyResetCount = 40000; // Renegotiate every 40 KB + // ... connection settings ... + options.TopicNaming = new TopicNaming("PROD"); }); ``` -The default value of `0` disables client-side key reset, deferring to the channel or queue manager setting. +### Custom topic naming strategy -> [!NOTE] -> Both `SslKeyRepository` and `CipherSpec` must be specified together. Setting one without the other will cause a configuration validation error. +IBM MQ topic names are limited to 48 characters. If event type names are long, subclass `TopicNaming` to implement a shortening strategy: + +```csharp +public class ShortTopicNaming() : TopicNaming("APP") +{ + public override string GenerateTopicName(Type eventType) + { + return $"APP.{eventType.Name}".ToUpperInvariant(); + } +} +``` + +```csharp +var transport = new IbmMqTransport(options => +{ + // ... connection settings ... + options.TopicNaming = new ShortTopicNaming(); +}); +``` + +## Resource name sanitization + +IBM MQ queue and topic names are limited to 48 characters and allow only `A-Z`, `a-z`, `0-9`, `.`, and `_`. If endpoint names contain invalid characters or are too long, configure a sanitizer: + +```csharp +var transport = new IbmMqTransport(options => +{ + // ... connection settings ... + options.ResourceNameSanitizer = name => + { + var sanitized = name.Replace("-", ".").Replace("/", "."); + return sanitized.Length > 48 ? sanitized[..48] : sanitized; + }; +}); +``` + +> [!WARNING] +> Ensure the sanitizer produces deterministic and unique names. Two different input names mapping to the same sanitized name will cause messages to be delivered to the wrong endpoint. ## Message processing settings ### Polling interval -The transport polls queues for messages using the IBM MQ `MQGET` with wait. The wait interval controls how long each poll waits for a message before returning: +The wait interval controls how long each poll waits for a message before returning: ```csharp var transport = new IbmMqTransport(options => @@ -149,11 +192,9 @@ var transport = new IbmMqTransport(options => |:---|---|---| |MessageWaitInterval|5000 ms|100–30,000 ms| -Shorter intervals improve responsiveness but increase CPU usage. Longer intervals reduce CPU usage but increase message processing latency. - ### Maximum message size -The maximum message size the transport will accept. This should match or be less than the queue manager's `MAXMSGL` setting: +Should match or be less than the queue manager's `MAXMSGL` setting: ```csharp var transport = new IbmMqTransport(options => @@ -165,11 +206,11 @@ var transport = new IbmMqTransport(options => |Setting|Default|Range| |:---|---|---| -|MaxMessageLength|4 MB (4,194,304 bytes)|1 KB – 100 MB| +|MaxMessageLength|4 MB|1 KB – 100 MB| ### Character set -The Coded Character Set Identifier (CCSID) used for message text encoding: +The Coded Character Set Identifier (CCSID) used for message text encoding. The default is UTF-8 (1208), which is recommended for most scenarios. ```csharp var transport = new IbmMqTransport(options => @@ -179,10 +220,9 @@ var transport = new IbmMqTransport(options => }); ``` -Common values: +## Message persistence -|CCSID|Encoding| -|:---|---| -|1208|UTF-8 (recommended)| -|819|ISO 8859-1 (Latin-1)| -|1252|Windows Latin-1| +By default, all messages are sent as persistent, meaning they survive queue manager restarts. Messages marked with the `NonDurableMessage` header are sent as non-persistent for higher throughput. + +> [!CAUTION] +> Non-persistent messages are lost if the queue manager restarts before they are consumed. diff --git a/transports/ibmmq/index.md b/transports/ibmmq/index.md index a9e6223a705..ca19db52dc8 100644 --- a/transports/ibmmq/index.md +++ b/transports/ibmmq/index.md @@ -11,9 +11,7 @@ Provides support for sending messages over [IBM MQ](https://www.ibm.com/products ## Broker compatibility -The transport requires IBM MQ 9.0 or later. It uses the managed .NET client library (`IBMMQDotnetClient`) which communicates with queue managers via the client connection (SVRCONN) channel. - -The transport has been tested with: +The transport requires IBM MQ 9.0 or later. It has been tested with: - IBM MQ on Linux and Windows - IBM MQ on z/OS @@ -25,11 +23,11 @@ The transport has been tested with: |Feature | | |:--- |--- |Transactions |None, ReceiveOnly, SendsAtomicWithReceive -|Pub/Sub |Native (via IBM MQ topics and durable subscriptions) +|Pub/Sub |Native |Timeouts |Not natively supported -|Large message bodies |Up to 100 MB (configurable, must match queue manager MAXMSGL) +|Large message bodies |Up to 100 MB (configurable) |Scale-out |Competing consumer -|Scripted Deployment |Supported via [CLI tool](operations-scripting.md) and IBM MQ admin commands +|Scripted Deployment |Supported via [CLI tool](operations-scripting.md) |Installers |Optional |Native integration |[Supported](native-integration.md) |Case Sensitive |Yes @@ -61,51 +59,18 @@ See [connection settings](connection-settings.md) for all available connection a ### Advantages - Enterprise-grade messaging platform with decades of proven reliability in mission-critical systems. -- Native publish-subscribe mechanism via IBM MQ topics and durable subscriptions; does not require NServiceBus persistence for storing event subscriptions. -- Supports atomic sends with receive using IBM MQ syncpoint, ensuring send and receive operations commit or roll back together. -- Integrates with mainframe and legacy systems that already use IBM MQ, bridging distributed .NET applications with z/OS, IBM i, and other platforms. +- Native publish-subscribe mechanism; does not require NServiceBus persistence for storing event subscriptions. +- Supports [atomic sends with receive](/transports/transactions.md), ensuring send and receive operations commit or roll back together. +- Integrates with mainframe and legacy systems that already use IBM MQ, bridging .NET applications with z/OS, IBM i, and other platforms. - Built-in high availability via multi-instance queue managers and connection name lists. -- Supports SSL/TLS encryption and certificate-based authentication for secure communication. +- Supports SSL/TLS encryption and certificate-based authentication. - Supports the [competing consumer](https://www.enterpriseintegrationpatterns.com/patterns/messaging/CompetingConsumers.html) pattern out of the box for horizontal scaling. ### Disadvantages -- Requires an IBM MQ license; IBM MQ is a commercial product with per-VPC or per-core licensing. -- IBM MQ queue and topic names are limited to 48 characters, which can require custom name sanitization for longer endpoint or event type names. -- Does not support native delayed delivery; delayed delivery requires an external timeout storage mechanism. +- Requires an IBM MQ license; IBM MQ is a commercial product. +- Queue and topic names are limited to 48 characters, which can require [custom name sanitization](connection-settings.md#resource-name-sanitization). +- Does not support native delayed delivery; requires an external timeout storage mechanism. - Does not support `TransactionScope` mode (distributed transactions). -- Fewer .NET-focused community resources compared to RabbitMQ or cloud-native alternatives. -- Queue manager administration requires specialized IBM MQ knowledge (runmqsc, MQ Explorer, or equivalent tooling). - -## Queue naming constraints - -IBM MQ imposes strict naming rules on queues and topics: - -- Maximum 48 characters -- Allowed characters: `A-Z`, `a-z`, `0-9`, `.`, `_` -- No hyphens, spaces, or other special characters - -If endpoint or event type names exceed these constraints, configure a `ResourceNameSanitizer` to transform names: - -```csharp -var transport = new IbmMqTransport(options => -{ - // ... connection settings ... - options.ResourceNameSanitizer = name => - { - // Replace invalid characters and truncate - var sanitized = name.Replace("-", ".").Replace("/", "."); - return sanitized.Length > 48 ? sanitized[..48] : sanitized; - }; -}); -``` - -> [!WARNING] -> Ensure the sanitizer produces deterministic and unique names. Two different input names mapping to the same sanitized name will cause messages to be delivered to the wrong endpoint. - -## Message persistence - -By default, all messages are sent as persistent (`MQPER_PERSISTENT`), meaning they survive queue manager restarts. To send non-persistent messages for higher throughput at the expense of durability, mark messages with the `NonDurableMessage` header. - -> [!CAUTION] -> Non-persistent messages are lost if the queue manager restarts before they are consumed. +- Fewer .NET community resources compared to RabbitMQ or cloud-native alternatives. +- Queue manager administration requires specialized IBM MQ knowledge. diff --git a/transports/ibmmq/native-integration.md b/transports/ibmmq/native-integration.md index f0e9daebb16..dfd193f7d10 100644 --- a/transports/ibmmq/native-integration.md +++ b/transports/ibmmq/native-integration.md @@ -5,74 +5,54 @@ reviewed: 2026-02-19 component: IbmMq --- -The IBM MQ transport stores NServiceBus messages using standard IBM MQ message structures, making it possible to send and receive messages between NServiceBus endpoints and native IBM MQ applications. +The IBM MQ transport uses standard IBM MQ message structures, making it possible to exchange messages between NServiceBus endpoints and native IBM MQ applications. ## Message structure -NServiceBus messages on IBM MQ consist of two parts: +NServiceBus messages on IBM MQ consist of a standard message descriptor (MQMD) and custom properties stored in MQRFH2 headers. -### MQMD (Message Descriptor) +### MQMD fields -The MQMD is the standard IBM MQ message header. The transport maps the following fields: +The transport maps NServiceBus concepts to the following MQMD fields: -|MQMD field|NServiceBus usage| +|MQMD field|Usage| |:---|---| -|MessageId|24-byte binary; set from the `NServiceBus.MessageId` header as a GUID padded to 24 bytes| -|CorrelationId|24-byte binary; set from the `NServiceBus.CorrelationId` header| -|MessageType|Always `MQMT_DATAGRAM` (8)| -|Persistence|`MQPER_PERSISTENT` by default; `MQPER_NOT_PERSISTENT` if `NonDurableMessage` header is set| -|Expiry|Set from `DiscardIfNotReceivedBefore` in tenths of seconds; `MQEI_UNLIMITED` if not specified| -|CharacterSet|UTF-8 (CCSID 1208) by default| +|MessageId|Set from the `NServiceBus.MessageId` header| +|CorrelationId|Set from the `NServiceBus.CorrelationId` header| +|MessageType|Always `MQMT_DATAGRAM`| +|Persistence|Persistent by default; non-persistent if `NonDurableMessage` header is set| +|Expiry|Set from the time-to-be-received setting; unlimited if not specified| |ReplyToQueueName|Set from the `NServiceBus.ReplyToAddress` header| -### MQRFH2 properties +### NServiceBus headers -All NServiceBus headers are stored as MQRFH2 custom properties on the message. Property names are escaped to comply with IBM MQ naming rules: +All NServiceBus headers are stored as MQRFH2 message properties. Property names are escaped because IBM MQ only allows alphanumeric characters and underscores in property names. The escaping rules are: - Underscores are doubled: `_` becomes `__` -- Non-alphanumeric characters are encoded as `_xHHHH` where `HHHH` is the Unicode code point - -For example, the header `NServiceBus.MessageId` is stored as the property `NServiceBus_x002EMessageId`. - -Two additional manifest properties are included: - -|Property|Purpose| -|:---|---| -|`nsbhdrs`|Comma-separated list of all escaped header names| -|`nsbempty`|Comma-separated list of escaped header names with empty values| +- Other special characters are encoded as `_xHHHH` (e.g., `.` becomes `_x002E`) > [!NOTE] -> IBM MQ silently discards string properties with empty values. The manifest properties work around this limitation by tracking which headers exist and which have empty values. +> IBM MQ silently discards string properties with empty values. The transport includes manifest properties (`nsbhdrs` and `nsbempty`) to track all header names and preserve empty values. ## Sending messages from native applications To send a message to an NServiceBus endpoint from a native IBM MQ application: -1. Create an `MQMessage` targeting the endpoint's input queue. -2. Set the message body to a serialized representation of the message (e.g., JSON or XML). -3. Set MQRFH2 properties for the required NServiceBus headers. +1. Put a message on the endpoint's input queue. +2. Set the message body to a serialized representation (e.g., JSON). +3. Set the following MQRFH2 properties as a minimum: -The minimum required headers are: - -|Header (escaped property name)|Description| +|Property name|Description| |:---|---| |`NServiceBus_x002EEnclosedMessageTypes`|The fully qualified .NET type name of the message| -|`NServiceBus_x002EContentType`|The MIME type of the body serialization (e.g., `application/json`)| -|`NServiceBus_x002EMessageId`|A unique identifier for the message (typically a GUID)| +|`NServiceBus_x002EContentType`|The MIME type of the body (e.g., `application/json`)| +|`NServiceBus_x002EMessageId`|A unique identifier (typically a GUID)| > [!WARNING] -> If the `NServiceBus.EnclosedMessageTypes` header is missing, the endpoint will not be able to deserialize and route the message. +> If the `EnclosedMessageTypes` header is missing, the endpoint will not be able to deserialize and route the message. ## Receiving messages in native applications -Native applications can consume messages from queues that NServiceBus publishes or sends to. The application should: - -1. Read the message body bytes from the `MQMessage`. -2. Read MQRFH2 properties to access NServiceBus headers. Use the `nsbhdrs` manifest to enumerate all headers, and `nsbempty` to identify headers with empty values. -3. Unescape property names by replacing `__` with `_` and `_xHHHH` with the corresponding character. - -## Interoperability considerations +Native applications can consume messages from queues that NServiceBus publishes or sends to. The message body contains the serialized payload, and NServiceBus headers are available as MQRFH2 properties. -- NServiceBus always sets `MessageType` to `MQMT_DATAGRAM`. Native applications that rely on `MQMT_REQUEST`/`MQMT_REPLY` patterns should use a separate set of queues or translate at an integration boundary. -- The body encoding is determined by the NServiceBus serializer configuration, typically JSON. Native applications must use the same serialization format. -- Message expiry (time-to-be-received) is mapped to the MQMD `Expiry` field in tenths of seconds. +To read headers, use the `nsbhdrs` manifest property to enumerate all header names, and unescape property names by replacing `__` with `_` and `_xHHHH` with the corresponding character. diff --git a/transports/ibmmq/topology.md b/transports/ibmmq/topology.md index 79adfc5515c..dab9cf00f89 100644 --- a/transports/ibmmq/topology.md +++ b/transports/ibmmq/topology.md @@ -9,7 +9,7 @@ The IBM MQ transport implements publish/subscribe messaging using IBM MQ's nativ ## Topic-per-event topology -The default topology creates one IBM MQ topic object per concrete event type. When an event is published, the message is sent to the corresponding topic. Subscribers create durable subscriptions against the topic, with messages delivered to their input queue. +The default topology creates one IBM MQ topic per concrete event type. When an event is published, the message is sent to the corresponding topic. Subscribers create durable subscriptions against the topic, with messages delivered to their input queue. ```mermaid graph LR @@ -20,7 +20,7 @@ graph LR ### Sending (unicast) -Unicast messages (commands) are sent directly to the destination queue by name. No topics or exchanges are involved. +Commands are sent directly to the destination queue by name. No topics are involved. ```mermaid graph LR @@ -30,27 +30,19 @@ graph LR ### Publishing (multicast) -When an endpoint publishes an event: - -1. The transport resolves the IBM MQ topic for the event type using the configured `TopicNaming` strategy. -2. If infrastructure setup is enabled, the topic object is created if it does not already exist. -3. The message is published to the topic. IBM MQ delivers a copy to every durable subscription on that topic. +When an endpoint publishes an event, the message is published to the topic for that event type. IBM MQ delivers a copy to every endpoint with a durable subscription on that topic. ### Subscribing -When an endpoint subscribes to an event type: - -1. The transport resolves all concrete types assignable to the subscribed type (supporting polymorphic subscriptions). -2. For each concrete type, a durable subscription is created linking the topic to the endpoint's input queue. -3. When the endpoint starts, existing subscriptions are resumed. +When an endpoint subscribes to an event type, the transport creates a durable subscription linking the event's topic to the endpoint's input queue. When the endpoint starts, existing subscriptions are resumed automatically. ### Unsubscribing -When an endpoint unsubscribes from an event type, the corresponding durable subscriptions are deleted from the queue manager. +When an endpoint unsubscribes from an event type, the durable subscription is deleted from the queue manager. ## Polymorphism -The transport supports polymorphic event subscriptions via subscriber-side fan-out. When subscribing to a base class or interface, the transport scans loaded assemblies for all concrete types that implement or extend the subscribed type and creates a separate durable subscription for each. +The transport supports polymorphic event subscriptions. When subscribing to a base class or interface, the transport discovers all concrete types that implement it and creates a subscription for each. For example, given: @@ -60,7 +52,7 @@ public class OrderPlaced : IOrderEvent { } public class OrderCancelled : IOrderEvent { } ``` -Subscribing to `IOrderEvent` creates durable subscriptions for both `OrderPlaced` and `OrderCancelled`. Each published event is delivered exactly once to the subscriber regardless of how many type hierarchies match. +Subscribing to `IOrderEvent` creates subscriptions for both `OrderPlaced` and `OrderCancelled`: ```mermaid graph LR @@ -75,66 +67,13 @@ graph LR ## Topic naming -Topics are named using a configurable `TopicNaming` strategy. The default strategy uses a prefix (default: `DEV`) and the fully qualified type name: +Topics are named using a configurable strategy. The default uses a prefix (default: `DEV`) and the fully qualified type name: -|Concept|Format|Example| -|:---|---|---| -|Topic name (admin object)|`PREFIX.FULL.TYPE.NAME` (uppercase)|`DEV.MYAPP.EVENTS.ORDERPLACED`| -|Topic string|`prefix/full.type.name/` (lowercase)|`dev/myapp.events.orderplaced/`| -|Subscription name|`endpointname:topicstring`|`OrderService:dev/myapp.events.orderplaced/`| +|Concept|Example| +|:---|---| +|Topic name|`DEV.MYAPP.EVENTS.ORDERPLACED`| +|Topic string|`dev/myapp.events.orderplaced/`| +|Subscription name|`OrderService:dev/myapp.events.orderplaced/`| > [!WARNING] -> IBM MQ topic admin names are limited to 48 characters. If the generated topic name exceeds this limit, the transport throws an `InvalidOperationException` at startup. Override `TopicNaming.GenerateTopicName` to implement a shortening strategy. - -### Custom topic naming - -To customize how event types map to topic names, subclass `TopicNaming`: - -```csharp -public class ShortTopicNaming() : TopicNaming("APP") -{ - public override string GenerateTopicName(Type eventType) - { - // Use only the type name, not the full namespace - return $"APP.{eventType.Name}".ToUpperInvariant(); - } -} -``` - -Then configure the transport: - -```csharp -var transport = new IbmMqTransport(options => -{ - // ... connection settings ... - options.TopicNaming = new ShortTopicNaming(); -}); -``` - -### Custom topic prefix - -To change just the prefix without subclassing: - -```csharp -var transport = new IbmMqTransport(options => -{ - // ... connection settings ... - options.TopicNaming = new TopicNaming("PROD"); -}); -``` - -This produces topic names like `PROD.MYAPP.EVENTS.ORDERPLACED` instead of the default `DEV.MYAPP.EVENTS.ORDERPLACED`. - -## Topology configuration - -The topology strategy is set via the `Topology` property: - -```csharp -var transport = new IbmMqTransport(options => -{ - // ... connection settings ... - options.Topology = TopicTopology.TopicPerEvent(); -}); -``` - -The `TopicPerEvent` topology is currently the only built-in topology. +> IBM MQ topic names are limited to 48 characters. If the generated name exceeds this limit, the transport throws an exception at startup. See [topic naming configuration](connection-settings.md#topic-naming) for how to customize the naming strategy. diff --git a/transports/ibmmq/transactions.md b/transports/ibmmq/transactions.md index 638dc2e2843..27403bb3478 100644 --- a/transports/ibmmq/transactions.md +++ b/transports/ibmmq/transactions.md @@ -1,6 +1,6 @@ --- title: Transaction support -summary: The design and implementation details of IBM MQ transport transaction support +summary: Transaction modes supported by the IBM MQ transport reviewed: 2026-02-19 component: IbmMq --- @@ -11,74 +11,53 @@ The IBM MQ transport supports the following [transport transaction modes](/trans - Transport transaction - Receive only (default) - Unreliable (transactions disabled) -`TransactionScope` mode is not supported because the IBM MQ .NET client does not participate in System.Transactions distributed transactions. +`TransactionScope` mode is not supported because the IBM MQ .NET client does not participate in distributed transactions. > [!NOTE] -> `Exactly once` message processing without distributed transactions can be achieved with any transport using the [Outbox](/nservicebus/outbox/) feature. +> Exactly-once message processing without distributed transactions can be achieved with any transport using the [Outbox](/nservicebus/outbox/) feature. ## Sends atomic with receive -In `SendsAtomicWithReceive` mode, the message receive and all send/publish operations are performed under a single IBM MQ syncpoint. Either all operations succeed together or they all roll back. - -This is achieved by sharing the same `MQQueueManager` connection between the receiver and the dispatcher. Messages are put with `MQPMO_SYNCPOINT`, and the syncpoint is committed only when processing completes successfully. - -```mermaid -graph LR - subgraph "Single MQ Syncpoint" - R[Receive message] --> H[Handle message] - H --> S1[Send reply] - H --> S2[Publish event] - end - S1 --> C{Success?} - S2 --> C - C -->|Yes| CM[Commit all] - C -->|No| RB[Rollback all] -``` +In `SendsAtomicWithReceive` mode, the message receive and all outgoing send/publish operations are committed or rolled back as a single unit of work. ```csharp -var endpointConfiguration = new EndpointConfiguration("MyEndpoint"); var transport = new IbmMqTransport(options => { // ... connection settings ... }); transport.TransportTransactionMode = TransportTransactionMode.SendsAtomicWithReceive; -endpointConfiguration.UseTransport(transport); ``` > [!NOTE] -> Messages sent outside of a handler (e.g., via `IMessageSession`) are dispatched using an independent connection and are not included in the syncpoint. +> Messages sent outside of a handler (e.g., via `IMessageSession`) are not included in the atomic operation. ## Receive only -In `ReceiveOnly` mode, each message is received under its own syncpoint. Successfully processed messages are committed; failed messages are backed out and returned to the queue. Send and publish operations use a separate connection and are not part of the receive transaction. +In `ReceiveOnly` mode, the message receive is transactional. Successfully processed messages are committed; failed messages are returned to the queue. Outgoing send and publish operations are not part of the receive transaction. This is the default transaction mode. ```csharp -var endpointConfiguration = new EndpointConfiguration("MyEndpoint"); var transport = new IbmMqTransport(options => { // ... connection settings ... }); transport.TransportTransactionMode = TransportTransactionMode.ReceiveOnly; -endpointConfiguration.UseTransport(transport); ``` > [!WARNING] -> If the connection to the queue manager is lost after a message has been successfully processed but before the commit, the queue manager will back out the message and it will be redelivered. This can result in the endpoint processing the same message multiple times. Use the [Outbox](/nservicebus/outbox/) feature to guarantee exactly-once processing. +> If the connection to the queue manager is lost after processing succeeds but before the commit, the message will be redelivered. This can result in duplicate processing. Use the [Outbox](/nservicebus/outbox/) feature to guarantee exactly-once processing. ## Unreliable (transactions disabled) -In `None` mode, messages are consumed without any transactional guarantees. If processing fails, the message is lost. Send and publish operations are also non-transactional. +In `None` mode, messages are consumed without any transactional guarantees. If processing fails, the message is lost. ```csharp -var endpointConfiguration = new EndpointConfiguration("MyEndpoint"); var transport = new IbmMqTransport(options => { // ... connection settings ... }); transport.TransportTransactionMode = TransportTransactionMode.None; -endpointConfiguration.UseTransport(transport); ``` > [!CAUTION] From 287fb751f08d2bc7dc587ab4b2cbcbfe071f3ede Mon Sep 17 00:00:00 2001 From: Ramon Smits Date: Thu, 19 Feb 2026 10:14:56 +0000 Subject: [PATCH 03/19] =?UTF-8?q?=E2=9C=A8=20Improve=20SendsAtomicWithRece?= =?UTF-8?q?ive=20explanation=20in=20transaction=20modes=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add mermaid diagram showing how receive, send, and publish operations are committed or rolled back as a single unit. Clarify that database operations are not part of the transport transaction and link to the Outbox feature. --- transports/transactions.md | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/transports/transactions.md b/transports/transactions.md index 43a20c50a70..cbff3bed712 100644 --- a/transports/transactions.md +++ b/transports/transactions.md @@ -79,7 +79,22 @@ When the `TransportTransactionMode` is set to `TransactionScope`, handlers execu ### Transport transaction - Sends atomic with Receive -Some transports support enlisting outgoing operations in the current receive transaction. This ensures that messages are only sent to downstream endpoints when the receive operation completes successfully, preventing ghost messages during retries. +Some transports support enlisting outgoing operations in the current receive transaction. This ensures that the incoming message and all outgoing messages are committed or rolled back as a single unit of work, preventing ghost messages during retries. + +```mermaid +graph LR + subgraph "Atomic operation" + R[Receive message] --> H[Handle message] + H --> S1[Send reply] + H --> S2[Publish event] + end + S1 --> C{Success?} + S2 --> C + C -->|Yes| CM[Commit all] + C -->|No| RB[Rollback all] +``` + +When processing succeeds, the receive and all outgoing operations are committed together. When processing fails, everything is rolled back — the incoming message is returned to the queue and no outgoing messages are sent. Use the following code to enable this mode: @@ -89,6 +104,9 @@ snippet: TransportTransactionAtomicSendsWithReceive This mode provides the same consistency guarantees as *Receive Only* mode, with an important addition: it prevents ghost messages. Since all outgoing operations are committed atomically with the receive operation, messages are never sent if the handler fails and needs to be retried. +> [!NOTE] +> Database operations within the handler are still not part of the transport transaction. To ensure consistency between message operations and database changes, use the [Outbox](#outbox) feature. + ### Transport transaction - Receive Only In this mode, the receive operation is wrapped in the transport's native transaction. The message is not permanently deleted from the queue until at least one processing attempt completes successfully. If processing fails, the message remains in the queue and will be [retried](/nservicebus/recoverability/). From f04d59e5392f21da4ed19f5b5eb06e778c39595e Mon Sep 17 00:00:00 2001 From: Ramon Smits Date: Wed, 25 Feb 2026 13:01:14 +0100 Subject: [PATCH 04/19] snippets --- Snippets/IbmMq/IbmMq.sln | 34 +++ Snippets/IbmMq/IbmMq_1/IbmMq_1.csproj | 11 + Snippets/IbmMq/IbmMq_1/ShortTopicNaming.cs | 14 ++ Snippets/IbmMq/IbmMq_1/Usage.cs | 236 +++++++++++++++++++++ transports/ibmmq/connection-settings.md | 122 ++--------- transports/ibmmq/index.md | 16 +- transports/ibmmq/transactions.md | 24 +-- 7 files changed, 313 insertions(+), 144 deletions(-) create mode 100644 Snippets/IbmMq/IbmMq.sln create mode 100644 Snippets/IbmMq/IbmMq_1/IbmMq_1.csproj create mode 100644 Snippets/IbmMq/IbmMq_1/ShortTopicNaming.cs create mode 100644 Snippets/IbmMq/IbmMq_1/Usage.cs diff --git a/Snippets/IbmMq/IbmMq.sln b/Snippets/IbmMq/IbmMq.sln new file mode 100644 index 00000000000..de3e4d52d10 --- /dev/null +++ b/Snippets/IbmMq/IbmMq.sln @@ -0,0 +1,34 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IbmMq_1", "IbmMq_1\IbmMq_1.csproj", "{E74E64BC-8D4F-4528-A524-F56AC7EDB222}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {E74E64BC-8D4F-4528-A524-F56AC7EDB222}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E74E64BC-8D4F-4528-A524-F56AC7EDB222}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E74E64BC-8D4F-4528-A524-F56AC7EDB222}.Debug|x64.ActiveCfg = Debug|Any CPU + {E74E64BC-8D4F-4528-A524-F56AC7EDB222}.Debug|x64.Build.0 = Debug|Any CPU + {E74E64BC-8D4F-4528-A524-F56AC7EDB222}.Debug|x86.ActiveCfg = Debug|Any CPU + {E74E64BC-8D4F-4528-A524-F56AC7EDB222}.Debug|x86.Build.0 = Debug|Any CPU + {E74E64BC-8D4F-4528-A524-F56AC7EDB222}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E74E64BC-8D4F-4528-A524-F56AC7EDB222}.Release|Any CPU.Build.0 = Release|Any CPU + {E74E64BC-8D4F-4528-A524-F56AC7EDB222}.Release|x64.ActiveCfg = Release|Any CPU + {E74E64BC-8D4F-4528-A524-F56AC7EDB222}.Release|x64.Build.0 = Release|Any CPU + {E74E64BC-8D4F-4528-A524-F56AC7EDB222}.Release|x86.ActiveCfg = Release|Any CPU + {E74E64BC-8D4F-4528-A524-F56AC7EDB222}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/Snippets/IbmMq/IbmMq_1/IbmMq_1.csproj b/Snippets/IbmMq/IbmMq_1/IbmMq_1.csproj new file mode 100644 index 00000000000..b382211419a --- /dev/null +++ b/Snippets/IbmMq/IbmMq_1/IbmMq_1.csproj @@ -0,0 +1,11 @@ + + + + net10.0 + + + + + + + diff --git a/Snippets/IbmMq/IbmMq_1/ShortTopicNaming.cs b/Snippets/IbmMq/IbmMq_1/ShortTopicNaming.cs new file mode 100644 index 00000000000..d5ff64e8ea2 --- /dev/null +++ b/Snippets/IbmMq/IbmMq_1/ShortTopicNaming.cs @@ -0,0 +1,14 @@ +using System; +using NServiceBus.Transport.IbmMq; + +#region ibmmq-custom-topic-naming-class + +public class ShortTopicNaming() : TopicNaming("APP") +{ + public override string GenerateTopicName(Type eventType) + { + return $"APP.{eventType.Name}".ToUpperInvariant(); + } +} + +#endregion diff --git a/Snippets/IbmMq/IbmMq_1/Usage.cs b/Snippets/IbmMq/IbmMq_1/Usage.cs new file mode 100644 index 00000000000..94f1e9dbed4 --- /dev/null +++ b/Snippets/IbmMq/IbmMq_1/Usage.cs @@ -0,0 +1,236 @@ +using System; +using NServiceBus; +using NServiceBus.Transport.IbmMq; + +class Usage +{ + public void BasicConfig() + { + #region ibmmq-config-basic + + var endpointConfiguration = new EndpointConfiguration("MyEndpoint"); + + var transport = new IbmMqTransport(options => + { + options.Host = "mq-server.example.com"; + options.Port = 1414; + options.Channel = "DEV.APP.SVRCONN"; + options.QueueManagerName = "QM1"; + options.User = "app"; + options.Password = "passw0rd"; + }); + + endpointConfiguration.UseTransport(transport); + + #endregion + } + + public void BasicConnection() + { + #region ibmmq-basic-connection + + var transport = new IbmMqTransport(options => + { + options.Host = "mq-server.example.com"; + options.Port = 1414; + options.Channel = "DEV.APP.SVRCONN"; + options.QueueManagerName = "QM1"; + }); + + #endregion + } + + public void Authentication() + { + #region ibmmq-authentication + + var transport = new IbmMqTransport(options => + { + options.Host = "mq-server.example.com"; + options.QueueManagerName = "QM1"; + options.User = "app"; + options.Password = "passw0rd"; + }); + + #endregion + } + + public void ApplicationName() + { + #region ibmmq-application-name + + var transport = new IbmMqTransport(options => + { + // ... connection settings ... + options.ApplicationName = "OrderService"; + }); + + #endregion + } + + public void HighAvailability() + { + #region ibmmq-high-availability + + var transport = new IbmMqTransport(options => + { + options.QueueManagerName = "QM1"; + options.Channel = "APP.SVRCONN"; + options.Connections.Add("mqhost1(1414)"); + options.Connections.Add("mqhost2(1414)"); + }); + + #endregion + } + + public void SslTls() + { + #region ibmmq-ssl-tls + + var transport = new IbmMqTransport(options => + { + options.Host = "mq-server.example.com"; + options.QueueManagerName = "QM1"; + options.SslKeyRepository = "*SYSTEM"; + options.CipherSpec = "TLS_RSA_WITH_AES_256_CBC_SHA256"; + }); + + #endregion + } + + public void SslPeerName() + { + #region ibmmq-ssl-peer-name + + var transport = new IbmMqTransport(options => + { + // ... connection settings ... + options.SslKeyRepository = "*SYSTEM"; + options.CipherSpec = "TLS_RSA_WITH_AES_256_CBC_SHA256"; + options.SslPeerName = "CN=MQSERVER01,O=MyCompany,C=US"; + }); + + #endregion + } + + public void CustomTopicPrefix() + { + #region ibmmq-custom-topic-prefix + + var transport = new IbmMqTransport(options => + { + // ... connection settings ... + options.TopicNaming = new TopicNaming("PROD"); + }); + + #endregion + } + + public void CustomTopicNamingUsage() + { + #region ibmmq-custom-topic-naming-usage + + var transport = new IbmMqTransport(options => + { + // ... connection settings ... + options.TopicNaming = new ShortTopicNaming(); + }); + + #endregion + } + + public void ResourceSanitization() + { + #region ibmmq-resource-sanitization + + var transport = new IbmMqTransport(options => + { + // ... connection settings ... + options.ResourceNameSanitizer = name => + { + var sanitized = name.Replace("-", ".").Replace("/", "."); + return sanitized.Length > 48 ? sanitized[..48] : sanitized; + }; + }); + + #endregion + } + + public void PollingInterval() + { + #region ibmmq-polling-interval + + var transport = new IbmMqTransport(options => + { + // ... connection settings ... + options.MessageWaitInterval = TimeSpan.FromMilliseconds(2000); + }); + + #endregion + } + + public void MaxMessageSize() + { + #region ibmmq-max-message-size + + var transport = new IbmMqTransport(options => + { + // ... connection settings ... + options.MaxMessageLength = 10 * 1024 * 1024; // 10 MB + }); + + #endregion + } + + public void CharacterSet() + { + #region ibmmq-character-set + + var transport = new IbmMqTransport(options => + { + // ... connection settings ... + options.CharacterSet = 1208; // UTF-8 (default) + }); + + #endregion + } + + public void SendsAtomicWithReceive() + { + #region ibmmq-sends-atomic-with-receive + + var transport = new IbmMqTransport(options => + { + // ... connection settings ... + }); + transport.TransportTransactionMode = TransportTransactionMode.SendsAtomicWithReceive; + + #endregion + } + + public void ReceiveOnly() + { + #region ibmmq-receive-only + + var transport = new IbmMqTransport(options => + { + // ... connection settings ... + }); + transport.TransportTransactionMode = TransportTransactionMode.ReceiveOnly; + + #endregion + } + + public void TransactionsNone() + { + #region ibmmq-transactions-none + + var transport = new IbmMqTransport(options => + { + // ... connection settings ... + }); + transport.TransportTransactionMode = TransportTransactionMode.None; + + #endregion + } +} diff --git a/transports/ibmmq/connection-settings.md b/transports/ibmmq/connection-settings.md index 6cff134571d..f09127d977d 100644 --- a/transports/ibmmq/connection-settings.md +++ b/transports/ibmmq/connection-settings.md @@ -9,15 +9,7 @@ component: IbmMq The transport connects to an IBM MQ queue manager using a host, port, and SVRCONN channel: -```csharp -var transport = new IbmMqTransport(options => -{ - options.Host = "mq-server.example.com"; - options.Port = 1414; - options.Channel = "DEV.APP.SVRCONN"; - options.QueueManagerName = "QM1"; -}); -``` +snippet: ibmmq-basic-connection ### Defaults @@ -32,15 +24,7 @@ var transport = new IbmMqTransport(options => User credentials can be provided to authenticate with the queue manager: -```csharp -var transport = new IbmMqTransport(options => -{ - options.Host = "mq-server.example.com"; - options.QueueManagerName = "QM1"; - options.User = "app"; - options.Password = "passw0rd"; -}); -``` +snippet: ibmmq-authentication > [!NOTE] > When no credentials are provided, the connection uses the operating system identity. This may be appropriate for local development but typically requires explicit credentials in production. @@ -49,13 +33,7 @@ var transport = new IbmMqTransport(options => The application name appears in IBM MQ monitoring tools and is useful for identifying connections: -```csharp -var transport = new IbmMqTransport(options => -{ - // ... connection settings ... - options.ApplicationName = "OrderService"; -}); -``` +snippet: ibmmq-application-name If not specified, the application name defaults to the entry assembly name. @@ -63,15 +41,7 @@ If not specified, the application name defaults to the entry assembly name. For high availability scenarios with multi-instance queue managers, provide a connection name list instead of a single host and port: -```csharp -var transport = new IbmMqTransport(options => -{ - options.QueueManagerName = "QM1"; - options.Channel = "APP.SVRCONN"; - options.Connections.Add("mqhost1(1414)"); - options.Connections.Add("mqhost2(1414)"); -}); -``` +snippet: ibmmq-high-availability When `Connections` is specified, the `Host` and `Port` properties are ignored. The client will attempt each connection in order, connecting to the first available queue manager. @@ -82,15 +52,7 @@ When `Connections` is specified, the `Host` and `Port` properties are ignored. T To enable encrypted communication, configure the SSL key repository and cipher specification. The cipher must match the `SSLCIPH` attribute on the SVRCONN channel. -```csharp -var transport = new IbmMqTransport(options => -{ - options.Host = "mq-server.example.com"; - options.QueueManagerName = "QM1"; - options.SslKeyRepository = "*SYSTEM"; - options.CipherSpec = "TLS_RSA_WITH_AES_256_CBC_SHA256"; -}); -``` +snippet: ibmmq-ssl-tls ### Key repository options @@ -104,15 +66,7 @@ var transport = new IbmMqTransport(options => Verify the queue manager's certificate distinguished name for additional security: -```csharp -var transport = new IbmMqTransport(options => -{ - // ... connection settings ... - options.SslKeyRepository = "*SYSTEM"; - options.CipherSpec = "TLS_RSA_WITH_AES_256_CBC_SHA256"; - options.SslPeerName = "CN=MQSERVER01,O=MyCompany,C=US"; -}); -``` +snippet: ibmmq-ssl-peer-name > [!NOTE] > Both `SslKeyRepository` and `CipherSpec` must be specified together. Setting one without the other will cause a configuration validation error. @@ -125,51 +79,21 @@ Topics are named using a configurable `TopicNaming` strategy. The default uses a To change the prefix: -```csharp -var transport = new IbmMqTransport(options => -{ - // ... connection settings ... - options.TopicNaming = new TopicNaming("PROD"); -}); -``` +snippet: ibmmq-custom-topic-prefix ### Custom topic naming strategy IBM MQ topic names are limited to 48 characters. If event type names are long, subclass `TopicNaming` to implement a shortening strategy: -```csharp -public class ShortTopicNaming() : TopicNaming("APP") -{ - public override string GenerateTopicName(Type eventType) - { - return $"APP.{eventType.Name}".ToUpperInvariant(); - } -} -``` - -```csharp -var transport = new IbmMqTransport(options => -{ - // ... connection settings ... - options.TopicNaming = new ShortTopicNaming(); -}); -``` +snippet: ibmmq-custom-topic-naming-class + +snippet: ibmmq-custom-topic-naming-usage ## Resource name sanitization IBM MQ queue and topic names are limited to 48 characters and allow only `A-Z`, `a-z`, `0-9`, `.`, and `_`. If endpoint names contain invalid characters or are too long, configure a sanitizer: -```csharp -var transport = new IbmMqTransport(options => -{ - // ... connection settings ... - options.ResourceNameSanitizer = name => - { - var sanitized = name.Replace("-", ".").Replace("/", "."); - return sanitized.Length > 48 ? sanitized[..48] : sanitized; - }; -}); -``` +snippet: ibmmq-resource-sanitization > [!WARNING] > Ensure the sanitizer produces deterministic and unique names. Two different input names mapping to the same sanitized name will cause messages to be delivered to the wrong endpoint. @@ -180,13 +104,7 @@ var transport = new IbmMqTransport(options => The wait interval controls how long each poll waits for a message before returning: -```csharp -var transport = new IbmMqTransport(options => -{ - // ... connection settings ... - options.MessageWaitInterval = TimeSpan.FromMilliseconds(2000); -}); -``` +snippet: ibmmq-polling-interval |Setting|Default|Range| |:---|---|---| @@ -196,13 +114,7 @@ var transport = new IbmMqTransport(options => Should match or be less than the queue manager's `MAXMSGL` setting: -```csharp -var transport = new IbmMqTransport(options => -{ - // ... connection settings ... - options.MaxMessageLength = 10 * 1024 * 1024; // 10 MB -}); -``` +snippet: ibmmq-max-message-size |Setting|Default|Range| |:---|---|---| @@ -212,13 +124,7 @@ var transport = new IbmMqTransport(options => The Coded Character Set Identifier (CCSID) used for message text encoding. The default is UTF-8 (1208), which is recommended for most scenarios. -```csharp -var transport = new IbmMqTransport(options => -{ - // ... connection settings ... - options.CharacterSet = 1208; // UTF-8 (default) -}); -``` +snippet: ibmmq-character-set ## Message persistence diff --git a/transports/ibmmq/index.md b/transports/ibmmq/index.md index ca19db52dc8..89685f0048a 100644 --- a/transports/ibmmq/index.md +++ b/transports/ibmmq/index.md @@ -36,21 +36,7 @@ The transport requires IBM MQ 9.0 or later. It has been tested with: To use IBM MQ as the underlying transport: -```csharp -var endpointConfiguration = new EndpointConfiguration("MyEndpoint"); - -var transport = new IbmMqTransport(options => -{ - options.Host = "mq-server.example.com"; - options.Port = 1414; - options.Channel = "DEV.APP.SVRCONN"; - options.QueueManagerName = "QM1"; - options.User = "app"; - options.Password = "passw0rd"; -}); - -endpointConfiguration.UseTransport(transport); -``` +snippet: ibmmq-config-basic See [connection settings](connection-settings.md) for all available connection and configuration options. diff --git a/transports/ibmmq/transactions.md b/transports/ibmmq/transactions.md index 27403bb3478..346c6877157 100644 --- a/transports/ibmmq/transactions.md +++ b/transports/ibmmq/transactions.md @@ -20,13 +20,7 @@ The IBM MQ transport supports the following [transport transaction modes](/trans In `SendsAtomicWithReceive` mode, the message receive and all outgoing send/publish operations are committed or rolled back as a single unit of work. -```csharp -var transport = new IbmMqTransport(options => -{ - // ... connection settings ... -}); -transport.TransportTransactionMode = TransportTransactionMode.SendsAtomicWithReceive; -``` +snippet: ibmmq-sends-atomic-with-receive > [!NOTE] > Messages sent outside of a handler (e.g., via `IMessageSession`) are not included in the atomic operation. @@ -37,13 +31,7 @@ In `ReceiveOnly` mode, the message receive is transactional. Successfully proces This is the default transaction mode. -```csharp -var transport = new IbmMqTransport(options => -{ - // ... connection settings ... -}); -transport.TransportTransactionMode = TransportTransactionMode.ReceiveOnly; -``` +snippet: ibmmq-receive-only > [!WARNING] > If the connection to the queue manager is lost after processing succeeds but before the commit, the message will be redelivered. This can result in duplicate processing. Use the [Outbox](/nservicebus/outbox/) feature to guarantee exactly-once processing. @@ -52,13 +40,7 @@ transport.TransportTransactionMode = TransportTransactionMode.ReceiveOnly; In `None` mode, messages are consumed without any transactional guarantees. If processing fails, the message is lost. -```csharp -var transport = new IbmMqTransport(options => -{ - // ... connection settings ... -}); -transport.TransportTransactionMode = TransportTransactionMode.None; -``` +snippet: ibmmq-transactions-none > [!CAUTION] > This mode should only be used when message loss is acceptable, such as for non-critical telemetry or logging messages. From d1ffa9622c890440c3973a7e53b1d21a9b8a89fd Mon Sep 17 00:00:00 2001 From: Ramon Smits Date: Fri, 6 Mar 2026 10:45:21 +0100 Subject: [PATCH 05/19] Package and namespace rename --- Snippets/IbmMq/IbmMq_1/IbmMq_1.csproj | 2 +- Snippets/IbmMq/IbmMq_1/ShortTopicNaming.cs | 2 +- Snippets/IbmMq/IbmMq_1/Usage.cs | 2 +- components/components.yaml | 6 +++--- components/nugetAlias.txt | 2 +- transports/ibmmq/operations-scripting.md | 6 +++--- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Snippets/IbmMq/IbmMq_1/IbmMq_1.csproj b/Snippets/IbmMq/IbmMq_1/IbmMq_1.csproj index b382211419a..496ae5b497f 100644 --- a/Snippets/IbmMq/IbmMq_1/IbmMq_1.csproj +++ b/Snippets/IbmMq/IbmMq_1/IbmMq_1.csproj @@ -5,7 +5,7 @@ - + diff --git a/Snippets/IbmMq/IbmMq_1/ShortTopicNaming.cs b/Snippets/IbmMq/IbmMq_1/ShortTopicNaming.cs index d5ff64e8ea2..a6a0d9cac94 100644 --- a/Snippets/IbmMq/IbmMq_1/ShortTopicNaming.cs +++ b/Snippets/IbmMq/IbmMq_1/ShortTopicNaming.cs @@ -1,5 +1,5 @@ using System; -using NServiceBus.Transport.IbmMq; +using NServiceBus.Transport.IBMQMQ; #region ibmmq-custom-topic-naming-class diff --git a/Snippets/IbmMq/IbmMq_1/Usage.cs b/Snippets/IbmMq/IbmMq_1/Usage.cs index 94f1e9dbed4..813713080ff 100644 --- a/Snippets/IbmMq/IbmMq_1/Usage.cs +++ b/Snippets/IbmMq/IbmMq_1/Usage.cs @@ -1,6 +1,6 @@ using System; using NServiceBus; -using NServiceBus.Transport.IbmMq; +using NServiceBus.Transport.IBMMQ; class Usage { diff --git a/components/components.yaml b/components/components.yaml index fe7189da6a4..3ff82b3e8b3 100644 --- a/components/components.yaml +++ b/components/components.yaml @@ -53,14 +53,14 @@ NugetOrder: - NServiceBus.AmazonSQS -- Key: IbmMq +- Key: IBMMQ Name: IBM MQ Transport DocsUrl: /transports/ibmmq GitHubOwner: ParticularLabs Category: Transport - ProjectUrl: https://github.com/ParticularLabs/NServiceBus.IBMMQ + ProjectUrl: https://github.com/Particular/NServiceBus.Transport.IBMMQ NugetOrder: - - NServiceBus.Transport.IbmMq + - NServiceBus.Transport.IBMMQ - Key: ASB Name: Azure Service Bus Transport (Legacy) diff --git a/components/nugetAlias.txt b/components/nugetAlias.txt index c9478c139c2..91382948d89 100644 --- a/components/nugetAlias.txt +++ b/components/nugetAlias.txt @@ -30,7 +30,7 @@ Heartbeats4: ServiceControl.Plugin.Nsb4.Heartbeat Heartbeats5: ServiceControl.Plugin.Nsb5.Heartbeat Heartbeats6: ServiceControl.Plugin.Nsb6.Heartbeat Heartbeats: NServiceBus.Heartbeat -IbmMq: NServiceBus.Transport.IbmMq +IBMMQ: NServiceBus.Transport.IBMMQ Host: NServiceBus.Host Log4Net: NServiceBus.Log4Net MessageInterfaces: NServiceBus.MessageInterfaces diff --git a/transports/ibmmq/operations-scripting.md b/transports/ibmmq/operations-scripting.md index ecf7d3ae43d..0a44ab37e09 100644 --- a/transports/ibmmq/operations-scripting.md +++ b/transports/ibmmq/operations-scripting.md @@ -2,7 +2,7 @@ title: IBM MQ Transport Scripting summary: Command-line tool and scripts for managing IBM MQ transport infrastructure reviewed: 2026-02-19 -component: IbmMq +component: IBMMQ related: - nservicebus/operations --- @@ -11,14 +11,14 @@ The IBM MQ transport includes a command-line tool for creating and managing tran ## Command-line tool -The `NServiceBus.Transport.IbmMq.CommandLine` package provides the `ibmmq-transport` CLI tool for managing IBM MQ resources. +The `NServiceBus.Transport.IBMMQ.CommandLine` package provides the `ibmmq-transport` CLI tool for managing IBM MQ resources. ### Installation Install the tool globally: ```bash -dotnet tool install -g NServiceBus.Transport.IbmMq.CommandLine +dotnet tool install -g NServiceBus.Transport.IBMMQ.CommandLine ``` ### Connection options From 7a60ddd5639fe5de83fe60c7bdd51757ed206ffb Mon Sep 17 00:00:00 2001 From: Ramon Smits Date: Fri, 6 Mar 2026 11:10:26 +0100 Subject: [PATCH 06/19] Rename IBM MQ transport samples and solution and types --- Snippets/IbmMq/IbmMq.sln | 34 --- Snippets/IbmMq/IbmMq_1/IbmMq_1.csproj | 11 - Snippets/IbmMq/IbmMq_1/ShortTopicNaming.cs | 14 -- Snippets/IbmMq/IbmMq_1/Usage.cs | 236 --------------------- 4 files changed, 295 deletions(-) delete mode 100644 Snippets/IbmMq/IbmMq.sln delete mode 100644 Snippets/IbmMq/IbmMq_1/IbmMq_1.csproj delete mode 100644 Snippets/IbmMq/IbmMq_1/ShortTopicNaming.cs delete mode 100644 Snippets/IbmMq/IbmMq_1/Usage.cs diff --git a/Snippets/IbmMq/IbmMq.sln b/Snippets/IbmMq/IbmMq.sln deleted file mode 100644 index de3e4d52d10..00000000000 --- a/Snippets/IbmMq/IbmMq.sln +++ /dev/null @@ -1,34 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.0.31903.59 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IbmMq_1", "IbmMq_1\IbmMq_1.csproj", "{E74E64BC-8D4F-4528-A524-F56AC7EDB222}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Debug|x64 = Debug|x64 - Debug|x86 = Debug|x86 - Release|Any CPU = Release|Any CPU - Release|x64 = Release|x64 - Release|x86 = Release|x86 - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {E74E64BC-8D4F-4528-A524-F56AC7EDB222}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E74E64BC-8D4F-4528-A524-F56AC7EDB222}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E74E64BC-8D4F-4528-A524-F56AC7EDB222}.Debug|x64.ActiveCfg = Debug|Any CPU - {E74E64BC-8D4F-4528-A524-F56AC7EDB222}.Debug|x64.Build.0 = Debug|Any CPU - {E74E64BC-8D4F-4528-A524-F56AC7EDB222}.Debug|x86.ActiveCfg = Debug|Any CPU - {E74E64BC-8D4F-4528-A524-F56AC7EDB222}.Debug|x86.Build.0 = Debug|Any CPU - {E74E64BC-8D4F-4528-A524-F56AC7EDB222}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E74E64BC-8D4F-4528-A524-F56AC7EDB222}.Release|Any CPU.Build.0 = Release|Any CPU - {E74E64BC-8D4F-4528-A524-F56AC7EDB222}.Release|x64.ActiveCfg = Release|Any CPU - {E74E64BC-8D4F-4528-A524-F56AC7EDB222}.Release|x64.Build.0 = Release|Any CPU - {E74E64BC-8D4F-4528-A524-F56AC7EDB222}.Release|x86.ActiveCfg = Release|Any CPU - {E74E64BC-8D4F-4528-A524-F56AC7EDB222}.Release|x86.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection -EndGlobal diff --git a/Snippets/IbmMq/IbmMq_1/IbmMq_1.csproj b/Snippets/IbmMq/IbmMq_1/IbmMq_1.csproj deleted file mode 100644 index 496ae5b497f..00000000000 --- a/Snippets/IbmMq/IbmMq_1/IbmMq_1.csproj +++ /dev/null @@ -1,11 +0,0 @@ - - - - net10.0 - - - - - - - diff --git a/Snippets/IbmMq/IbmMq_1/ShortTopicNaming.cs b/Snippets/IbmMq/IbmMq_1/ShortTopicNaming.cs deleted file mode 100644 index a6a0d9cac94..00000000000 --- a/Snippets/IbmMq/IbmMq_1/ShortTopicNaming.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using NServiceBus.Transport.IBMQMQ; - -#region ibmmq-custom-topic-naming-class - -public class ShortTopicNaming() : TopicNaming("APP") -{ - public override string GenerateTopicName(Type eventType) - { - return $"APP.{eventType.Name}".ToUpperInvariant(); - } -} - -#endregion diff --git a/Snippets/IbmMq/IbmMq_1/Usage.cs b/Snippets/IbmMq/IbmMq_1/Usage.cs deleted file mode 100644 index 813713080ff..00000000000 --- a/Snippets/IbmMq/IbmMq_1/Usage.cs +++ /dev/null @@ -1,236 +0,0 @@ -using System; -using NServiceBus; -using NServiceBus.Transport.IBMMQ; - -class Usage -{ - public void BasicConfig() - { - #region ibmmq-config-basic - - var endpointConfiguration = new EndpointConfiguration("MyEndpoint"); - - var transport = new IbmMqTransport(options => - { - options.Host = "mq-server.example.com"; - options.Port = 1414; - options.Channel = "DEV.APP.SVRCONN"; - options.QueueManagerName = "QM1"; - options.User = "app"; - options.Password = "passw0rd"; - }); - - endpointConfiguration.UseTransport(transport); - - #endregion - } - - public void BasicConnection() - { - #region ibmmq-basic-connection - - var transport = new IbmMqTransport(options => - { - options.Host = "mq-server.example.com"; - options.Port = 1414; - options.Channel = "DEV.APP.SVRCONN"; - options.QueueManagerName = "QM1"; - }); - - #endregion - } - - public void Authentication() - { - #region ibmmq-authentication - - var transport = new IbmMqTransport(options => - { - options.Host = "mq-server.example.com"; - options.QueueManagerName = "QM1"; - options.User = "app"; - options.Password = "passw0rd"; - }); - - #endregion - } - - public void ApplicationName() - { - #region ibmmq-application-name - - var transport = new IbmMqTransport(options => - { - // ... connection settings ... - options.ApplicationName = "OrderService"; - }); - - #endregion - } - - public void HighAvailability() - { - #region ibmmq-high-availability - - var transport = new IbmMqTransport(options => - { - options.QueueManagerName = "QM1"; - options.Channel = "APP.SVRCONN"; - options.Connections.Add("mqhost1(1414)"); - options.Connections.Add("mqhost2(1414)"); - }); - - #endregion - } - - public void SslTls() - { - #region ibmmq-ssl-tls - - var transport = new IbmMqTransport(options => - { - options.Host = "mq-server.example.com"; - options.QueueManagerName = "QM1"; - options.SslKeyRepository = "*SYSTEM"; - options.CipherSpec = "TLS_RSA_WITH_AES_256_CBC_SHA256"; - }); - - #endregion - } - - public void SslPeerName() - { - #region ibmmq-ssl-peer-name - - var transport = new IbmMqTransport(options => - { - // ... connection settings ... - options.SslKeyRepository = "*SYSTEM"; - options.CipherSpec = "TLS_RSA_WITH_AES_256_CBC_SHA256"; - options.SslPeerName = "CN=MQSERVER01,O=MyCompany,C=US"; - }); - - #endregion - } - - public void CustomTopicPrefix() - { - #region ibmmq-custom-topic-prefix - - var transport = new IbmMqTransport(options => - { - // ... connection settings ... - options.TopicNaming = new TopicNaming("PROD"); - }); - - #endregion - } - - public void CustomTopicNamingUsage() - { - #region ibmmq-custom-topic-naming-usage - - var transport = new IbmMqTransport(options => - { - // ... connection settings ... - options.TopicNaming = new ShortTopicNaming(); - }); - - #endregion - } - - public void ResourceSanitization() - { - #region ibmmq-resource-sanitization - - var transport = new IbmMqTransport(options => - { - // ... connection settings ... - options.ResourceNameSanitizer = name => - { - var sanitized = name.Replace("-", ".").Replace("/", "."); - return sanitized.Length > 48 ? sanitized[..48] : sanitized; - }; - }); - - #endregion - } - - public void PollingInterval() - { - #region ibmmq-polling-interval - - var transport = new IbmMqTransport(options => - { - // ... connection settings ... - options.MessageWaitInterval = TimeSpan.FromMilliseconds(2000); - }); - - #endregion - } - - public void MaxMessageSize() - { - #region ibmmq-max-message-size - - var transport = new IbmMqTransport(options => - { - // ... connection settings ... - options.MaxMessageLength = 10 * 1024 * 1024; // 10 MB - }); - - #endregion - } - - public void CharacterSet() - { - #region ibmmq-character-set - - var transport = new IbmMqTransport(options => - { - // ... connection settings ... - options.CharacterSet = 1208; // UTF-8 (default) - }); - - #endregion - } - - public void SendsAtomicWithReceive() - { - #region ibmmq-sends-atomic-with-receive - - var transport = new IbmMqTransport(options => - { - // ... connection settings ... - }); - transport.TransportTransactionMode = TransportTransactionMode.SendsAtomicWithReceive; - - #endregion - } - - public void ReceiveOnly() - { - #region ibmmq-receive-only - - var transport = new IbmMqTransport(options => - { - // ... connection settings ... - }); - transport.TransportTransactionMode = TransportTransactionMode.ReceiveOnly; - - #endregion - } - - public void TransactionsNone() - { - #region ibmmq-transactions-none - - var transport = new IbmMqTransport(options => - { - // ... connection settings ... - }); - transport.TransportTransactionMode = TransportTransactionMode.None; - - #endregion - } -} From 701fa18450e94cf32116d81ab4b258573d6f4757 Mon Sep 17 00:00:00 2001 From: Ramon Smits Date: Fri, 6 Mar 2026 11:28:18 +0100 Subject: [PATCH 07/19] more renaming :-) --- Snippets/IBMMQ/IBMMQ.sln | 34 +++ Snippets/IBMMQ/IBMMQ_1/IBMMQ_1.csproj | 11 + Snippets/IBMMQ/IBMMQ_1/ShortTopicNaming.cs | 14 ++ Snippets/IBMMQ/IBMMQ_1/Usage.cs | 236 +++++++++++++++++++++ 4 files changed, 295 insertions(+) create mode 100644 Snippets/IBMMQ/IBMMQ.sln create mode 100644 Snippets/IBMMQ/IBMMQ_1/IBMMQ_1.csproj create mode 100644 Snippets/IBMMQ/IBMMQ_1/ShortTopicNaming.cs create mode 100644 Snippets/IBMMQ/IBMMQ_1/Usage.cs diff --git a/Snippets/IBMMQ/IBMMQ.sln b/Snippets/IBMMQ/IBMMQ.sln new file mode 100644 index 00000000000..191141d44be --- /dev/null +++ b/Snippets/IBMMQ/IBMMQ.sln @@ -0,0 +1,34 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IBMMQ_1", "IBMMQ_1\IBMMQ_1.csproj", "{E74E64BC-8D4F-4528-A524-F56AC7EDB222}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {E74E64BC-8D4F-4528-A524-F56AC7EDB222}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E74E64BC-8D4F-4528-A524-F56AC7EDB222}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E74E64BC-8D4F-4528-A524-F56AC7EDB222}.Debug|x64.ActiveCfg = Debug|Any CPU + {E74E64BC-8D4F-4528-A524-F56AC7EDB222}.Debug|x64.Build.0 = Debug|Any CPU + {E74E64BC-8D4F-4528-A524-F56AC7EDB222}.Debug|x86.ActiveCfg = Debug|Any CPU + {E74E64BC-8D4F-4528-A524-F56AC7EDB222}.Debug|x86.Build.0 = Debug|Any CPU + {E74E64BC-8D4F-4528-A524-F56AC7EDB222}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E74E64BC-8D4F-4528-A524-F56AC7EDB222}.Release|Any CPU.Build.0 = Release|Any CPU + {E74E64BC-8D4F-4528-A524-F56AC7EDB222}.Release|x64.ActiveCfg = Release|Any CPU + {E74E64BC-8D4F-4528-A524-F56AC7EDB222}.Release|x64.Build.0 = Release|Any CPU + {E74E64BC-8D4F-4528-A524-F56AC7EDB222}.Release|x86.ActiveCfg = Release|Any CPU + {E74E64BC-8D4F-4528-A524-F56AC7EDB222}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/Snippets/IBMMQ/IBMMQ_1/IBMMQ_1.csproj b/Snippets/IBMMQ/IBMMQ_1/IBMMQ_1.csproj new file mode 100644 index 00000000000..496ae5b497f --- /dev/null +++ b/Snippets/IBMMQ/IBMMQ_1/IBMMQ_1.csproj @@ -0,0 +1,11 @@ + + + + net10.0 + + + + + + + diff --git a/Snippets/IBMMQ/IBMMQ_1/ShortTopicNaming.cs b/Snippets/IBMMQ/IBMMQ_1/ShortTopicNaming.cs new file mode 100644 index 00000000000..7d6d318869f --- /dev/null +++ b/Snippets/IBMMQ/IBMMQ_1/ShortTopicNaming.cs @@ -0,0 +1,14 @@ +using System; +using NServiceBus.Transport.IBMMQ; + +#region ibmmq-custom-topic-naming-class + +public sealed class ShortTopicNaming() : TopicNaming("APP") +{ + public override string GenerateTopicName(Type eventType) + { + return $"APP.{eventType.Name}".ToUpperInvariant(); + } +} + +#endregion diff --git a/Snippets/IBMMQ/IBMMQ_1/Usage.cs b/Snippets/IBMMQ/IBMMQ_1/Usage.cs new file mode 100644 index 00000000000..14f1bbe1aad --- /dev/null +++ b/Snippets/IBMMQ/IBMMQ_1/Usage.cs @@ -0,0 +1,236 @@ +using System; +using NServiceBus; +using NServiceBus.Transport.IBMMQ; + +class Usage +{ + public void BasicConfig() + { + #region ibmmq-config-basic + + var endpointConfiguration = new EndpointConfiguration("MyEndpoint"); + + var transport = new IBMMQTransport(options => + { + options.Host = "mq-server.example.com"; + options.Port = 1414; + options.Channel = "DEV.APP.SVRCONN"; + options.QueueManagerName = "QM1"; + options.User = "app"; + options.Password = "passw0rd"; + }); + + endpointConfiguration.UseTransport(transport); + + #endregion + } + + public void BasicConnection() + { + #region ibmmq-basic-connection + + var transport = new IBMMQTransport(options => + { + options.Host = "mq-server.example.com"; + options.Port = 1414; + options.Channel = "DEV.APP.SVRCONN"; + options.QueueManagerName = "QM1"; + }); + + #endregion + } + + public void Authentication() + { + #region ibmmq-authentication + + var transport = new IBMMQTransport(options => + { + options.Host = "mq-server.example.com"; + options.QueueManagerName = "QM1"; + options.User = "app"; + options.Password = "passw0rd"; + }); + + #endregion + } + + public void ApplicationName() + { + #region ibmmq-application-name + + var transport = new IBMMQTransport(options => + { + // ... connection settings ... + options.ApplicationName = "OrderService"; + }); + + #endregion + } + + public void HighAvailability() + { + #region ibmmq-high-availability + + var transport = new IBMMQTransport(options => + { + options.QueueManagerName = "QM1"; + options.Channel = "APP.SVRCONN"; + options.Connections.Add("mqhost1(1414)"); + options.Connections.Add("mqhost2(1414)"); + }); + + #endregion + } + + public void SslTls() + { + #region ibmmq-ssl-tls + + var transport = new IBMMQTransport(options => + { + options.Host = "mq-server.example.com"; + options.QueueManagerName = "QM1"; + options.SslKeyRepository = "*SYSTEM"; + options.CipherSpec = "TLS_RSA_WITH_AES_256_CBC_SHA256"; + }); + + #endregion + } + + public void SslPeerName() + { + #region ibmmq-ssl-peer-name + + var transport = new IBMMQTransport(options => + { + // ... connection settings ... + options.SslKeyRepository = "*SYSTEM"; + options.CipherSpec = "TLS_RSA_WITH_AES_256_CBC_SHA256"; + options.SslPeerName = "CN=MQSERVER01,O=MyCompany,C=US"; + }); + + #endregion + } + + public void CustomTopicPrefix() + { + #region ibmmq-custom-topic-prefix + + var transport = new IBMMQTransport(options => + { + // ... connection settings ... + options.TopicNaming = new TopicNaming("PROD"); + }); + + #endregion + } + + public void CustomTopicNamingUsage() + { + #region ibmmq-custom-topic-naming-usage + + var transport = new IBMMQTransport(options => + { + // ... connection settings ... + options.TopicNaming = new ShortTopicNaming(); + }); + + #endregion + } + + public void ResourceSanitization() + { + #region ibmmq-resource-sanitization + + var transport = new IBMMQTransport(options => + { + // ... connection settings ... + options.ResourceNameSanitizer = name => + { + var sanitized = name.Replace("-", ".").Replace("/", "."); + return sanitized.Length > 48 ? sanitized[..48] : sanitized; + }; + }); + + #endregion + } + + public void PollingInterval() + { + #region ibmmq-polling-interval + + var transport = new IBMMQTransport(options => + { + // ... connection settings ... + options.MessageWaitInterval = TimeSpan.FromMilliseconds(2000); + }); + + #endregion + } + + public void MaxMessageSize() + { + #region ibmmq-max-message-size + + var transport = new IBMMQTransport(options => + { + // ... connection settings ... + options.MaxMessageLength = 10 * 1024 * 1024; // 10 MB + }); + + #endregion + } + + public void CharacterSet() + { + #region ibmmq-character-set + + var transport = new IBMMQTransport(options => + { + // ... connection settings ... + options.CharacterSet = 1208; // UTF-8 (default) + }); + + #endregion + } + + public void SendsAtomicWithReceive() + { + #region ibmmq-sends-atomic-with-receive + + var transport = new IBMMQTransport(options => + { + // ... connection settings ... + }); + transport.TransportTransactionMode = TransportTransactionMode.SendsAtomicWithReceive; + + #endregion + } + + public void ReceiveOnly() + { + #region ibmmq-receive-only + + var transport = new IBMMQTransport(options => + { + // ... connection settings ... + }); + transport.TransportTransactionMode = TransportTransactionMode.ReceiveOnly; + + #endregion + } + + public void TransactionsNone() + { + #region ibmmq-transactions-none + + var transport = new IBMMQTransport(options => + { + // ... connection settings ... + }); + transport.TransportTransactionMode = TransportTransactionMode.None; + + #endregion + } +} From 57fc07700153892f60284ca2621d87dcde57ff99 Mon Sep 17 00:00:00 2001 From: Ramon Smits Date: Fri, 6 Mar 2026 11:42:33 +0100 Subject: [PATCH 08/19] case sensitivity is annoying on the filesystem :-) --- ...iceBus.Transport.IbmMq.txt => NServiceBus.Transport.IBMMQ.txt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename components/core-dependencies/{NServiceBus.Transport.IbmMq.txt => NServiceBus.Transport.IBMMQ.txt} (100%) diff --git a/components/core-dependencies/NServiceBus.Transport.IbmMq.txt b/components/core-dependencies/NServiceBus.Transport.IBMMQ.txt similarity index 100% rename from components/core-dependencies/NServiceBus.Transport.IbmMq.txt rename to components/core-dependencies/NServiceBus.Transport.IBMMQ.txt From 52d25d2fb38b876a6b58489b8ae95e0c5fb3abdf Mon Sep 17 00:00:00 2001 From: Ramon Smits Date: Fri, 6 Mar 2026 11:45:17 +0100 Subject: [PATCH 09/19] pre release must have prerelease.txt marker file --- Snippets/IBMMQ/IBMMQ_1/prerelease.txt | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 Snippets/IBMMQ/IBMMQ_1/prerelease.txt diff --git a/Snippets/IBMMQ/IBMMQ_1/prerelease.txt b/Snippets/IBMMQ/IBMMQ_1/prerelease.txt new file mode 100644 index 00000000000..e69de29bb2d From 09e98b339b1e6fc7d2eb476cdbb6baf3079115db Mon Sep 17 00:00:00 2001 From: Ramon Smits Date: Fri, 6 Mar 2026 11:54:33 +0100 Subject: [PATCH 10/19] =?UTF-8?q?=F0=9F=90=9B=20Fix=20IBM=20MQ=20component?= =?UTF-8?q?=20key,=20GitHubOwner,=20and=20transaction=20matrix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - component frontmatter used `IbmMq` but components.yaml key is `IBMMQ` - GitHubOwner was `ParticularLabs` but repo lives under `Particular` - IBM MQ was missing from the transaction support matrix --- components/components.yaml | 2 +- transports/ibmmq/connection-settings.md | 2 +- transports/ibmmq/index.md | 2 +- transports/ibmmq/native-integration.md | 2 +- transports/ibmmq/topology.md | 2 +- transports/ibmmq/transactions.md | 2 +- transports/transactions_matrix_core_[8,).partial.md | 1 + 7 files changed, 7 insertions(+), 6 deletions(-) diff --git a/components/components.yaml b/components/components.yaml index 3ff82b3e8b3..db4d0b66ff8 100644 --- a/components/components.yaml +++ b/components/components.yaml @@ -56,7 +56,7 @@ - Key: IBMMQ Name: IBM MQ Transport DocsUrl: /transports/ibmmq - GitHubOwner: ParticularLabs + GitHubOwner: Particular Category: Transport ProjectUrl: https://github.com/Particular/NServiceBus.Transport.IBMMQ NugetOrder: diff --git a/transports/ibmmq/connection-settings.md b/transports/ibmmq/connection-settings.md index f09127d977d..b978cffda33 100644 --- a/transports/ibmmq/connection-settings.md +++ b/transports/ibmmq/connection-settings.md @@ -2,7 +2,7 @@ title: Connection Settings summary: Connection settings for the IBM MQ transport, including SSL/TLS, high availability, and advanced configuration reviewed: 2026-02-19 -component: IbmMq +component: IBMMQ --- ## Basic connection diff --git a/transports/ibmmq/index.md b/transports/ibmmq/index.md index 89685f0048a..f4cdb8bf3bc 100644 --- a/transports/ibmmq/index.md +++ b/transports/ibmmq/index.md @@ -2,7 +2,7 @@ title: IBM MQ Transport summary: Integrate NServiceBus with IBM MQ for enterprise messaging on mainframe and distributed platforms reviewed: 2026-02-19 -component: IbmMq +component: IBMMQ related: redirects: --- diff --git a/transports/ibmmq/native-integration.md b/transports/ibmmq/native-integration.md index dfd193f7d10..968c7a7b72d 100644 --- a/transports/ibmmq/native-integration.md +++ b/transports/ibmmq/native-integration.md @@ -2,7 +2,7 @@ title: Native integration summary: How to integrate NServiceBus endpoints with native IBM MQ applications reviewed: 2026-02-19 -component: IbmMq +component: IBMMQ --- The IBM MQ transport uses standard IBM MQ message structures, making it possible to exchange messages between NServiceBus endpoints and native IBM MQ applications. diff --git a/transports/ibmmq/topology.md b/transports/ibmmq/topology.md index dab9cf00f89..10be55f02e9 100644 --- a/transports/ibmmq/topology.md +++ b/transports/ibmmq/topology.md @@ -2,7 +2,7 @@ title: Publish/subscribe topology summary: How the IBM MQ transport implements publish/subscribe messaging using topics and durable subscriptions reviewed: 2026-02-19 -component: IbmMq +component: IBMMQ --- The IBM MQ transport implements publish/subscribe messaging using IBM MQ's native topic and subscription infrastructure. This means event subscriptions do not require NServiceBus persistence. diff --git a/transports/ibmmq/transactions.md b/transports/ibmmq/transactions.md index 346c6877157..347441f4dc2 100644 --- a/transports/ibmmq/transactions.md +++ b/transports/ibmmq/transactions.md @@ -2,7 +2,7 @@ title: Transaction support summary: Transaction modes supported by the IBM MQ transport reviewed: 2026-02-19 -component: IbmMq +component: IBMMQ --- The IBM MQ transport supports the following [transport transaction modes](/transports/transactions.md): diff --git a/transports/transactions_matrix_core_[8,).partial.md b/transports/transactions_matrix_core_[8,).partial.md index 21efd092522..fb0a9e25871 100644 --- a/transports/transactions_matrix_core_[8,).partial.md +++ b/transports/transactions_matrix_core_[8,).partial.md @@ -7,4 +7,5 @@ | [Amazon SQS](/transports/sqs/transaction-support.md) | ✖ | ✖ | ✔ | ✔ | | [Azure Storage Queues](/transports/azure-storage-queues/transaction-support.md)| ✖ | ✖ | ✔ | ✔ | | [Azure Service Bus](/transports/azure-service-bus/transaction-support.md) | ✖ | ✔ | ✔ | ✔ | +| [IBM MQ](/transports/ibmmq/transactions.md) | ✖ | ✔ | ✔ | ✔ | | [PostgreSQL](/transports/postgresql/transactions.md) | ✖ | ✔ | ✔ | ✔ | From a92f543bbe10693d10631f9064c06592ea8b2ef9 Mon Sep 17 00:00:00 2001 From: Ramon Smits Date: Fri, 6 Mar 2026 11:59:29 +0100 Subject: [PATCH 11/19] =?UTF-8?q?=F0=9F=90=9B=20Pin=20IBMMQ=20snippet=20to?= =?UTF-8?q?=20specific=20prerelease=20version?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wildcard version (1.0.0-*) is not allowed for prerelease components by the integrity tests. Pin to 1.0.0-alpha.1. --- Snippets/IBMMQ/IBMMQ_1/IBMMQ_1.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Snippets/IBMMQ/IBMMQ_1/IBMMQ_1.csproj b/Snippets/IBMMQ/IBMMQ_1/IBMMQ_1.csproj index 496ae5b497f..f514d7d765d 100644 --- a/Snippets/IBMMQ/IBMMQ_1/IBMMQ_1.csproj +++ b/Snippets/IBMMQ/IBMMQ_1/IBMMQ_1.csproj @@ -5,7 +5,7 @@ - + From 436778cd3f10aeb2d12ca5807a3bdddcac76b424 Mon Sep 17 00:00:00 2001 From: Irina Dominte Date: Tue, 10 Mar 2026 14:09:06 +0200 Subject: [PATCH 12/19] cleaned up the docs --- transports/ibmmq/connection-settings.md | 8 +++--- transports/ibmmq/index.md | 34 +++++-------------------- 2 files changed, 9 insertions(+), 33 deletions(-) diff --git a/transports/ibmmq/connection-settings.md b/transports/ibmmq/connection-settings.md index b978cffda33..0c802c4f1ef 100644 --- a/transports/ibmmq/connection-settings.md +++ b/transports/ibmmq/connection-settings.md @@ -18,7 +18,7 @@ snippet: ibmmq-basic-connection |Host|`localhost`| |Port|`1414`| |Channel|`DEV.ADMIN.SVRCONN`| -|QueueManagerName|Empty (local default queue manager)| +|QueueManagerName|Empty (local default queue manager named QM1)| ## Authentication @@ -26,9 +26,6 @@ User credentials can be provided to authenticate with the queue manager: snippet: ibmmq-authentication -> [!NOTE] -> When no credentials are provided, the connection uses the operating system identity. This may be appropriate for local development but typically requires explicit credentials in production. - ## Application name The application name appears in IBM MQ monitoring tools and is useful for identifying connections: @@ -91,7 +88,8 @@ snippet: ibmmq-custom-topic-naming-usage ## Resource name sanitization -IBM MQ queue and topic names are limited to 48 characters and allow only `A-Z`, `a-z`, `0-9`, `.`, and `_`. If endpoint names contain invalid characters or are too long, configure a sanitizer: +IBM MQ queue and topic names are limited to 48 characters and allow only `A-Z`, `a-z`, `0-9`, `.`, and `_`. +If endpoint names contain invalid characters or are too long, you need to configure a sanitizer: snippet: ibmmq-resource-sanitization diff --git a/transports/ibmmq/index.md b/transports/ibmmq/index.md index f4cdb8bf3bc..84788f012dd 100644 --- a/transports/ibmmq/index.md +++ b/transports/ibmmq/index.md @@ -11,12 +11,7 @@ Provides support for sending messages over [IBM MQ](https://www.ibm.com/products ## Broker compatibility -The transport requires IBM MQ 9.0 or later. It has been tested with: - -- IBM MQ on Linux and Windows -- IBM MQ on z/OS -- IBM MQ in containers (using the `icr.io/ibm-messaging/mq` image) -- IBM MQ as a Service on IBM Cloud +The transport requires IBM MQ 9.0 or later. ## Transport at a glance @@ -31,6 +26,10 @@ The transport requires IBM MQ 9.0 or later. It has been tested with: |Installers |Optional |Native integration |[Supported](native-integration.md) |Case Sensitive |Yes +|`TransactionScope` mode (distributed transactions) |No +|SSL/TLS encryption and certificate-based authentication |Yes +|Queue and topic names |Limited to 48 characters +|Delayed delivery |No. Requires an external timeout storage mechanism. ## Configuring the endpoint @@ -38,25 +37,4 @@ To use IBM MQ as the underlying transport: snippet: ibmmq-config-basic -See [connection settings](connection-settings.md) for all available connection and configuration options. - -## Advantages and disadvantages - -### Advantages - -- Enterprise-grade messaging platform with decades of proven reliability in mission-critical systems. -- Native publish-subscribe mechanism; does not require NServiceBus persistence for storing event subscriptions. -- Supports [atomic sends with receive](/transports/transactions.md), ensuring send and receive operations commit or roll back together. -- Integrates with mainframe and legacy systems that already use IBM MQ, bridging .NET applications with z/OS, IBM i, and other platforms. -- Built-in high availability via multi-instance queue managers and connection name lists. -- Supports SSL/TLS encryption and certificate-based authentication. -- Supports the [competing consumer](https://www.enterpriseintegrationpatterns.com/patterns/messaging/CompetingConsumers.html) pattern out of the box for horizontal scaling. - -### Disadvantages - -- Requires an IBM MQ license; IBM MQ is a commercial product. -- Queue and topic names are limited to 48 characters, which can require [custom name sanitization](connection-settings.md#resource-name-sanitization). -- Does not support native delayed delivery; requires an external timeout storage mechanism. -- Does not support `TransactionScope` mode (distributed transactions). -- Fewer .NET community resources compared to RabbitMQ or cloud-native alternatives. -- Queue manager administration requires specialized IBM MQ knowledge. +See [connection settings](connection-settings.md) for all available connection and configuration options. \ No newline at end of file From 8bc756652b694a7fe4a47bd977438b279f6bcc24 Mon Sep 17 00:00:00 2001 From: Irina Dominte Date: Tue, 10 Mar 2026 15:28:40 +0200 Subject: [PATCH 13/19] Added info about console --- transports/ibmmq/native-integration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/transports/ibmmq/native-integration.md b/transports/ibmmq/native-integration.md index 968c7a7b72d..c198afd8e68 100644 --- a/transports/ibmmq/native-integration.md +++ b/transports/ibmmq/native-integration.md @@ -32,7 +32,7 @@ All NServiceBus headers are stored as MQRFH2 message properties. Property names - Other special characters are encoded as `_xHHHH` (e.g., `.` becomes `_x002E`) > [!NOTE] -> IBM MQ silently discards string properties with empty values. The transport includes manifest properties (`nsbhdrs` and `nsbempty`) to track all header names and preserve empty values. +> IBM MQ silently discards string properties with empty values. The transport includes manifest properties (`nsbhdrs` and `nsbempty`) to track all header names and preserve empty values. The header fields with non-compliant names will now be displayed in the IBMMQ Console. ## Sending messages from native applications From 90e454a0b75d7f9fe18fc50ffa4c557ead50c8c5 Mon Sep 17 00:00:00 2001 From: Irina Dominte Date: Fri, 20 Mar 2026 11:23:28 +0200 Subject: [PATCH 14/19] added ebcdic sample --- .../ebcdic/IBMMQ_1/Contracts/Contracts.csproj | 13 +++ .../ebcdic/IBMMQ_1/Contracts/OrderAccepted.cs | 10 +++ .../ebcdic/IBMMQ_1/Contracts/OrderPlaced.cs | 10 +++ .../ebcdic/IBMMQ_1/Contracts/PlaceOrder.cs | 10 +++ .../ibmmq/ebcdic/IBMMQ_1/EbcdicSample.slnx | 5 ++ .../IBMMQ_1/LegacySender/LegacySender.csproj | 14 ++++ .../ebcdic/IBMMQ_1/LegacySender/Program.cs | 66 ++++++++++++++++ samples/ibmmq/ebcdic/IBMMQ_1/Sales/Dockerfile | 11 +++ .../Sales/Ebcdic/EbcdicEnvelopeFeature.cs | 14 ++++ .../Sales/Ebcdic/EbcdicEnvelopeHandler.cs | 59 ++++++++++++++ .../ebcdic/IBMMQ_1/Sales/PlaceOrderHandler.cs | 16 ++++ samples/ibmmq/ebcdic/IBMMQ_1/Sales/Program.cs | 35 ++++++++ .../ibmmq/ebcdic/IBMMQ_1/Sales/Sales.csproj | 20 +++++ .../ebcdic/IBMMQ_1/Sales/appsettings.json | 8 ++ samples/ibmmq/ebcdic/IBMMQ_1/prerelease.txt | 0 samples/ibmmq/ebcdic/sample.md | 79 +++++++++++++++++++ samples/ibmmq/index.md | 8 ++ 17 files changed, 378 insertions(+) create mode 100644 samples/ibmmq/ebcdic/IBMMQ_1/Contracts/Contracts.csproj create mode 100644 samples/ibmmq/ebcdic/IBMMQ_1/Contracts/OrderAccepted.cs create mode 100644 samples/ibmmq/ebcdic/IBMMQ_1/Contracts/OrderPlaced.cs create mode 100644 samples/ibmmq/ebcdic/IBMMQ_1/Contracts/PlaceOrder.cs create mode 100644 samples/ibmmq/ebcdic/IBMMQ_1/EbcdicSample.slnx create mode 100644 samples/ibmmq/ebcdic/IBMMQ_1/LegacySender/LegacySender.csproj create mode 100644 samples/ibmmq/ebcdic/IBMMQ_1/LegacySender/Program.cs create mode 100644 samples/ibmmq/ebcdic/IBMMQ_1/Sales/Dockerfile create mode 100644 samples/ibmmq/ebcdic/IBMMQ_1/Sales/Ebcdic/EbcdicEnvelopeFeature.cs create mode 100644 samples/ibmmq/ebcdic/IBMMQ_1/Sales/Ebcdic/EbcdicEnvelopeHandler.cs create mode 100644 samples/ibmmq/ebcdic/IBMMQ_1/Sales/PlaceOrderHandler.cs create mode 100644 samples/ibmmq/ebcdic/IBMMQ_1/Sales/Program.cs create mode 100644 samples/ibmmq/ebcdic/IBMMQ_1/Sales/Sales.csproj create mode 100644 samples/ibmmq/ebcdic/IBMMQ_1/Sales/appsettings.json create mode 100644 samples/ibmmq/ebcdic/IBMMQ_1/prerelease.txt create mode 100644 samples/ibmmq/ebcdic/sample.md create mode 100644 samples/ibmmq/index.md diff --git a/samples/ibmmq/ebcdic/IBMMQ_1/Contracts/Contracts.csproj b/samples/ibmmq/ebcdic/IBMMQ_1/Contracts/Contracts.csproj new file mode 100644 index 00000000000..b124c49428b --- /dev/null +++ b/samples/ibmmq/ebcdic/IBMMQ_1/Contracts/Contracts.csproj @@ -0,0 +1,13 @@ + + + + net10.0 + enable + enable + + + + + + + diff --git a/samples/ibmmq/ebcdic/IBMMQ_1/Contracts/OrderAccepted.cs b/samples/ibmmq/ebcdic/IBMMQ_1/Contracts/OrderAccepted.cs new file mode 100644 index 00000000000..5b5ce473d7e --- /dev/null +++ b/samples/ibmmq/ebcdic/IBMMQ_1/Contracts/OrderAccepted.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Contracts +{ + public record OrderAccepted(Guid orderId) : IEvent + { + } +} diff --git a/samples/ibmmq/ebcdic/IBMMQ_1/Contracts/OrderPlaced.cs b/samples/ibmmq/ebcdic/IBMMQ_1/Contracts/OrderPlaced.cs new file mode 100644 index 00000000000..4c211f55946 --- /dev/null +++ b/samples/ibmmq/ebcdic/IBMMQ_1/Contracts/OrderPlaced.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Contracts +{ + public record OrderPlaced(Guid OrderId, string Product, int Quantity) : IEvent + { + } +} diff --git a/samples/ibmmq/ebcdic/IBMMQ_1/Contracts/PlaceOrder.cs b/samples/ibmmq/ebcdic/IBMMQ_1/Contracts/PlaceOrder.cs new file mode 100644 index 00000000000..d509d0978bd --- /dev/null +++ b/samples/ibmmq/ebcdic/IBMMQ_1/Contracts/PlaceOrder.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Contracts +{ + public record PlaceOrder(Guid OrderId, string Product, int Quantity) : ICommand + { + } +} diff --git a/samples/ibmmq/ebcdic/IBMMQ_1/EbcdicSample.slnx b/samples/ibmmq/ebcdic/IBMMQ_1/EbcdicSample.slnx new file mode 100644 index 00000000000..afa3f59aaeb --- /dev/null +++ b/samples/ibmmq/ebcdic/IBMMQ_1/EbcdicSample.slnx @@ -0,0 +1,5 @@ + + + + + diff --git a/samples/ibmmq/ebcdic/IBMMQ_1/LegacySender/LegacySender.csproj b/samples/ibmmq/ebcdic/IBMMQ_1/LegacySender/LegacySender.csproj new file mode 100644 index 00000000000..99d40d62205 --- /dev/null +++ b/samples/ibmmq/ebcdic/IBMMQ_1/LegacySender/LegacySender.csproj @@ -0,0 +1,14 @@ + + + + Exe + net10.0 + enable + enable + + + + + + + diff --git a/samples/ibmmq/ebcdic/IBMMQ_1/LegacySender/Program.cs b/samples/ibmmq/ebcdic/IBMMQ_1/LegacySender/Program.cs new file mode 100644 index 00000000000..fdf4eff6164 --- /dev/null +++ b/samples/ibmmq/ebcdic/IBMMQ_1/LegacySender/Program.cs @@ -0,0 +1,66 @@ +using System.Buffers.Binary; +using System.Collections; +using System.Text; +using IBM.WMQ; + +Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + +var host = Environment.GetEnvironmentVariable("IBMMQ_HOST") ?? "localhost"; +var port = int.Parse(Environment.GetEnvironmentVariable("IBMMQ_PORT") ?? "1414"); +var channel = Environment.GetEnvironmentVariable("IBMMQ_CHANNEL") ?? "APP.SVRCONN"; +var user = Environment.GetEnvironmentVariable("IBMMQ_USER") ?? "sender"; + +const int CODESET_EBCDIC_INT = 500; +var ebcdic = Encoding.GetEncoding(CODESET_EBCDIC_INT); + +var props = new Hashtable +{ + { MQC.TRANSPORT_PROPERTY, MQC.TRANSPORT_MQSERIES_MANAGED }, + { MQC.HOST_NAME_PROPERTY, host }, + { MQC.PORT_PROPERTY, port }, + { MQC.CHANNEL_PROPERTY, channel }, + { MQC.USER_ID_PROPERTY, user }, +}; + +using var qm = new MQQueueManager("QM1", props); +using var queue = qm.AccessQueue("Acme.Sales", MQC.MQOO_OUTPUT); + +Console.WriteLine("Acme.LegacySender (EBCDIC) started."); +Console.WriteLine("Press [P] to place an order, [Q] to quit."); + +while (true) +{ + var key = Console.ReadKey(true); + + if (key.Key is ConsoleKey.Q) + break; + + if (key.Key is ConsoleKey.P) + { + var orderId = Guid.NewGuid(); + var product = "Widget"; + var quantity = Random.Shared.Next(1, 10); + + var body = new byte[70]; + + // OrderId: 36 bytes EBCDIC + ebcdic.GetBytes(orderId.ToString(), body.AsSpan(0, 36)); + + // Product: 30 bytes EBCDIC, space-padded + var productSpan = body.AsSpan(36, 30); + productSpan.Fill(ebcdic.GetBytes(" ")[0]); // EBCDIC space = 0x40 + ebcdic.GetBytes(product, productSpan); + + // Quantity: 4 bytes big-endian int32 + BinaryPrimitives.WriteInt32BigEndian(body.AsSpan(66, 4), quantity); + + var msg = new MQMessage(); + msg.CharacterSet = CODESET_EBCDIC_INT; + msg.Format = MQC.MQFMT_NONE; + msg.Write(body); + + queue.Put(msg); + + Console.WriteLine($"Sent EBCDIC PlaceOrder {{ OrderId = {orderId}, Product = {product}, Quantity = {quantity} }}"); + } +} diff --git a/samples/ibmmq/ebcdic/IBMMQ_1/Sales/Dockerfile b/samples/ibmmq/ebcdic/IBMMQ_1/Sales/Dockerfile new file mode 100644 index 00000000000..fcf25fb7d5e --- /dev/null +++ b/samples/ibmmq/ebcdic/IBMMQ_1/Sales/Dockerfile @@ -0,0 +1,11 @@ +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +WORKDIR /src +COPY nuget.config . +COPY src/Acme.Shared/Acme.Shared.csproj src/Acme.Shared/ +COPY src/Acme.Sales/Acme.Sales.csproj src/Acme.Sales/ +RUN dotnet restore src/Acme.Sales/Acme.Sales.csproj + +FROM mcr.microsoft.com/dotnet/aspnet:10.0 +WORKDIR /app +COPY --from=build /app . +ENTRYPOINT ["dotnet", "Acme.Sales.dll"] diff --git a/samples/ibmmq/ebcdic/IBMMQ_1/Sales/Ebcdic/EbcdicEnvelopeFeature.cs b/samples/ibmmq/ebcdic/IBMMQ_1/Sales/Ebcdic/EbcdicEnvelopeFeature.cs new file mode 100644 index 00000000000..733c43e4f7d --- /dev/null +++ b/samples/ibmmq/ebcdic/IBMMQ_1/Sales/Ebcdic/EbcdicEnvelopeFeature.cs @@ -0,0 +1,14 @@ + +using System.Text; +using NServiceBus.Features; + +#region EbcdicEnvelopeFeature +sealed class EbcdicEnvelopeFeature : Feature +{ + protected override void Setup(FeatureConfigurationContext context) + { + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + context.AddEnvelopeHandler(); + } +} +#endregion diff --git a/samples/ibmmq/ebcdic/IBMMQ_1/Sales/Ebcdic/EbcdicEnvelopeHandler.cs b/samples/ibmmq/ebcdic/IBMMQ_1/Sales/Ebcdic/EbcdicEnvelopeHandler.cs new file mode 100644 index 00000000000..cd6f19b6ca7 --- /dev/null +++ b/samples/ibmmq/ebcdic/IBMMQ_1/Sales/Ebcdic/EbcdicEnvelopeHandler.cs @@ -0,0 +1,59 @@ +using System.Buffers; +using System.Buffers.Binary; +using System.Text; +using System.Text.Json; +using Contracts; +using NServiceBus.Extensibility; + +#region EbcdicEnvelopeHandler +sealed class EbcdicEnvelopeHandler : IEnvelopeHandler +{ + const int CODESET_EBCDIC_INT = 500; + static readonly Encoding Ebcdic = Encoding.GetEncoding(CODESET_EBCDIC_INT); + const int RecordLength = 70; + + public Dictionary? UnwrapEnvelope( + string nativeMessageId, + IDictionary incomingHeaders, + ReadOnlySpan incomingBody, + ContextBag extensions, + IBufferWriter bodyWriter) + { + if (incomingHeaders.ContainsKey("NServiceBus.EnclosedMessageTypes")) + return null; + + if (incomingBody.Length != RecordLength) + return null; + + // Parse fixed-length EBCDIC record + var orderId = Ebcdic.GetString(incomingBody[..36]); + var product = Ebcdic.GetString(incomingBody[36..66]).TrimEnd(); + var quantity = BinaryPrimitives.ReadInt32BigEndian(incomingBody[66..70]); + + // Write JSON body + var writer = new Utf8JsonWriter(bodyWriter); + writer.WriteStartObject(); + writer.WriteString("OrderId", orderId); + writer.WriteString("Product", product); + writer.WriteNumber("Quantity", quantity); + writer.WriteEndObject(); + writer.Flush(); + + var messageType = typeof(PlaceOrder); + + //MQMessage m; + return new Dictionary + { + ["NServiceBus.MessageId"] = nativeMessageId, + ["NServiceBus.ConversationId"] = nativeMessageId, + ["NServiceBus.EnclosedMessageTypes"] = messageType.FullName + ", " + messageType.Assembly.GetName().Name, + ["NServiceBus.ContentType"] = "application/json", + ["NServiceBus.MessageIntent"] = "Send", + ["NServiceBus.TimeSent"] = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss:ffffff") + " Z", + ["NServiceBus.ReplyToAddress"] = "Acme.LegacySender", + ["NServiceBus.OriginatingEndpoint"] = "LegacyMainframe", + ["NServiceBus.OriginatingMachine"] = "MAINFRAME", + }; + } +} +#endregion diff --git a/samples/ibmmq/ebcdic/IBMMQ_1/Sales/PlaceOrderHandler.cs b/samples/ibmmq/ebcdic/IBMMQ_1/Sales/PlaceOrderHandler.cs new file mode 100644 index 00000000000..cf2e04d6423 --- /dev/null +++ b/samples/ibmmq/ebcdic/IBMMQ_1/Sales/PlaceOrderHandler.cs @@ -0,0 +1,16 @@ +using Contracts; +using Microsoft.Extensions.Logging; + +#region PlaceOrderHandler +sealed class PlaceOrderHandler(ILogger logger) : IHandleMessages +{ + public async Task Handle(PlaceOrder message, IMessageHandlerContext context) + { + await context.Reply(new OrderAccepted(message.OrderId)); + logger.LogInformation("Replied OrderAccepted {{ OrderId = {OrderId} }}", message.OrderId); + + await context.Publish(new OrderPlaced(message.OrderId, message.Product, message.Quantity)); + logger.LogInformation("Published OrderPlaced {{ OrderId = {OrderId} }}", message.OrderId); + } +} +#endregion diff --git a/samples/ibmmq/ebcdic/IBMMQ_1/Sales/Program.cs b/samples/ibmmq/ebcdic/IBMMQ_1/Sales/Program.cs new file mode 100644 index 00000000000..6ad6c9f1946 --- /dev/null +++ b/samples/ibmmq/ebcdic/IBMMQ_1/Sales/Program.cs @@ -0,0 +1,35 @@ +using Microsoft.Extensions.Hosting; +using NServiceBus; + +var host = Host.CreateDefaultBuilder(args) + .UseNServiceBus(context => + { + var endpointConfiguration = new EndpointConfiguration("Sales"); + + endpointConfiguration.UseSerialization(); + + + var transport = new IBMMQTransport(o => + { + o.Host = "ibmmq"; + o.Port = 1414; + o.QueueManagerName = "QM1"; + o.Channel = "APP.SVRCONN"; + o.User = "sales"; + o.TopicNaming = new ShortenedTopicNaming(); + }); + + var routing = endpointConfiguration.UseTransport(transport); + endpointConfiguration.EnableInstallers(); + + + endpointConfiguration.EnableFeature(); + + endpointConfiguration.AuditProcessedMessagesTo("audit"); + endpointConfiguration.SendFailedMessagesTo("error"); + + return endpointConfiguration; + }) + .Build(); + +await host.RunAsync(); diff --git a/samples/ibmmq/ebcdic/IBMMQ_1/Sales/Sales.csproj b/samples/ibmmq/ebcdic/IBMMQ_1/Sales/Sales.csproj new file mode 100644 index 00000000000..73fcc7e9ee3 --- /dev/null +++ b/samples/ibmmq/ebcdic/IBMMQ_1/Sales/Sales.csproj @@ -0,0 +1,20 @@ + + + + Exe + net10.0 + enable + enable + + + + + + + + + + + + + diff --git a/samples/ibmmq/ebcdic/IBMMQ_1/Sales/appsettings.json b/samples/ibmmq/ebcdic/IBMMQ_1/Sales/appsettings.json new file mode 100644 index 00000000000..f29650766b1 --- /dev/null +++ b/samples/ibmmq/ebcdic/IBMMQ_1/Sales/appsettings.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Warning", + "Acme.ControlQueueFeature": "Information" + } + } +} diff --git a/samples/ibmmq/ebcdic/IBMMQ_1/prerelease.txt b/samples/ibmmq/ebcdic/IBMMQ_1/prerelease.txt new file mode 100644 index 00000000000..e69de29bb2d diff --git a/samples/ibmmq/ebcdic/sample.md b/samples/ibmmq/ebcdic/sample.md new file mode 100644 index 00000000000..ab70c640261 --- /dev/null +++ b/samples/ibmmq/ebcdic/sample.md @@ -0,0 +1,79 @@ +--- +title: IBM MQ EBCDIC interoperability +summary: Receiving fixed-length EBCDIC-encoded messages from a legacy mainframe system via IBM MQ +reviewed: 2026-03-20 +component: Core +related: +- transports/ibmmq +- transports/ibmmq/native-integration +- nservicebus/pipeline/features +- nservicebus/messaging/headers +- nservicebus/serialization/system-json +--- + +This sample demonstrates how to receive fixed-length [EBCDIC](https://en.wikipedia.org/wiki/EBCDIC)-encoded messages sent by a legacy mainframe system over IBM MQ, and process them in a modern NServiceBus endpoint. + +The sample includes: + +- A **LegacySender** console application that simulates a mainframe producing EBCDIC-encoded fixed-length records directly to IBM MQ using the native `IBM.WMQ` API +- A **Sales** NServiceBus endpoint that receives those raw messages, decodes them, and processes them as standard NServiceBus [commands](/nservicebus/messaging/messages-events-commands.md) + +## How it works + +Mainframe systems often communicate using EBCDIC encoding and fixed-length binary record layouts rather than JSON or XML. Such messages carry no [NServiceBus headers](/nservicebus/messaging/headers.md) and cannot be deserialized by the endpoint without prior transformation. + +The IBM MQ transport exposes an envelope handler extension point (`IEnvelopeHandler`) that intercepts raw incoming messages before they reach the [message handler pipeline](/nservicebus/pipeline/). The envelope handler can inspect the raw bytes, decode them, and return a rewritten body and a set of headers - making the transformed message indistinguishable from one sent natively by NServiceBus. + +## Running the sample + +The sample requires a running IBM MQ instance. A Docker Compose configuration is recommended. Start both projects, then press `P` in the LegacySender console to place an order. The Sales endpoint will log the decoded order and reply with an `OrderAccepted` event. + +## Code walk-through + +### Legacy sender + +The `LegacySender` project simulates a mainframe producing a 70-byte fixed-length EBCDIC record and putting it directly on the `Acme.Sales` queue using the native `IBM.WMQ` API. The message carries no [MQRFH2 headers](/transports/ibmmq/native-integration.md#message-structure) and no NServiceBus metadata. + +The record layout is: + +| Bytes | Field | Encoding | +|-------|----------|------------------------------------------------| +| 0-35 | OrderId | EBCDIC code page 500, 36 chars | +| 36-65 | Product | EBCDIC code page 500, space-padded to 30 chars | +| 66-69 | Quantity | Big-endian `Int32` | + +The `MQMessage.CharacterSet` is set to `500` (EBCDIC) and `MQMessage.Format` is set to `MQFMT_NONE`, indicating raw binary content with no broker-managed structured headers. + +### Enabling the envelope handler + +`EbcdicEnvelopeFeature` is a custom [NServiceBus feature](/nservicebus/pipeline/features.md) that registers the EBCDIC code page provider and wires up the envelope handler: + +snippet: EbcdicEnvelopeFeature + +Enable the feature in endpoint configuration: + +```csharp +endpointConfiguration.EnableFeature(); +``` + +### Decoding the EBCDIC envelope + +`EbcdicEnvelopeHandler` implements `IEnvelopeHandler`. It is called for every incoming message before the message is dispatched to a [message handler](/nservicebus/handlers/). + +snippet: EbcdicEnvelopeHandler + +The handler: + +1. Returns `null` for messages that already carry `NServiceBus.EnclosedMessageTypes` - those are standard NServiceBus messages that require no transformation. +2. Returns `null` for records that are not exactly 70 bytes - allowing the message to be handled by any other registered envelope handler or failing with an appropriate error. +3. Decodes the three fixed-length fields from EBCDIC bytes into .NET types using `Encoding.GetEncoding(500)` and `BinaryPrimitives.ReadInt32BigEndian`. +4. Writes a JSON body into the provided `IBufferWriter`, compatible with the [`SystemJsonSerializer`](/nservicebus/serialization/system-json.md) configured on the endpoint. +5. Returns a [headers](/nservicebus/messaging/headers.md) dictionary that provides all the NServiceBus routing metadata the pipeline requires: message type, content type, intent, reply-to address, and originating endpoint. + +### Message handler + +`PlaceOrderHandler` processes the decoded [`PlaceOrder`](/nservicebus/messaging/messages-events-commands.md) command using standard NServiceBus APIs - it has no awareness of the EBCDIC origin of the message: + +snippet: PlaceOrderHandler + +The handler [replies](/nservicebus/messaging/reply-to-a-message.md) to the originating address (`Acme.LegacySender` as set in the envelope headers) and [publishes](/nservicebus/messaging/publish-subscribe/) an `OrderPlaced` event. diff --git a/samples/ibmmq/index.md b/samples/ibmmq/index.md new file mode 100644 index 00000000000..9a153437248 --- /dev/null +++ b/samples/ibmmq/index.md @@ -0,0 +1,8 @@ +--- +title: IBM MQ Transport Samples +reviewed: 2026-03-20 +--- + +These samples show how to use NServiceBus with IBM MQ Transport. + + From a4083d2ff2375e668fe19d8ef96fe288098560ac Mon Sep 17 00:00:00 2001 From: Irina Dominte Date: Fri, 20 Mar 2026 15:45:56 +0200 Subject: [PATCH 15/19] removed not needed line --- samples/ibmmq/ebcdic/IBMMQ_1/Sales/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/ibmmq/ebcdic/IBMMQ_1/Sales/Program.cs b/samples/ibmmq/ebcdic/IBMMQ_1/Sales/Program.cs index 6ad6c9f1946..a573c77de22 100644 --- a/samples/ibmmq/ebcdic/IBMMQ_1/Sales/Program.cs +++ b/samples/ibmmq/ebcdic/IBMMQ_1/Sales/Program.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.Hosting; using NServiceBus; +using NServiceBus.Transport.IBMMQ; var host = Host.CreateDefaultBuilder(args) .UseNServiceBus(context => @@ -16,7 +17,6 @@ o.QueueManagerName = "QM1"; o.Channel = "APP.SVRCONN"; o.User = "sales"; - o.TopicNaming = new ShortenedTopicNaming(); }); var routing = endpointConfiguration.UseTransport(transport); From 4415781639bbb333c60e69797bc31c502d10926c Mon Sep 17 00:00:00 2001 From: Irina Dominte Date: Mon, 23 Mar 2026 13:11:15 +0200 Subject: [PATCH 16/19] added native integration and observability --- transports/ibmmq/index.md | 3 +- transports/ibmmq/native-integration.md | 22 ++++++-- transports/ibmmq/observability.md | 71 ++++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 5 deletions(-) create mode 100644 transports/ibmmq/observability.md diff --git a/transports/ibmmq/index.md b/transports/ibmmq/index.md index 84788f012dd..462512a536f 100644 --- a/transports/ibmmq/index.md +++ b/transports/ibmmq/index.md @@ -1,7 +1,7 @@ --- title: IBM MQ Transport summary: Integrate NServiceBus with IBM MQ for enterprise messaging on mainframe and distributed platforms -reviewed: 2026-02-19 +reviewed: 2026-03-23 component: IBMMQ related: redirects: @@ -25,6 +25,7 @@ The transport requires IBM MQ 9.0 or later. |Scripted Deployment |Supported via [CLI tool](operations-scripting.md) |Installers |Optional |Native integration |[Supported](native-integration.md) +|OpenTelemetry tracing |[Supported](observability.md) |Case Sensitive |Yes |`TransactionScope` mode (distributed transactions) |No |SSL/TLS encryption and certificate-based authentication |Yes diff --git a/transports/ibmmq/native-integration.md b/transports/ibmmq/native-integration.md index c198afd8e68..d01584bd0c1 100644 --- a/transports/ibmmq/native-integration.md +++ b/transports/ibmmq/native-integration.md @@ -1,7 +1,7 @@ --- title: Native integration summary: How to integrate NServiceBus endpoints with native IBM MQ applications -reviewed: 2026-02-19 +reviewed: 2026-03-23 component: IBMMQ --- @@ -13,12 +13,12 @@ NServiceBus messages on IBM MQ consist of a standard message descriptor (MQMD) a ### MQMD fields -The transport maps NServiceBus concepts to the following MQMD fields: +The transport maps NServiceBus concepts to the following MQMD fields on send: |MQMD field|Usage| |:---|---| -|MessageId|Set from the `NServiceBus.MessageId` header| -|CorrelationId|Set from the `NServiceBus.CorrelationId` header| +|MessageId|Set from the `NServiceBus.MessageId` header. Accepts a GUID or a hex string up to 24 bytes.| +|CorrelationId|Set from the `NServiceBus.CorrelationId` header. Accepts a GUID or a hex string up to 24 bytes.| |MessageType|Always `MQMT_DATAGRAM`| |Persistence|Persistent by default; non-persistent if `NonDurableMessage` header is set| |Expiry|Set from the time-to-be-received setting; unlimited if not specified| @@ -34,6 +34,20 @@ All NServiceBus headers are stored as MQRFH2 message properties. Property names > [!NOTE] > IBM MQ silently discards string properties with empty values. The transport includes manifest properties (`nsbhdrs` and `nsbempty`) to track all header names and preserve empty values. The header fields with non-compliant names will now be displayed in the IBMMQ Console. +## Receiving from non-NServiceBus senders + +When a message arrives from a native IBM MQ application that does not set MQRFH2 properties, the transport promotes the following MQMD fields to NServiceBus headers. Promotion only occurs when the corresponding NServiceBus header is not already present, so NServiceBus-originated messages are unaffected. + +|MQMD field|NServiceBus header|Notes| +|:---|---|---| +|`MessageId`|`NServiceBus.MessageId`|Encoded as an uppercase hex string| +|`CorrelationId`|`NServiceBus.CorrelationId`|Encoded as an uppercase hex string| +|`ReplyToQueueName`|`NServiceBus.ReplyToAddress`|Trimmed of whitespace; ignored if empty| +|`Persistence`|`NServiceBus.NonDurableMessage`|Set to `True` when persistence is `NOT_PERSISTENT`| +|`Expiry`|`NServiceBus.TimeToBeReceived`|Converted from tenths of seconds; ignored when unlimited| + +This enables native senders to rely on standard MQMD fields for identity and routing without requiring knowledge of the NServiceBus header format. + ## Sending messages from native applications To send a message to an NServiceBus endpoint from a native IBM MQ application: diff --git a/transports/ibmmq/observability.md b/transports/ibmmq/observability.md new file mode 100644 index 00000000000..5d011dfaeca --- /dev/null +++ b/transports/ibmmq/observability.md @@ -0,0 +1,71 @@ +--- +title: Observability +summary: Tracing IBM MQ transport operations with OpenTelemetry +reviewed: 2026-03-23 +component: IBMMQ +related: +- nservicebus/operations/opentelemetry +--- + +The IBM MQ transport instruments send, receive, and dispatch operations using `System.Diagnostics.Activity` and follows the [OpenTelemetry messaging semantic conventions](https://opentelemetry.io/docs/specs/semconv/messaging/). + +## Activity source + +The transport emits activities under the activity source named `NServiceBus.Transport.IBMMQ`. Register this source with the OpenTelemetry tracer provider to collect transport-level spans: + +```csharp +builder.Services.AddOpenTelemetry() + .WithTracing(tracing => tracing + .AddSource("NServiceBus.Transport.IBMMQ") + .AddOtlpExporter()); +``` + +## Activities + +The following activities are created for each transport operation: + +| Activity name | Kind | Display name | +|:---|---|---| +| `NServiceBus.Transport.IBMMQ.Receive` | Consumer | `receive {queueName}` | +| `NServiceBus.Transport.IBMMQ.Dispatch` | Internal | `dispatch` | +| `NServiceBus.Transport.IBMMQ.PutToQueue` | Producer | `send {destination}` | +| `NServiceBus.Transport.IBMMQ.PutToTopic` | Producer | `publish {topicName}` | +| `NServiceBus.Transport.IBMMQ.Attempt` | Internal | _(unnamed)_ | + +The `Attempt` activity wraps each processing attempt inside the immediate retry loop. It records failure details when message processing raises an exception. + +NServiceBus core parents its receive pipeline activity to the transport `Receive` activity, producing a complete trace from transport dequeue through handler execution. + +## Tags + +### Standard messaging tags + +These tags follow the [OpenTelemetry messaging semantic conventions](https://opentelemetry.io/docs/specs/semconv/messaging/): + +| Tag | Activities | Value | +|:---|---|---| +| `messaging.system` | All | `ibm_mq` | +| `messaging.destination.name` | `Receive`, `PutToQueue`, `PutToTopic` | Queue or topic name | +| `messaging.operation.type` | `Receive`, `PutToQueue`, `PutToTopic` | `receive`, `send`, or `publish` | +| `messaging.message.id` | `PutToQueue`, `PutToTopic` | Native MQ message ID as a hex string | +| `messaging.batch.message_count` | `Dispatch` | Total number of outgoing operations in the batch | + +### Vendor-specific tags + +| Tag | Activities | Value | +|:---|---|---| +| `nservicebus.transport.ibmmq.topic_string` | `PutToTopic` | The IBM MQ topic string used to open the topic | +| `nservicebus.transport.ibmmq.failure_count` | `Receive`, `Attempt` | Number of processing failures for the current message | + +## Activity events + +The following events are added to the active receive activity when a transaction outcome is recorded: + +| Event | When | +|:---|---| +| `mq.commit` | Transaction committed successfully | +| `mq.backout` | Transaction backed out (message returned to queue) | + +## Error status + +When an operation fails, the activity status is set to `Error` with the exception message as the description. This applies to `Receive`, `Dispatch`, `PutToQueue`, `PutToTopic`, and `Attempt` activities. From 18a57ed990729da60d0c930941b28a2831b33d3b Mon Sep 17 00:00:00 2001 From: Irina Dominte Date: Tue, 24 Mar 2026 15:07:12 +0200 Subject: [PATCH 17/19] Sample for request reply, polymorphic events, and simple pub sub --- .claude/skills/sample-doc/SKILL.md | 231 ++++++++++++++++++ docker-compose.yml | 17 ++ samples/ibmmq/ebcdic/IBMMQ_1/Sales/Dockerfile | 11 - .../Sales/Ebcdic/EbcdicEnvelopeFeature.cs | 14 -- .../Sales/Ebcdic/EbcdicEnvelopeHandler.cs | 59 ----- .../IBMMQ_1/Sales/Ebcdic/EbcdicMutator.cs | 63 +++++ samples/ibmmq/ebcdic/IBMMQ_1/Sales/Program.cs | 10 +- .../ibmmq/ebcdic/IBMMQ_1/Sales/Sales.csproj | 2 +- samples/ibmmq/ebcdic/sample.md | 35 ++- .../IBMMQ_2/Orders/Orders.csproj | 20 ++ .../IBMMQ_2/Orders/Program.cs | 58 +++++ .../IBMMQ_2/Shared/Messages.cs | 6 + .../IBMMQ_2/Shared/Shared.csproj | 13 + .../IBMMQ_2/Shipping/OrderPlacedHandler.cs | 11 + .../IBMMQ_2/Shipping/Program.cs | 27 ++ .../IBMMQ_2/Shipping/Shipping.csproj | 20 ++ .../IBMMQ_2/docker-compose.yml | 19 ++ .../polymorphic-events/IBMMQ_2/prerelease.txt | 0 .../polymorphic-events/IBMMQ_2/sample.slnx | 10 + samples/ibmmq/polymorphic-events/sample.md | 53 ++++ .../IBMMQ_2/Client/Client.csproj | 20 ++ .../IBMMQ_2/Client/OrderResponseHandler.cs | 10 + .../request-reply/IBMMQ_2/Client/Program.cs | 58 +++++ .../IBMMQ_2/Server/PlaceOrderHandler.cs | 14 ++ .../request-reply/IBMMQ_2/Server/Program.cs | 27 ++ .../IBMMQ_2/Server/Server.csproj | 20 ++ .../IBMMQ_2/Shared/PlaceOrderRequest.cs | 5 + .../IBMMQ_2/Shared/Shared.csproj | 13 + .../request-reply/IBMMQ_2/docker-compose.yml | 19 ++ .../request-reply/IBMMQ_2/prerelease.txt | 0 .../ibmmq/request-reply/IBMMQ_2/sample.slnx | 10 + samples/ibmmq/request-reply/sample.md | 69 ++++++ .../simple/IBMMQ_2/Receiver/MyHandler.cs | 21 ++ .../ibmmq/simple/IBMMQ_2/Receiver/Program.cs | 30 +++ .../Receiver/Properties/launchSettings.json | 12 + .../simple/IBMMQ_2/Receiver/Receiver.csproj | 25 ++ .../ibmmq/simple/IBMMQ_2/Sender/Program.cs | 78 ++++++ .../Sender/Properties/launchSettings.json | 12 + .../ibmmq/simple/IBMMQ_2/Sender/Sender.csproj | 25 ++ .../Sender/appsettings.Development.json | 9 + .../simple/IBMMQ_2/Sender/appsettings.json | 8 + .../ibmmq/simple/IBMMQ_2/Shared/MyMessage.cs | 3 + .../ibmmq/simple/IBMMQ_2/Shared/Shared.csproj | 14 ++ .../ibmmq/simple/IBMMQ_2/docker-compose.yml | 21 ++ samples/ibmmq/simple/IBMMQ_2/prerelease.txt | 0 samples/ibmmq/simple/IBMMQ_2/sample.slnLaunch | 15 ++ samples/ibmmq/simple/IBMMQ_2/sample.slnx | 10 + samples/ibmmq/simple/sample.md | 61 +++++ 48 files changed, 1176 insertions(+), 112 deletions(-) create mode 100644 .claude/skills/sample-doc/SKILL.md create mode 100644 docker-compose.yml delete mode 100644 samples/ibmmq/ebcdic/IBMMQ_1/Sales/Dockerfile delete mode 100644 samples/ibmmq/ebcdic/IBMMQ_1/Sales/Ebcdic/EbcdicEnvelopeFeature.cs delete mode 100644 samples/ibmmq/ebcdic/IBMMQ_1/Sales/Ebcdic/EbcdicEnvelopeHandler.cs create mode 100644 samples/ibmmq/ebcdic/IBMMQ_1/Sales/Ebcdic/EbcdicMutator.cs create mode 100644 samples/ibmmq/polymorphic-events/IBMMQ_2/Orders/Orders.csproj create mode 100644 samples/ibmmq/polymorphic-events/IBMMQ_2/Orders/Program.cs create mode 100644 samples/ibmmq/polymorphic-events/IBMMQ_2/Shared/Messages.cs create mode 100644 samples/ibmmq/polymorphic-events/IBMMQ_2/Shared/Shared.csproj create mode 100644 samples/ibmmq/polymorphic-events/IBMMQ_2/Shipping/OrderPlacedHandler.cs create mode 100644 samples/ibmmq/polymorphic-events/IBMMQ_2/Shipping/Program.cs create mode 100644 samples/ibmmq/polymorphic-events/IBMMQ_2/Shipping/Shipping.csproj create mode 100644 samples/ibmmq/polymorphic-events/IBMMQ_2/docker-compose.yml create mode 100644 samples/ibmmq/polymorphic-events/IBMMQ_2/prerelease.txt create mode 100644 samples/ibmmq/polymorphic-events/IBMMQ_2/sample.slnx create mode 100644 samples/ibmmq/polymorphic-events/sample.md create mode 100644 samples/ibmmq/request-reply/IBMMQ_2/Client/Client.csproj create mode 100644 samples/ibmmq/request-reply/IBMMQ_2/Client/OrderResponseHandler.cs create mode 100644 samples/ibmmq/request-reply/IBMMQ_2/Client/Program.cs create mode 100644 samples/ibmmq/request-reply/IBMMQ_2/Server/PlaceOrderHandler.cs create mode 100644 samples/ibmmq/request-reply/IBMMQ_2/Server/Program.cs create mode 100644 samples/ibmmq/request-reply/IBMMQ_2/Server/Server.csproj create mode 100644 samples/ibmmq/request-reply/IBMMQ_2/Shared/PlaceOrderRequest.cs create mode 100644 samples/ibmmq/request-reply/IBMMQ_2/Shared/Shared.csproj create mode 100644 samples/ibmmq/request-reply/IBMMQ_2/docker-compose.yml create mode 100644 samples/ibmmq/request-reply/IBMMQ_2/prerelease.txt create mode 100644 samples/ibmmq/request-reply/IBMMQ_2/sample.slnx create mode 100644 samples/ibmmq/request-reply/sample.md create mode 100644 samples/ibmmq/simple/IBMMQ_2/Receiver/MyHandler.cs create mode 100644 samples/ibmmq/simple/IBMMQ_2/Receiver/Program.cs create mode 100644 samples/ibmmq/simple/IBMMQ_2/Receiver/Properties/launchSettings.json create mode 100644 samples/ibmmq/simple/IBMMQ_2/Receiver/Receiver.csproj create mode 100644 samples/ibmmq/simple/IBMMQ_2/Sender/Program.cs create mode 100644 samples/ibmmq/simple/IBMMQ_2/Sender/Properties/launchSettings.json create mode 100644 samples/ibmmq/simple/IBMMQ_2/Sender/Sender.csproj create mode 100644 samples/ibmmq/simple/IBMMQ_2/Sender/appsettings.Development.json create mode 100644 samples/ibmmq/simple/IBMMQ_2/Sender/appsettings.json create mode 100644 samples/ibmmq/simple/IBMMQ_2/Shared/MyMessage.cs create mode 100644 samples/ibmmq/simple/IBMMQ_2/Shared/Shared.csproj create mode 100644 samples/ibmmq/simple/IBMMQ_2/docker-compose.yml create mode 100644 samples/ibmmq/simple/IBMMQ_2/prerelease.txt create mode 100644 samples/ibmmq/simple/IBMMQ_2/sample.slnLaunch create mode 100644 samples/ibmmq/simple/IBMMQ_2/sample.slnx create mode 100644 samples/ibmmq/simple/sample.md diff --git a/.claude/skills/sample-doc/SKILL.md b/.claude/skills/sample-doc/SKILL.md new file mode 100644 index 00000000000..37e0da78653 --- /dev/null +++ b/.claude/skills/sample-doc/SKILL.md @@ -0,0 +1,231 @@ +--- +name: sample-doc +description: Generate a sample.md documentation file for a new NServiceBus sample. Reads the sample source code, analyses the structure, drafts the doc, then waits for approval before saving. Also verifies and fixes all wiring required for the docs engine to render the sample correctly. +--- + +You are a technical writer for the docs.particular.net documentation site, specialising in NServiceBus and the Particular Service Platform. + +--- + +## Task + +Generate a `sample.md` file for a sample in the `samples/` folder, then verify all the infrastructure the docs engine needs to render it. The engine has strict requirements - a missing file or wrong directory name causes unhandled exceptions at render time. + +--- + +## Workflow — follow in order + +### Step 1 — Identify the sample + +If the user provides a path or sample name, resolve it under `samples/`. If not, ask: +*"Which sample should I document? Provide the folder path under samples/ (e.g. samples/ibmmq/ebcdic)."* + +### Step 2 — Read the source + +Before writing anything, read: +- All `.cs` source files in the sample folder (especially `Program.cs`, feature files, handlers, and any interop/helper classes) +- All `.csproj` files to identify NuGet package dependencies and the component being used +- Any existing `sample.md` in a sibling folder of the same transport/component for style reference + +Use these reads to answer: +- What does this sample demonstrate? (one sentence) +- What are the projects and their roles? +- What are the key classes/methods a reader needs to understand? +- What `snippet:` names can be referenced? (`#region` marker names — see note below) + +### Step 3 — Identify the component key + +Check `transports//index.md` or grep existing docs for `component:` values to find the correct key (e.g. `IBMMQ`, `RabbitMQ`, `SqlTransport`, `Core`). + +Check `components/nugetAlias.txt` to confirm the NuGet alias for the component (e.g. `IBMMQ: NServiceBus.Transport.IBMMQ`). The alias is the prefix used in versioned directory names. + +### Step 4 — Draft the sample.md + +Write a draft following the structure and rules below. Present it clearly labelled as **Draft sample.md**. + +### Step 5 — Wait for feedback + +After presenting the draft, **stop and wait**. +- If the user approves or says "go" / "looks good", save the file to `samples//sample.md` using the Write tool and confirm. +- If they request edits, apply them and show the revised draft. + +### Step 6 — Wire the sample into the site + +After saving `sample.md`, run through every check below in order. Fix any issues found before moving to the next check. Report all findings clearly. + +#### 6a — Snippet markers + +For every `snippet: KEY` directive in `sample.md`, verify that a corresponding `#region KEY` / `#endregion` block exists somewhere in the `.cs` files inside the sample folder. + +**Important**: the engine does not extract snippets by class name. Every key — including ones named after a class like `MyHandler` — requires an explicit `#region`/`#endregion` marker. Class name matching does not work. + +If any are missing, add the `#region`/`#endregion` markers around the relevant class body or method. The region name must match the snippet key exactly (case-insensitive). + +**Also check the reverse**: every `#region` in every `.cs` file inside the versioned directory must be referenced by a `snippet:` in `sample.md`. Any unreferenced region causes a `RedundantSnippets` exception at render time. Either add a `snippet:` reference for it or remove the `#region`/`#endregion` markers. + +#### 6b — Versioned directory + +The docs engine resolves which component version to render a sample for by looking for directories matching `{NugetAlias}_{MajorVersion}` (e.g. `IBMMQ_1`, `Core_10`, `Rabbit_10`) directly inside the sample folder (same level as `sample.md`). + +Check that such a directory exists: + +``` +samples/// + {NugetAlias}_{MajorVersion}/ ← versioned directory (e.g. IBMMQ_1, Rabbit_10, Core_10) + MySolution.sln + MyProject/ + MyProject.csproj + sample.md +``` + +If the solution directory exists but is not named with the versioned convention: + +- Rename it to `{NugetAlias}_{MajorVersion}/` using `mv` (requires VS to not have the solution open — if locked, tell the user to close VS first) +- The major version number comes from the transport package version in `.csproj` + +If there is an extra nesting level (e.g. `IBMMQ_1/OldName/solution files`), move the solution files up one level so they sit directly inside `IBMMQ_1/`. + +#### 6c — Package reference + +Open every `.csproj` inside the versioned directory. The transport's NuGet package (e.g. `NServiceBus.Transport.IBMMQ`) must be listed as a `PackageReference`. + +If it is missing, add it. Use the same version as found in `Snippets/{NugetAlias}/{NugetAlias}_1/{NugetAlias}_1.csproj` (the snippets project for that component), which always pins the canonical version. + +#### 6d — prerelease.txt + +If the transport package version contains a prerelease suffix (e.g. `-alpha.1`, `-beta.2`), the versioned directory must contain an empty `prerelease.txt` file. + +Check: `ls samples///{NugetAlias}_{Version}/prerelease.txt` + +If missing, create it as an empty file. + +#### 6e — Category index + +Every transport-level sample folder (`samples//`) must have an `index.md` so the category appears in the Samples left-hand navigation menu. + +Check that `samples//index.md` exists. If it does not, create it with this exact content (no `component:` field — adding it breaks rendering): + +```yaml +--- +title: Samples +reviewed: +--- +``` + +Compare with `samples/msmq/index.md` or `samples/rabbitmq/index.md` for the expected format. + +#### 6f — Related links (optional, confirm before doing) + +Offer to add `samples//` to the `related:` list in `transports//index.md` so the sample appears as a related link on the transport docs page. + +**Only do this after all previous checks pass** - the engine throws a `Could not find referenced` exception at render time if the sample page is not yet fully indexed (which requires steps 6a-6e to be complete). Ask the user to confirm before adding the link. + +#### 6g — Restart the docs engine + +**Always remind the user to restart the docs engine after wiring.** The `SolutionDownloadMetadata` (the dictionary of downloadable solution zips) is built once at startup. If the versioned directory (`IBMMQ_1/`, etc.) was created or renamed after the engine started, it will not be in the dictionary and every request to that sample page will throw a `KeyNotFoundException`. A restart is always required when a new versioned directory is added. + +--- + +## Frontmatter + +```yaml +--- +title: +summary: +reviewed: +component: +related: +- +- +--- +``` + +Rules: +- `title`: phrase like "IBM MQ EBCDIC interoperability", not "EBCDIC sample" +- `summary`: start with a gerund ("Receiving...", "Demonstrating...", "Configuring...") +- `reviewed`: always set to today's date +- `component`: must match the exact key used in the transport/component docs +- `related`: link to the transport index page, and any NServiceBus concept pages the sample relies on (pipeline, features, sagas, etc.) + +--- + +## Body structure + +``` +<1–2 paragraph introduction — what the sample does and why it matters> + +[Optional: bullet list of what the sample includes, if there are multiple projects] + +## How it works ← high-level concept explanation (skip if obvious) + +## Prerequisites ← only if non-trivial setup is needed (Docker, broker, DB, etc.) + +## Running the sample ← step-by-step instructions, what to observe + +## Code walk-through ← main section; one H3 per key concept + +### + +<1–2 sentences explaining what this does and why> + +snippet: + + +``` + +--- + +## Writing rules + +- **Introduction**: state what the sample demonstrates in the first sentence. Don't say "this sample shows how to show". Say what it does. +- **No filler**: skip phrases like "In this sample, we will explore..." — just describe. +- **Snippet references**: use `snippet: KEY` to embed code. The build system requires a `#region KEY` / `#endregion` block in a `.cs` file - it does **not** extract by class name alone. Every snippet key in `sample.md` must have a corresponding region marker in the source, including handler classes. Never paste raw code unless it is a short inline example that supplements a snippet. +- **Inline code**: use backtick formatting for class names, method names, interface names, header keys, and config values. +- **Tables**: use markdown tables for structured data like message layouts, field mappings, or config options. +- **Links**: two different rules depending on context: + - Inline body links: `.md` extension **required** — `/path/to/page.md`. Omitting it causes a `Path does not exist` render error. + - `related:` frontmatter entries: `.md` extension **forbidden** — `path/to/page`. Including it causes an `Invalid related '...'. Ends with a '.md'` startup exception. + - External links use standard markdown `[text](url)`. +- **Numbered lists**: use for sequential steps (decoding steps, setup steps). Bullet lists for unordered items. +- **Tone**: plain, technical, direct. No marketing language. Write for a developer who wants to understand the code, not be sold on the platform. +- **Hyphen not em dash**: use `-` not `—`. + +--- + +## What to include in the code walk-through + +Cover every class or concept a reader would not immediately understand from the code alone: + +| Element | Include when | +|---|---| +| Feature registration | Always, if a custom `Feature` is used | +| Envelope/pipeline behavior | Always - these are non-obvious extension points | +| Endpoint configuration | When non-standard options are set | +| Message handlers | When the handler does more than a trivial reply/publish | +| Message contracts | Only if the shape is important (e.g. fixed-length, versioned) | +| External system setup | When the sample interacts with a broker/DB/legacy system | + +Skip boilerplate that any NServiceBus developer would recognise (standard `EndpointConfiguration`, basic `IHandleMessages` implementations with a single `Reply`). + +--- + +## Example output + +See `samples/ibmmq/ebcdic/sample.md` for a complete reference of the expected output format and level of detail. + +--- + +## Output summary (after Step 6) + +Report the outcome of each wiring check: + +```text +✓ sample.md saved to samples//sample.md +✓ Snippet markers: all present +✓ Versioned directory: IBMMQ_1/ exists +✓ Package reference: NServiceBus.Transport.IBMMQ added to Sales.csproj +✓ prerelease.txt: created +✓ Category index: samples/ibmmq/index.md exists +⚠ Related link in transports/ibmmq/index.md: skipped (confirm when ready) +⚠ Restart the docs engine to pick up the new versioned directory. +``` diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000000..b4084d4c869 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,17 @@ +services: + ibm-mq: + image: icr.io/ibm-messaging/mq:latest + container_name: ibm-mq + ports: + - "1414:1414" # MQ listener port + - "9443:9443" # Web console + environment: + LICENSE: accept + MQ_QMGR_NAME: QM1 + MQ_ADMIN_PASSWORD: passw0rd + healthcheck: + test: ["CMD", "dspmq"] + interval: 10s + retries: 5 + start_period: 10s + timeout: 5s diff --git a/samples/ibmmq/ebcdic/IBMMQ_1/Sales/Dockerfile b/samples/ibmmq/ebcdic/IBMMQ_1/Sales/Dockerfile deleted file mode 100644 index fcf25fb7d5e..00000000000 --- a/samples/ibmmq/ebcdic/IBMMQ_1/Sales/Dockerfile +++ /dev/null @@ -1,11 +0,0 @@ -FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build -WORKDIR /src -COPY nuget.config . -COPY src/Acme.Shared/Acme.Shared.csproj src/Acme.Shared/ -COPY src/Acme.Sales/Acme.Sales.csproj src/Acme.Sales/ -RUN dotnet restore src/Acme.Sales/Acme.Sales.csproj - -FROM mcr.microsoft.com/dotnet/aspnet:10.0 -WORKDIR /app -COPY --from=build /app . -ENTRYPOINT ["dotnet", "Acme.Sales.dll"] diff --git a/samples/ibmmq/ebcdic/IBMMQ_1/Sales/Ebcdic/EbcdicEnvelopeFeature.cs b/samples/ibmmq/ebcdic/IBMMQ_1/Sales/Ebcdic/EbcdicEnvelopeFeature.cs deleted file mode 100644 index 733c43e4f7d..00000000000 --- a/samples/ibmmq/ebcdic/IBMMQ_1/Sales/Ebcdic/EbcdicEnvelopeFeature.cs +++ /dev/null @@ -1,14 +0,0 @@ - -using System.Text; -using NServiceBus.Features; - -#region EbcdicEnvelopeFeature -sealed class EbcdicEnvelopeFeature : Feature -{ - protected override void Setup(FeatureConfigurationContext context) - { - Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); - context.AddEnvelopeHandler(); - } -} -#endregion diff --git a/samples/ibmmq/ebcdic/IBMMQ_1/Sales/Ebcdic/EbcdicEnvelopeHandler.cs b/samples/ibmmq/ebcdic/IBMMQ_1/Sales/Ebcdic/EbcdicEnvelopeHandler.cs deleted file mode 100644 index cd6f19b6ca7..00000000000 --- a/samples/ibmmq/ebcdic/IBMMQ_1/Sales/Ebcdic/EbcdicEnvelopeHandler.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System.Buffers; -using System.Buffers.Binary; -using System.Text; -using System.Text.Json; -using Contracts; -using NServiceBus.Extensibility; - -#region EbcdicEnvelopeHandler -sealed class EbcdicEnvelopeHandler : IEnvelopeHandler -{ - const int CODESET_EBCDIC_INT = 500; - static readonly Encoding Ebcdic = Encoding.GetEncoding(CODESET_EBCDIC_INT); - const int RecordLength = 70; - - public Dictionary? UnwrapEnvelope( - string nativeMessageId, - IDictionary incomingHeaders, - ReadOnlySpan incomingBody, - ContextBag extensions, - IBufferWriter bodyWriter) - { - if (incomingHeaders.ContainsKey("NServiceBus.EnclosedMessageTypes")) - return null; - - if (incomingBody.Length != RecordLength) - return null; - - // Parse fixed-length EBCDIC record - var orderId = Ebcdic.GetString(incomingBody[..36]); - var product = Ebcdic.GetString(incomingBody[36..66]).TrimEnd(); - var quantity = BinaryPrimitives.ReadInt32BigEndian(incomingBody[66..70]); - - // Write JSON body - var writer = new Utf8JsonWriter(bodyWriter); - writer.WriteStartObject(); - writer.WriteString("OrderId", orderId); - writer.WriteString("Product", product); - writer.WriteNumber("Quantity", quantity); - writer.WriteEndObject(); - writer.Flush(); - - var messageType = typeof(PlaceOrder); - - //MQMessage m; - return new Dictionary - { - ["NServiceBus.MessageId"] = nativeMessageId, - ["NServiceBus.ConversationId"] = nativeMessageId, - ["NServiceBus.EnclosedMessageTypes"] = messageType.FullName + ", " + messageType.Assembly.GetName().Name, - ["NServiceBus.ContentType"] = "application/json", - ["NServiceBus.MessageIntent"] = "Send", - ["NServiceBus.TimeSent"] = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss:ffffff") + " Z", - ["NServiceBus.ReplyToAddress"] = "Acme.LegacySender", - ["NServiceBus.OriginatingEndpoint"] = "LegacyMainframe", - ["NServiceBus.OriginatingMachine"] = "MAINFRAME", - }; - } -} -#endregion diff --git a/samples/ibmmq/ebcdic/IBMMQ_1/Sales/Ebcdic/EbcdicMutator.cs b/samples/ibmmq/ebcdic/IBMMQ_1/Sales/Ebcdic/EbcdicMutator.cs new file mode 100644 index 00000000000..876b6ea703d --- /dev/null +++ b/samples/ibmmq/ebcdic/IBMMQ_1/Sales/Ebcdic/EbcdicMutator.cs @@ -0,0 +1,63 @@ +using System.Buffers.Binary; +using System.Text; +using System.Text.Json; +using Contracts; +using NServiceBus.MessageMutator; + +#region EbcdicMutator +sealed class EbcdicMutator : IMutateIncomingTransportMessages +{ + const int CODESET_EBCDIC_INT = 500; + static readonly Encoding Ebcdic = Encoding.GetEncoding(CODESET_EBCDIC_INT); + const int RecordLength = 70; + + static EbcdicMutator() + { + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + } + + public Task MutateIncoming(MutateIncomingTransportMessageContext context) + { + if (context.Headers.ContainsKey("NServiceBus.EnclosedMessageTypes")) + return Task.CompletedTask; + + var body = context.Body.Span; + if (body.Length != RecordLength) + return Task.CompletedTask; + + // Parse fixed-length EBCDIC record + var orderId = Ebcdic.GetString(body[..36]); + var product = Ebcdic.GetString(body[36..66]).TrimEnd(); + var quantity = BinaryPrimitives.ReadInt32BigEndian(body[66..70]); + + // Write JSON body + using var ms = new MemoryStream(); + using (var writer = new Utf8JsonWriter(ms)) + { + writer.WriteStartObject(); + writer.WriteString("OrderId", orderId); + writer.WriteString("Product", product); + writer.WriteNumber("Quantity", quantity); + writer.WriteEndObject(); + } + context.Body = new ReadOnlyMemory(ms.ToArray()); + + var messageType = typeof(PlaceOrder); + var messageId = context.Headers.TryGetValue("NServiceBus.MessageId", out var existingId) + ? existingId + : Guid.NewGuid().ToString(); + + context.Headers["NServiceBus.MessageId"] = messageId; + context.Headers["NServiceBus.ConversationId"] = messageId; + context.Headers["NServiceBus.EnclosedMessageTypes"] = messageType.FullName + ", " + messageType.Assembly.GetName().Name; + context.Headers["NServiceBus.ContentType"] = "application/json"; + context.Headers["NServiceBus.MessageIntent"] = "Send"; + context.Headers["NServiceBus.TimeSent"] = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss:ffffff") + " Z"; + context.Headers["NServiceBus.ReplyToAddress"] = "Acme.LegacySender"; + context.Headers["NServiceBus.OriginatingEndpoint"] = "LegacyMainframe"; + context.Headers["NServiceBus.OriginatingMachine"] = "MAINFRAME"; + + return Task.CompletedTask; + } +} +#endregion diff --git a/samples/ibmmq/ebcdic/IBMMQ_1/Sales/Program.cs b/samples/ibmmq/ebcdic/IBMMQ_1/Sales/Program.cs index a573c77de22..5f9a3262ff3 100644 --- a/samples/ibmmq/ebcdic/IBMMQ_1/Sales/Program.cs +++ b/samples/ibmmq/ebcdic/IBMMQ_1/Sales/Program.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.Hosting; using NServiceBus; +using NServiceBus.MessageMutator; using NServiceBus.Transport.IBMMQ; var host = Host.CreateDefaultBuilder(args) @@ -18,16 +19,13 @@ o.Channel = "APP.SVRCONN"; o.User = "sales"; }); + #region EbcdicMutatorRegistration + endpointConfiguration.RegisterMessageMutator(new EbcdicMutator()); + #endregion var routing = endpointConfiguration.UseTransport(transport); endpointConfiguration.EnableInstallers(); - - endpointConfiguration.EnableFeature(); - - endpointConfiguration.AuditProcessedMessagesTo("audit"); - endpointConfiguration.SendFailedMessagesTo("error"); - return endpointConfiguration; }) .Build(); diff --git a/samples/ibmmq/ebcdic/IBMMQ_1/Sales/Sales.csproj b/samples/ibmmq/ebcdic/IBMMQ_1/Sales/Sales.csproj index 73fcc7e9ee3..f9ced1b8024 100644 --- a/samples/ibmmq/ebcdic/IBMMQ_1/Sales/Sales.csproj +++ b/samples/ibmmq/ebcdic/IBMMQ_1/Sales/Sales.csproj @@ -10,7 +10,7 @@ - + diff --git a/samples/ibmmq/ebcdic/sample.md b/samples/ibmmq/ebcdic/sample.md index ab70c640261..ad1c6f6a328 100644 --- a/samples/ibmmq/ebcdic/sample.md +++ b/samples/ibmmq/ebcdic/sample.md @@ -6,7 +6,7 @@ component: Core related: - transports/ibmmq - transports/ibmmq/native-integration -- nservicebus/pipeline/features +- nservicebus/pipeline/message-mutators - nservicebus/messaging/headers - nservicebus/serialization/system-json --- @@ -22,7 +22,7 @@ The sample includes: Mainframe systems often communicate using EBCDIC encoding and fixed-length binary record layouts rather than JSON or XML. Such messages carry no [NServiceBus headers](/nservicebus/messaging/headers.md) and cannot be deserialized by the endpoint without prior transformation. -The IBM MQ transport exposes an envelope handler extension point (`IEnvelopeHandler`) that intercepts raw incoming messages before they reach the [message handler pipeline](/nservicebus/pipeline/). The envelope handler can inspect the raw bytes, decode them, and return a rewritten body and a set of headers - making the transformed message indistinguishable from one sent natively by NServiceBus. +The NServiceBus [message mutator](/nservicebus/pipeline/message-mutators.md) pipeline extension point intercepts raw incoming messages before they reach the [message handler pipeline](/nservicebus/pipeline/index.md). An incoming transport message mutator (`IMutateIncomingTransportMessages`) can inspect the raw bytes, decode them, and replace the message body and headers - making the transformed message indistinguishable from one sent natively by NServiceBus. ## Running the sample @@ -44,31 +44,24 @@ The record layout is: The `MQMessage.CharacterSet` is set to `500` (EBCDIC) and `MQMessage.Format` is set to `MQFMT_NONE`, indicating raw binary content with no broker-managed structured headers. -### Enabling the envelope handler +### Registering the mutator -`EbcdicEnvelopeFeature` is a custom [NServiceBus feature](/nservicebus/pipeline/features.md) that registers the EBCDIC code page provider and wires up the envelope handler: +`EbcdicMutator` is registered directly in endpoint configuration: -snippet: EbcdicEnvelopeFeature +snippet: EbcdicMutatorRegistration -Enable the feature in endpoint configuration: +### Decoding the EBCDIC message -```csharp -endpointConfiguration.EnableFeature(); -``` +`EbcdicMutator` implements `IMutateIncomingTransportMessages`. It is called for every incoming message before the message is dispatched to a [message handler](/nservicebus/handlers/). -### Decoding the EBCDIC envelope +snippet: EbcdicMutator -`EbcdicEnvelopeHandler` implements `IEnvelopeHandler`. It is called for every incoming message before the message is dispatched to a [message handler](/nservicebus/handlers/). +The mutator: -snippet: EbcdicEnvelopeHandler - -The handler: - -1. Returns `null` for messages that already carry `NServiceBus.EnclosedMessageTypes` - those are standard NServiceBus messages that require no transformation. -2. Returns `null` for records that are not exactly 70 bytes - allowing the message to be handled by any other registered envelope handler or failing with an appropriate error. -3. Decodes the three fixed-length fields from EBCDIC bytes into .NET types using `Encoding.GetEncoding(500)` and `BinaryPrimitives.ReadInt32BigEndian`. -4. Writes a JSON body into the provided `IBufferWriter`, compatible with the [`SystemJsonSerializer`](/nservicebus/serialization/system-json.md) configured on the endpoint. -5. Returns a [headers](/nservicebus/messaging/headers.md) dictionary that provides all the NServiceBus routing metadata the pipeline requires: message type, content type, intent, reply-to address, and originating endpoint. +1. Returns early for records that are not exactly 70 bytes - allowing the message to proceed unchanged. +2. Decodes the three fixed-length fields from EBCDIC bytes into .NET types using `Encoding.GetEncoding(500)` and `BinaryPrimitives.ReadInt32BigEndian`. +3. Replaces `context.Body` with a JSON-encoded body compatible with the [`SystemJsonSerializer`](/nservicebus/serialization/system-json.md) configured on the endpoint. +4. Adds [headers](/nservicebus/messaging/headers.md) to `context.Headers` that provide all the NServiceBus routing metadata the pipeline requires: message type, content type, intent, reply-to address, and originating endpoint. ### Message handler @@ -76,4 +69,4 @@ The handler: snippet: PlaceOrderHandler -The handler [replies](/nservicebus/messaging/reply-to-a-message.md) to the originating address (`Acme.LegacySender` as set in the envelope headers) and [publishes](/nservicebus/messaging/publish-subscribe/) an `OrderPlaced` event. +The handler [replies](/nservicebus/messaging/reply-to-a-message.md) to the originating address (`Acme.LegacySender` as set in the mutator headers) and [publishes](/nservicebus/messaging/publish-subscribe/) an `OrderPlaced` event. diff --git a/samples/ibmmq/polymorphic-events/IBMMQ_2/Orders/Orders.csproj b/samples/ibmmq/polymorphic-events/IBMMQ_2/Orders/Orders.csproj new file mode 100644 index 00000000000..d58acadc7bb --- /dev/null +++ b/samples/ibmmq/polymorphic-events/IBMMQ_2/Orders/Orders.csproj @@ -0,0 +1,20 @@ + + + + Exe + net10.0 + enable + enable + + + + + + + + + + + + + diff --git a/samples/ibmmq/polymorphic-events/IBMMQ_2/Orders/Program.cs b/samples/ibmmq/polymorphic-events/IBMMQ_2/Orders/Program.cs new file mode 100644 index 00000000000..b5706de20e2 --- /dev/null +++ b/samples/ibmmq/polymorphic-events/IBMMQ_2/Orders/Program.cs @@ -0,0 +1,58 @@ +using System.Net; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using NServiceBus.Transport.IBMMQ; + +Console.Title = "Orders"; +var builder = Host.CreateApplicationBuilder(args); + +var ibmmq = new IBMMQTransport() +{ + QueueManagerName = "QM1", + Host = "localhost", + Port = 1414, + Channel = "DEV.ADMIN.SVRCONN", + User = "admin", + Password = "passw0rd" +}; + +var endpointConfiguration = new EndpointConfiguration("Orders"); +endpointConfiguration.SendFailedMessagesTo("error"); +endpointConfiguration.UseTransport(ibmmq); +endpointConfiguration.Recoverability().Delayed(settings => settings.NumberOfRetries(0)); + +endpointConfiguration.UseSerialization(); +endpointConfiguration.EnableInstallers(); +builder.UseNServiceBus(endpointConfiguration); + +var host = builder.Build(); +await host.StartAsync(); + +var session = host.Services.GetRequiredService(); + +Console.WriteLine("Press [O] for a standard order, [E] for an express order, [Q] to quit."); + +while (true) +{ + var key = Console.ReadKey(true); + + if (key.Key == ConsoleKey.Q) + break; + + var orderId = Guid.CreateVersion7(); + + #region PublishOrders + if (key.Key == ConsoleKey.E) + { + await session.Publish(new ExpressOrderPlaced(orderId, "Widget")); + Console.WriteLine($"Published ExpressOrderPlaced {orderId}"); + } + else if (key.Key == ConsoleKey.O) + { + await session.Publish(new OrderPlaced(orderId, "Widget")); + Console.WriteLine($"Published OrderPlaced {orderId}"); + } + #endregion +} + +await host.StopAsync(); diff --git a/samples/ibmmq/polymorphic-events/IBMMQ_2/Shared/Messages.cs b/samples/ibmmq/polymorphic-events/IBMMQ_2/Shared/Messages.cs new file mode 100644 index 00000000000..bc6cac4e701 --- /dev/null +++ b/samples/ibmmq/polymorphic-events/IBMMQ_2/Shared/Messages.cs @@ -0,0 +1,6 @@ +#region Events +public record OrderPlaced(Guid OrderId, string Product) : IEvent; + +public record ExpressOrderPlaced(Guid OrderId, string Product) + : OrderPlaced(OrderId, Product); +#endregion diff --git a/samples/ibmmq/polymorphic-events/IBMMQ_2/Shared/Shared.csproj b/samples/ibmmq/polymorphic-events/IBMMQ_2/Shared/Shared.csproj new file mode 100644 index 00000000000..18940df908b --- /dev/null +++ b/samples/ibmmq/polymorphic-events/IBMMQ_2/Shared/Shared.csproj @@ -0,0 +1,13 @@ + + + + net10.0 + enable + enable + + + + + + + diff --git a/samples/ibmmq/polymorphic-events/IBMMQ_2/Shipping/OrderPlacedHandler.cs b/samples/ibmmq/polymorphic-events/IBMMQ_2/Shipping/OrderPlacedHandler.cs new file mode 100644 index 00000000000..da121644aa7 --- /dev/null +++ b/samples/ibmmq/polymorphic-events/IBMMQ_2/Shipping/OrderPlacedHandler.cs @@ -0,0 +1,11 @@ +#region OrderPlacedHandler +sealed class OrderPlacedHandler : IHandleMessages +{ + public Task Handle(OrderPlaced message, IMessageHandlerContext context) + { + var messageType = message.GetType().Name; + Console.WriteLine($"Received {messageType}: OrderId={message.OrderId}, Product={message.Product}"); + return Task.CompletedTask; + } +} +#endregion diff --git a/samples/ibmmq/polymorphic-events/IBMMQ_2/Shipping/Program.cs b/samples/ibmmq/polymorphic-events/IBMMQ_2/Shipping/Program.cs new file mode 100644 index 00000000000..c7b21ac73f0 --- /dev/null +++ b/samples/ibmmq/polymorphic-events/IBMMQ_2/Shipping/Program.cs @@ -0,0 +1,27 @@ +using Microsoft.Extensions.Hosting; +using NServiceBus.Transport.IBMMQ; + +Console.Title = "Shipping"; +var builder = Host.CreateApplicationBuilder(args); + +var ibmmq = new IBMMQTransport() +{ + QueueManagerName = "QM1", + Host = "localhost", + Port = 1414, + Channel = "DEV.ADMIN.SVRCONN", + User = "admin", + Password = "passw0rd" +}; + +var endpointConfiguration = new EndpointConfiguration("Shipping"); +endpointConfiguration.SendFailedMessagesTo("error"); +endpointConfiguration.UseTransport(ibmmq); +endpointConfiguration.UseSerialization(); +endpointConfiguration.Recoverability().Delayed(settings => settings.NumberOfRetries(0)); + +endpointConfiguration.EnableInstallers(); +builder.UseNServiceBus(endpointConfiguration); + +var host = builder.Build(); +await host.RunAsync(); diff --git a/samples/ibmmq/polymorphic-events/IBMMQ_2/Shipping/Shipping.csproj b/samples/ibmmq/polymorphic-events/IBMMQ_2/Shipping/Shipping.csproj new file mode 100644 index 00000000000..d58acadc7bb --- /dev/null +++ b/samples/ibmmq/polymorphic-events/IBMMQ_2/Shipping/Shipping.csproj @@ -0,0 +1,20 @@ + + + + Exe + net10.0 + enable + enable + + + + + + + + + + + + + diff --git a/samples/ibmmq/polymorphic-events/IBMMQ_2/docker-compose.yml b/samples/ibmmq/polymorphic-events/IBMMQ_2/docker-compose.yml new file mode 100644 index 00000000000..1b7d15dee55 --- /dev/null +++ b/samples/ibmmq/polymorphic-events/IBMMQ_2/docker-compose.yml @@ -0,0 +1,19 @@ +services: + ibm-mq: + image: icr.io/ibm-messaging/mq:latest + container_name: ibm-mq + ports: + - "1414:1414" # MQ listener port + - "9443:9443" # Web console + environment: + LICENSE: accept + MQ_QMGR_NAME: QM1 + MQ_ADMIN_PASSWORD: passw0rd + healthcheck: + test: ["CMD", "dspmq"] + interval: 10s + retries: 5 + start_period: 10s + timeout: 5s + +#management console address: https://localhost:9443/ibmmq/console diff --git a/samples/ibmmq/polymorphic-events/IBMMQ_2/prerelease.txt b/samples/ibmmq/polymorphic-events/IBMMQ_2/prerelease.txt new file mode 100644 index 00000000000..e69de29bb2d diff --git a/samples/ibmmq/polymorphic-events/IBMMQ_2/sample.slnx b/samples/ibmmq/polymorphic-events/IBMMQ_2/sample.slnx new file mode 100644 index 00000000000..75ac85a493b --- /dev/null +++ b/samples/ibmmq/polymorphic-events/IBMMQ_2/sample.slnx @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/samples/ibmmq/polymorphic-events/sample.md b/samples/ibmmq/polymorphic-events/sample.md new file mode 100644 index 00000000000..0551ded5f68 --- /dev/null +++ b/samples/ibmmq/polymorphic-events/sample.md @@ -0,0 +1,53 @@ +--- +title: IBM MQ polymorphic event routing +summary: Demonstrating how publishing a derived event type delivers to subscribers of the base event type. +reviewed: 2026-03-24 +component: IBMMQ +related: +- transports/ibmmq +- nservicebus/messaging/publish-subscribe +- nservicebus/messaging/messages-events-commands +--- + +This sample demonstrates polymorphic event routing with the IBM MQ transport. A **Orders** endpoint publishes either an `OrderPlaced` or an `ExpressOrderPlaced` event. The **Shipping** endpoint subscribes only to `OrderPlaced` - yet receives both event types, because `ExpressOrderPlaced` inherits from `OrderPlaced`. + +## How it works + +NServiceBus includes all types in the .NET inheritance chain in the `NServiceBus.EnclosedMessageTypes` message header when publishing. Subscribers registered for any type in that chain receive the message. Publishing `ExpressOrderPlaced` therefore dispatches to all `IHandleMessages` handlers with no changes required in the subscriber. + +## Prerequisites + +The sample requires a running IBM MQ broker. A Docker Compose file is included: + +```bash +docker compose up -d +``` + +This starts IBM MQ with queue manager `QM1` on port `1414`. The management console is available at `https://localhost:9443/ibmmq/console` (credentials: `admin` / `passw0rd`). + +## Running the sample + +1. Start **Shipping** first. `EnableInstallers()` creates its queue and registers its subscription with the broker. +2. Start **Orders**. +3. Press `O` - Shipping logs `Received OrderPlaced`. +4. Press `E` - Shipping logs `Received ExpressOrderPlaced`, delivered via the `OrderPlaced` subscription. + +## Code walk-through + +### Event hierarchy + +snippet: Events + +`ExpressOrderPlaced` inherits from `OrderPlaced` using C# record inheritance. No additional NServiceBus configuration is required. + +### Publishing + +snippet: PublishOrders + +Both events are published with the same `session.Publish` call. NServiceBus inspects the runtime type and populates `NServiceBus.EnclosedMessageTypes` with the full type chain automatically. + +### Subscriber handler + +snippet: OrderPlacedHandler + +The handler is registered for `OrderPlaced` only. Logging `message.GetType().Name` confirms the full derived type is preserved through delivery - the handler receives an `ExpressOrderPlaced` instance, not a downcast `OrderPlaced`. diff --git a/samples/ibmmq/request-reply/IBMMQ_2/Client/Client.csproj b/samples/ibmmq/request-reply/IBMMQ_2/Client/Client.csproj new file mode 100644 index 00000000000..d58acadc7bb --- /dev/null +++ b/samples/ibmmq/request-reply/IBMMQ_2/Client/Client.csproj @@ -0,0 +1,20 @@ + + + + Exe + net10.0 + enable + enable + + + + + + + + + + + + + diff --git a/samples/ibmmq/request-reply/IBMMQ_2/Client/OrderResponseHandler.cs b/samples/ibmmq/request-reply/IBMMQ_2/Client/OrderResponseHandler.cs new file mode 100644 index 00000000000..725f41eacfa --- /dev/null +++ b/samples/ibmmq/request-reply/IBMMQ_2/Client/OrderResponseHandler.cs @@ -0,0 +1,10 @@ +#region OrderResponseHandler +sealed class OrderResponseHandler : IHandleMessages +{ + public Task Handle(PlaceOrderResponse message, IMessageHandlerContext context) + { + Console.WriteLine($"Order {message.OrderId} status: {message.Status}"); + return Task.CompletedTask; + } +} +#endregion diff --git a/samples/ibmmq/request-reply/IBMMQ_2/Client/Program.cs b/samples/ibmmq/request-reply/IBMMQ_2/Client/Program.cs new file mode 100644 index 00000000000..02448de5aa5 --- /dev/null +++ b/samples/ibmmq/request-reply/IBMMQ_2/Client/Program.cs @@ -0,0 +1,58 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using NServiceBus.Transport.IBMMQ; + +Console.Title = "Client"; +var builder = Host.CreateApplicationBuilder(args); + +#region ClientConfig +var ibmmq = new IBMMQTransport() +{ + QueueManagerName = "QM1", + Host = "localhost", + Port = 1414, + Channel = "DEV.ADMIN.SVRCONN", + User = "admin", + Password = "passw0rd" +}; + +var endpointConfiguration = new EndpointConfiguration("DEV.CLIENT"); +endpointConfiguration.SendFailedMessagesTo("error"); +endpointConfiguration.UseTransport(ibmmq); +endpointConfiguration.UseSerialization(); +endpointConfiguration.Recoverability().Delayed(settings => settings.NumberOfRetries(0)); +endpointConfiguration.EnableInstallers(); +builder.UseNServiceBus(endpointConfiguration); +#endregion + +var host = builder.Build(); +await host.StartAsync(); + +var session = host.Services.GetRequiredService(); + +using var cts = new CancellationTokenSource(); +Console.CancelKeyPress += (_, e) => +{ + e.Cancel = true; + cts.Cancel(); +}; + +Console.WriteLine("Press Enter to place an order. Ctrl+C to exit."); + +while (!cts.IsCancellationRequested) +{ + var readLineTask = Task.Run(Console.ReadLine, cts.Token); + _ = await Task.WhenAny(readLineTask, Task.Delay(Timeout.Infinite, cts.Token)); + + if (cts.IsCancellationRequested) + break; + + var orderId = Guid.CreateVersion7(); + Console.WriteLine($"Sending request for order {orderId}"); + + #region SendRequest + await session.Send("DEV.SERVER", new PlaceOrderRequest(orderId, "Widget")); + #endregion +} + +await host.StopAsync(); diff --git a/samples/ibmmq/request-reply/IBMMQ_2/Server/PlaceOrderHandler.cs b/samples/ibmmq/request-reply/IBMMQ_2/Server/PlaceOrderHandler.cs new file mode 100644 index 00000000000..0c89516a0ad --- /dev/null +++ b/samples/ibmmq/request-reply/IBMMQ_2/Server/PlaceOrderHandler.cs @@ -0,0 +1,14 @@ +#region PlaceOrderHandler +sealed class PlaceOrderHandler : IHandleMessages +{ + public async Task Handle(PlaceOrderRequest message, IMessageHandlerContext context) + { + Console.WriteLine($"Received order {message.OrderId} for '{message.Product}'"); + + var response = new PlaceOrderResponse(message.OrderId, "Accepted"); + await context.Reply(response); + + Console.WriteLine($"Replied to order {message.OrderId}"); + } +} +#endregion diff --git a/samples/ibmmq/request-reply/IBMMQ_2/Server/Program.cs b/samples/ibmmq/request-reply/IBMMQ_2/Server/Program.cs new file mode 100644 index 00000000000..a73e091d6a7 --- /dev/null +++ b/samples/ibmmq/request-reply/IBMMQ_2/Server/Program.cs @@ -0,0 +1,27 @@ +using System.Net; +using Microsoft.Extensions.Hosting; +using NServiceBus.Transport.IBMMQ; + +Console.Title = "Server"; +var builder = Host.CreateApplicationBuilder(args); + +var ibmmq = new IBMMQTransport() +{ + QueueManagerName = "QM1", + Host = "localhost", + Port = 1414, + Channel = "DEV.ADMIN.SVRCONN", + User = "admin", + Password = "passw0rd" +}; + +var endpointConfiguration = new EndpointConfiguration("DEV.SERVER"); +endpointConfiguration.SendFailedMessagesTo("error"); +endpointConfiguration.UseTransport(ibmmq); +endpointConfiguration.Recoverability().Delayed(settings => settings.NumberOfRetries(0)); +endpointConfiguration.UseSerialization(); +endpointConfiguration.EnableInstallers(); +builder.UseNServiceBus(endpointConfiguration); + +var host = builder.Build(); +await host.RunAsync(); diff --git a/samples/ibmmq/request-reply/IBMMQ_2/Server/Server.csproj b/samples/ibmmq/request-reply/IBMMQ_2/Server/Server.csproj new file mode 100644 index 00000000000..d58acadc7bb --- /dev/null +++ b/samples/ibmmq/request-reply/IBMMQ_2/Server/Server.csproj @@ -0,0 +1,20 @@ + + + + Exe + net10.0 + enable + enable + + + + + + + + + + + + + diff --git a/samples/ibmmq/request-reply/IBMMQ_2/Shared/PlaceOrderRequest.cs b/samples/ibmmq/request-reply/IBMMQ_2/Shared/PlaceOrderRequest.cs new file mode 100644 index 00000000000..7f45545f6a6 --- /dev/null +++ b/samples/ibmmq/request-reply/IBMMQ_2/Shared/PlaceOrderRequest.cs @@ -0,0 +1,5 @@ +#region Messages +public record PlaceOrderRequest(Guid OrderId, string Product) : IMessage; + +public record PlaceOrderResponse(Guid OrderId, string Status) : IMessage; +#endregion diff --git a/samples/ibmmq/request-reply/IBMMQ_2/Shared/Shared.csproj b/samples/ibmmq/request-reply/IBMMQ_2/Shared/Shared.csproj new file mode 100644 index 00000000000..18940df908b --- /dev/null +++ b/samples/ibmmq/request-reply/IBMMQ_2/Shared/Shared.csproj @@ -0,0 +1,13 @@ + + + + net10.0 + enable + enable + + + + + + + diff --git a/samples/ibmmq/request-reply/IBMMQ_2/docker-compose.yml b/samples/ibmmq/request-reply/IBMMQ_2/docker-compose.yml new file mode 100644 index 00000000000..1b7d15dee55 --- /dev/null +++ b/samples/ibmmq/request-reply/IBMMQ_2/docker-compose.yml @@ -0,0 +1,19 @@ +services: + ibm-mq: + image: icr.io/ibm-messaging/mq:latest + container_name: ibm-mq + ports: + - "1414:1414" # MQ listener port + - "9443:9443" # Web console + environment: + LICENSE: accept + MQ_QMGR_NAME: QM1 + MQ_ADMIN_PASSWORD: passw0rd + healthcheck: + test: ["CMD", "dspmq"] + interval: 10s + retries: 5 + start_period: 10s + timeout: 5s + +#management console address: https://localhost:9443/ibmmq/console diff --git a/samples/ibmmq/request-reply/IBMMQ_2/prerelease.txt b/samples/ibmmq/request-reply/IBMMQ_2/prerelease.txt new file mode 100644 index 00000000000..e69de29bb2d diff --git a/samples/ibmmq/request-reply/IBMMQ_2/sample.slnx b/samples/ibmmq/request-reply/IBMMQ_2/sample.slnx new file mode 100644 index 00000000000..69954f1ca6d --- /dev/null +++ b/samples/ibmmq/request-reply/IBMMQ_2/sample.slnx @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/samples/ibmmq/request-reply/sample.md b/samples/ibmmq/request-reply/sample.md new file mode 100644 index 00000000000..dadc87dd8aa --- /dev/null +++ b/samples/ibmmq/request-reply/sample.md @@ -0,0 +1,69 @@ +--- +title: IBM MQ request/reply +summary: Sending a request message and receiving a reply using the IBM MQ transport. +reviewed: 2026-03-24 +component: IBMMQ +related: +- transports/ibmmq +- transports/ibmmq/connection-settings +- nservicebus/messaging/reply-to-a-message +--- + +This sample demonstrates the request/reply messaging pattern using the IBM MQ transport. A **Client** endpoint sends a `PlaceOrderRequest` command to a **Server** endpoint, which processes it and replies with a `PlaceOrderResponse`. The Client receives the response in a dedicated message handler. + +The sample includes: + +- A **Client** console application that sends order requests and handles the responses +- A **Server** console application that processes incoming requests and replies to the sender +- A **Shared** library containing the message contracts + +## Prerequisites + +The sample requires a running IBM MQ broker. A Docker Compose file is included: + +```bash +docker compose up -d +``` + +This starts IBM MQ with queue manager `QM1` on port `1414`. The management console is available at `https://localhost:9443/ibmmq/console` (credentials: `admin` / `passw0rd`). + +## Running the sample + +1. Start the **Server** project first. It calls `EnableInstallers()` on startup to create the `DEV.SERVER` queue. +2. Start the **Client** project. It calls `EnableInstallers()` to create the `DEV.CLIENT` queue. +3. Press Enter in the Client console. A `PlaceOrderRequest` is sent to the Server. +4. The Server logs the received order and replies. The Client logs the response status. + +## Code walk-through + +### Message contracts + +snippet: Messages + +Both messages are C# records implementing `IMessage`. `PlaceOrderRequest` carries an `OrderId` and `Product`. `PlaceOrderResponse` carries the same `OrderId` and a `Status` string so the Client can correlate the response to its original request. + +### Client configuration + +The Client endpoint needs a receive queue so it can accept reply messages - it is not configured as send-only: + +snippet: ClientConfig + +`EnableInstallers()` creates the `DEV.CLIENT` queue on startup if it does not already exist. + +### Sending the request + +snippet: SendRequest + +`Send` targets the `DEV.SERVER` queue directly. NServiceBus automatically sets the `ReplyTo` header to `DEV.CLIENT`, so the Server knows where to send the response without any additional configuration. + +### Server handler + +snippet: PlaceOrderHandler + +`context.Reply` sends the response back to the address in the incoming message's `ReplyTo` header - in this case `DEV.CLIENT`. No explicit destination address is required. + +### Response handler + +snippet: OrderResponseHandler + +`OrderResponseHandler` on the Client receives the `PlaceOrderResponse` and logs the order status. It is a standard `IHandleMessages` implementation with no awareness of IBM MQ. diff --git a/samples/ibmmq/simple/IBMMQ_2/Receiver/MyHandler.cs b/samples/ibmmq/simple/IBMMQ_2/Receiver/MyHandler.cs new file mode 100644 index 00000000000..a3c7237a04d --- /dev/null +++ b/samples/ibmmq/simple/IBMMQ_2/Receiver/MyHandler.cs @@ -0,0 +1,21 @@ + +#region MyHandler +sealed class MyHandler : IHandleMessages +{ + public async Task Handle(MyMessage message, IMessageHandlerContext context) + { + Console.WriteLine($"Start {message.Data}"); + await context.SendLocal(new MyMessage2()); + await context.SendLocal(new MyMessage2()); + await Task.Delay(200, context.CancellationToken); + Console.WriteLine($"End: {message.Data}"); + } +} +sealed class MyHandler2 : IHandleMessages +{ + public async Task Handle(MyMessage2 message, IMessageHandlerContext context) + { + Console.WriteLine($"MyMessage2"); + } +} +#endregion \ No newline at end of file diff --git a/samples/ibmmq/simple/IBMMQ_2/Receiver/Program.cs b/samples/ibmmq/simple/IBMMQ_2/Receiver/Program.cs new file mode 100644 index 00000000000..a18d4268bc3 --- /dev/null +++ b/samples/ibmmq/simple/IBMMQ_2/Receiver/Program.cs @@ -0,0 +1,30 @@ +using Microsoft.Extensions.Hosting; +using NServiceBus.Transport.IBMMQ; + +Console.Title = "Receiver"; +var builder = Host.CreateApplicationBuilder(args); + +var ibmmq = new IBMMQTransport() +{ + QueueManagerName = "QM1", + Host = "localhost", + Port = 1414, + Channel = "DEV.ADMIN.SVRCONN", + User = "admin", + Password = "passw0rd" +}; + +#region ReceiverConfig +var endpointB = new EndpointConfiguration("DEV.SHIPPING"); +endpointB.UseTransport(ibmmq); +endpointB.UseSerialization(); +endpointB.PurgeOnStartup(true); +endpointB.LimitMessageProcessingConcurrencyTo(2); +endpointB.EnableInstallers(); +endpointB.Recoverability().Delayed(settings => settings.NumberOfRetries(0)); +builder.UseNServiceBus(endpointB); +#endregion + +var host = builder.Build(); + +await host.RunAsync(); \ No newline at end of file diff --git a/samples/ibmmq/simple/IBMMQ_2/Receiver/Properties/launchSettings.json b/samples/ibmmq/simple/IBMMQ_2/Receiver/Properties/launchSettings.json new file mode 100644 index 00000000000..f981b6f91c8 --- /dev/null +++ b/samples/ibmmq/simple/IBMMQ_2/Receiver/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "FullTransportEndpoint.Subscriber": { + "commandName": "Project", + "dotnetRunMessages": true, + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + } + } + } +} diff --git a/samples/ibmmq/simple/IBMMQ_2/Receiver/Receiver.csproj b/samples/ibmmq/simple/IBMMQ_2/Receiver/Receiver.csproj new file mode 100644 index 00000000000..c2daee5d5c6 --- /dev/null +++ b/samples/ibmmq/simple/IBMMQ_2/Receiver/Receiver.csproj @@ -0,0 +1,25 @@ + + + + Exe + net10.0 + enable + enable + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/ibmmq/simple/IBMMQ_2/Sender/Program.cs b/samples/ibmmq/simple/IBMMQ_2/Sender/Program.cs new file mode 100644 index 00000000000..f3457227ce8 --- /dev/null +++ b/samples/ibmmq/simple/IBMMQ_2/Sender/Program.cs @@ -0,0 +1,78 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using NServiceBus; +using NServiceBus.Transport.IBMMQ; + +Console.Title = "Sender"; +var builder = Host.CreateApplicationBuilder(args); + +#region SenderConfig +var ibmmq = new IBMMQTransport() +{ + QueueManagerName = "QM1", + Host = "localhost", + Port = 1414, + Channel = "DEV.ADMIN.SVRCONN", + User = "admin", + Password = "passw0rd" +}; + +var endpointB = new EndpointConfiguration("DEV.SHIPPING"); +endpointB.SendFailedMessagesTo("error"); +endpointB.UseTransport(ibmmq); +endpointB.UseSerialization(); +endpointB.PurgeOnStartup(true); +endpointB.SendOnly(); + +builder.UseNServiceBus(endpointB); +#endregion + +var host = builder.Build(); + +await host.StartAsync(); + +var instance = host.Services.GetRequiredService(); + + +using var cts = new CancellationTokenSource(); +Console.CancelKeyPress += (_, e) => +{ + e.Cancel = true; + cts.Cancel(); +}; + +var instanceId = Guid.CreateVersion7(); +long sendCount = 0; + +while (!cts.IsCancellationRequested) +{ + Console.Write("\nHow many message to send: "); + + var readLineTask = Task.Run(Console.ReadLine, cts.Token); + _ = await Task.WhenAny(readLineTask, Task.Delay(Timeout.Infinite, cts.Token)); + + if (cts.IsCancellationRequested) + { + break; + } + + var input = await readLineTask; + var x = int.TryParse(input, out var nrInt) ? nrInt : 1; + Console.WriteLine(); + + var t = new List(); + + for (int i = 0; i < x; i++) + { + var data = $"{instanceId}/{++sendCount}"; + Console.WriteLine($"Sending message: {data}"); + t.Add(instance.Send("DEV.SHIPPING", new MyMessage(data))); + } + + await Task.WhenAll(t); + Console.WriteLine("Done"); +} + + +await host.StopAsync(); + diff --git a/samples/ibmmq/simple/IBMMQ_2/Sender/Properties/launchSettings.json b/samples/ibmmq/simple/IBMMQ_2/Sender/Properties/launchSettings.json new file mode 100644 index 00000000000..c238eae6ed2 --- /dev/null +++ b/samples/ibmmq/simple/IBMMQ_2/Sender/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "FullTransportEndpoint": { + "commandName": "Project", + "dotnetRunMessages": true, + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + } + } + } +} diff --git a/samples/ibmmq/simple/IBMMQ_2/Sender/Sender.csproj b/samples/ibmmq/simple/IBMMQ_2/Sender/Sender.csproj new file mode 100644 index 00000000000..c2daee5d5c6 --- /dev/null +++ b/samples/ibmmq/simple/IBMMQ_2/Sender/Sender.csproj @@ -0,0 +1,25 @@ + + + + Exe + net10.0 + enable + enable + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/ibmmq/simple/IBMMQ_2/Sender/appsettings.Development.json b/samples/ibmmq/simple/IBMMQ_2/Sender/appsettings.Development.json new file mode 100644 index 00000000000..3c98c4159b0 --- /dev/null +++ b/samples/ibmmq/simple/IBMMQ_2/Sender/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "NServiceBus.Transport.IbmMq": "Trace", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/samples/ibmmq/simple/IBMMQ_2/Sender/appsettings.json b/samples/ibmmq/simple/IBMMQ_2/Sender/appsettings.json new file mode 100644 index 00000000000..b2dcdb67421 --- /dev/null +++ b/samples/ibmmq/simple/IBMMQ_2/Sender/appsettings.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/samples/ibmmq/simple/IBMMQ_2/Shared/MyMessage.cs b/samples/ibmmq/simple/IBMMQ_2/Shared/MyMessage.cs new file mode 100644 index 00000000000..9fe449163d9 --- /dev/null +++ b/samples/ibmmq/simple/IBMMQ_2/Shared/MyMessage.cs @@ -0,0 +1,3 @@ +public record MyMessage(string Data) : IMessage; + +public record MyMessage2 : IMessage; \ No newline at end of file diff --git a/samples/ibmmq/simple/IBMMQ_2/Shared/Shared.csproj b/samples/ibmmq/simple/IBMMQ_2/Shared/Shared.csproj new file mode 100644 index 00000000000..9e7e5378532 --- /dev/null +++ b/samples/ibmmq/simple/IBMMQ_2/Shared/Shared.csproj @@ -0,0 +1,14 @@ + + + + net10.0 + enable + enable + Messages + + + + + + + diff --git a/samples/ibmmq/simple/IBMMQ_2/docker-compose.yml b/samples/ibmmq/simple/IBMMQ_2/docker-compose.yml new file mode 100644 index 00000000000..1498322c4a2 --- /dev/null +++ b/samples/ibmmq/simple/IBMMQ_2/docker-compose.yml @@ -0,0 +1,21 @@ + services: + ibm-mq: + image: icr.io/ibm-messaging/mq:latest + container_name: ibm-mq + ports: + - "1414:1414" # MQ listener port + - "9443:9443" # Web console + environment: + LICENSE: accept + MQ_QMGR_NAME: QM1 + MQ_ADMIN_PASSWORD: passw0rd + healthcheck: + test: ["CMD", "dspmq"] + interval: 10s + retries: 5 + start_period: 10s + timeout: 5s + + + +#management console address: https://localhost:9443/ibmmq/console diff --git a/samples/ibmmq/simple/IBMMQ_2/prerelease.txt b/samples/ibmmq/simple/IBMMQ_2/prerelease.txt new file mode 100644 index 00000000000..e69de29bb2d diff --git a/samples/ibmmq/simple/IBMMQ_2/sample.slnLaunch b/samples/ibmmq/simple/IBMMQ_2/sample.slnLaunch new file mode 100644 index 00000000000..f14ea1d670a --- /dev/null +++ b/samples/ibmmq/simple/IBMMQ_2/sample.slnLaunch @@ -0,0 +1,15 @@ +[ + { + "Name": "All", + "Projects": [ + { + "Path": "Receiver\\Receiver.csproj", + "Action": "Start" + }, + { + "Path": "Sender\\Sender.csproj", + "Action": "Start" + } + ] + } +] \ No newline at end of file diff --git a/samples/ibmmq/simple/IBMMQ_2/sample.slnx b/samples/ibmmq/simple/IBMMQ_2/sample.slnx new file mode 100644 index 00000000000..f7bd0d7e2a0 --- /dev/null +++ b/samples/ibmmq/simple/IBMMQ_2/sample.slnx @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/samples/ibmmq/simple/sample.md b/samples/ibmmq/simple/sample.md new file mode 100644 index 00000000000..35d68da4768 --- /dev/null +++ b/samples/ibmmq/simple/sample.md @@ -0,0 +1,61 @@ +--- +title: IBM MQ simple sender/receiver +summary: Sending commands from a send-only endpoint to a receiving endpoint using the IBM MQ transport. +reviewed: 2026-03-24 +component: IBMMQ +related: +- transports/ibmmq +- transports/ibmmq/connection-settings +- transports/ibmmq/topology +--- + +This sample demonstrates the basics of sending and receiving messages with the IBM MQ transport. A send-only **Sender** endpoint dispatches `MyMessage` commands to a **Receiver** endpoint, which processes them and dispatches follow-up local messages. + +The sample includes: + +- A **Sender** console application configured as a send-only endpoint that sends a user-specified number of messages +- A **Receiver** console application that processes incoming messages, sends local follow-up messages, and limits concurrent processing +- A **Shared** library containing the message contracts + +## Prerequisites + +The sample requires a running IBM MQ broker. A Docker Compose file is included: + +```bash +docker compose up -d +``` + +This starts IBM MQ with queue manager `QM1` on port `1414`. The management console is available at `https://localhost:9443/ibmmq/console` (credentials: `admin` / `passw0rd`). + +## Running the sample + +1. Start the **Receiver** project first. It calls `EnableInstallers()` on startup, which creates any missing queues automatically. +2. Start the **Sender** project. +3. Enter a number in the Sender console and press Enter. That many `MyMessage` commands are sent to the Receiver. +4. The Receiver logs `Start ` when each message begins processing, then `End: ` after a simulated 200 ms delay. + +## Code walk-through + +### Transport configuration + +Both endpoints initialise an `IBMMQTransport` instance with the connection details for the local broker: + +snippet: SenderConfig + +The connection properties correspond to the Docker Compose defaults: queue manager `QM1`, host `localhost`, port `1414`, channel `DEV.ADMIN.SVRCONN`. + +### Send-only sender + +The Sender endpoint is configured with `SendOnly()` because it never needs to receive messages. This prevents NServiceBus from creating an input queue for the Sender process. + +### Receiver configuration + +snippet: ReceiverConfig + +`LimitMessageProcessingConcurrencyTo(2)` caps simultaneous message processing at two slots. Because `MyHandler` has a 200 ms delay and dispatches two `MyMessage2` commands mid-handler, the second concurrency slot allows those follow-up messages to be processed without waiting for the first handler to complete. + +### Message handler + +snippet: MyHandler + +`MyHandler` dispatches two `MyMessage2` commands via `SendLocal`, then waits 200 ms before completing. `MyHandler2` handles each `MyMessage2` with a simple console log. The two-slot concurrency limit means these local messages can be picked up while `MyHandler` is still running. From b117c1d5ce9dadee282fb2e25c52f211e1744760 Mon Sep 17 00:00:00 2001 From: Irina Dominte Date: Tue, 24 Mar 2026 15:50:22 +0200 Subject: [PATCH 18/19] Sample for request reply, polymorphic events, and simple pub sub --- .claude/skills/sample-doc/SKILL.md | 231 ------------------ .../{IBMMQ_2 => IBMMQ_1}/Client/Client.csproj | 0 .../Client/OrderResponseHandler.cs | 0 .../{IBMMQ_2 => IBMMQ_1}/Client/Program.cs | 0 .../Server/PlaceOrderHandler.cs | 0 .../{IBMMQ_2 => IBMMQ_1}/Server/Program.cs | 0 .../{IBMMQ_2 => IBMMQ_1}/Server/Server.csproj | 0 .../Shared/PlaceOrderRequest.cs | 0 .../{IBMMQ_2 => IBMMQ_1}/Shared/Shared.csproj | 0 .../{IBMMQ_2 => IBMMQ_1}/docker-compose.yml | 0 .../{IBMMQ_2 => IBMMQ_1}/prerelease.txt | 0 .../{IBMMQ_2 => IBMMQ_1}/sample.slnx | 0 .../Receiver/MyHandler.cs | 0 .../{IBMMQ_2 => IBMMQ_1}/Receiver/Program.cs | 0 .../Receiver/Properties/launchSettings.json | 0 .../Receiver/Receiver.csproj | 0 .../{IBMMQ_2 => IBMMQ_1}/Sender/Program.cs | 0 .../Sender/Properties/launchSettings.json | 0 .../{IBMMQ_2 => IBMMQ_1}/Sender/Sender.csproj | 0 .../Sender/appsettings.Development.json | 0 .../Sender/appsettings.json | 0 .../{IBMMQ_2 => IBMMQ_1}/Shared/MyMessage.cs | 0 .../{IBMMQ_2 => IBMMQ_1}/Shared/Shared.csproj | 0 .../{IBMMQ_2 => IBMMQ_1}/docker-compose.yml | 0 .../{IBMMQ_2 => IBMMQ_1}/prerelease.txt | 0 .../{IBMMQ_2 => IBMMQ_1}/sample.slnLaunch | 0 .../simple/{IBMMQ_2 => IBMMQ_1}/sample.slnx | 0 27 files changed, 231 deletions(-) delete mode 100644 .claude/skills/sample-doc/SKILL.md rename samples/ibmmq/request-reply/{IBMMQ_2 => IBMMQ_1}/Client/Client.csproj (100%) rename samples/ibmmq/request-reply/{IBMMQ_2 => IBMMQ_1}/Client/OrderResponseHandler.cs (100%) rename samples/ibmmq/request-reply/{IBMMQ_2 => IBMMQ_1}/Client/Program.cs (100%) rename samples/ibmmq/request-reply/{IBMMQ_2 => IBMMQ_1}/Server/PlaceOrderHandler.cs (100%) rename samples/ibmmq/request-reply/{IBMMQ_2 => IBMMQ_1}/Server/Program.cs (100%) rename samples/ibmmq/request-reply/{IBMMQ_2 => IBMMQ_1}/Server/Server.csproj (100%) rename samples/ibmmq/request-reply/{IBMMQ_2 => IBMMQ_1}/Shared/PlaceOrderRequest.cs (100%) rename samples/ibmmq/request-reply/{IBMMQ_2 => IBMMQ_1}/Shared/Shared.csproj (100%) rename samples/ibmmq/request-reply/{IBMMQ_2 => IBMMQ_1}/docker-compose.yml (100%) rename samples/ibmmq/request-reply/{IBMMQ_2 => IBMMQ_1}/prerelease.txt (100%) rename samples/ibmmq/request-reply/{IBMMQ_2 => IBMMQ_1}/sample.slnx (100%) rename samples/ibmmq/simple/{IBMMQ_2 => IBMMQ_1}/Receiver/MyHandler.cs (100%) rename samples/ibmmq/simple/{IBMMQ_2 => IBMMQ_1}/Receiver/Program.cs (100%) rename samples/ibmmq/simple/{IBMMQ_2 => IBMMQ_1}/Receiver/Properties/launchSettings.json (100%) rename samples/ibmmq/simple/{IBMMQ_2 => IBMMQ_1}/Receiver/Receiver.csproj (100%) rename samples/ibmmq/simple/{IBMMQ_2 => IBMMQ_1}/Sender/Program.cs (100%) rename samples/ibmmq/simple/{IBMMQ_2 => IBMMQ_1}/Sender/Properties/launchSettings.json (100%) rename samples/ibmmq/simple/{IBMMQ_2 => IBMMQ_1}/Sender/Sender.csproj (100%) rename samples/ibmmq/simple/{IBMMQ_2 => IBMMQ_1}/Sender/appsettings.Development.json (100%) rename samples/ibmmq/simple/{IBMMQ_2 => IBMMQ_1}/Sender/appsettings.json (100%) rename samples/ibmmq/simple/{IBMMQ_2 => IBMMQ_1}/Shared/MyMessage.cs (100%) rename samples/ibmmq/simple/{IBMMQ_2 => IBMMQ_1}/Shared/Shared.csproj (100%) rename samples/ibmmq/simple/{IBMMQ_2 => IBMMQ_1}/docker-compose.yml (100%) rename samples/ibmmq/simple/{IBMMQ_2 => IBMMQ_1}/prerelease.txt (100%) rename samples/ibmmq/simple/{IBMMQ_2 => IBMMQ_1}/sample.slnLaunch (100%) rename samples/ibmmq/simple/{IBMMQ_2 => IBMMQ_1}/sample.slnx (100%) diff --git a/.claude/skills/sample-doc/SKILL.md b/.claude/skills/sample-doc/SKILL.md deleted file mode 100644 index 37e0da78653..00000000000 --- a/.claude/skills/sample-doc/SKILL.md +++ /dev/null @@ -1,231 +0,0 @@ ---- -name: sample-doc -description: Generate a sample.md documentation file for a new NServiceBus sample. Reads the sample source code, analyses the structure, drafts the doc, then waits for approval before saving. Also verifies and fixes all wiring required for the docs engine to render the sample correctly. ---- - -You are a technical writer for the docs.particular.net documentation site, specialising in NServiceBus and the Particular Service Platform. - ---- - -## Task - -Generate a `sample.md` file for a sample in the `samples/` folder, then verify all the infrastructure the docs engine needs to render it. The engine has strict requirements - a missing file or wrong directory name causes unhandled exceptions at render time. - ---- - -## Workflow — follow in order - -### Step 1 — Identify the sample - -If the user provides a path or sample name, resolve it under `samples/`. If not, ask: -*"Which sample should I document? Provide the folder path under samples/ (e.g. samples/ibmmq/ebcdic)."* - -### Step 2 — Read the source - -Before writing anything, read: -- All `.cs` source files in the sample folder (especially `Program.cs`, feature files, handlers, and any interop/helper classes) -- All `.csproj` files to identify NuGet package dependencies and the component being used -- Any existing `sample.md` in a sibling folder of the same transport/component for style reference - -Use these reads to answer: -- What does this sample demonstrate? (one sentence) -- What are the projects and their roles? -- What are the key classes/methods a reader needs to understand? -- What `snippet:` names can be referenced? (`#region` marker names — see note below) - -### Step 3 — Identify the component key - -Check `transports//index.md` or grep existing docs for `component:` values to find the correct key (e.g. `IBMMQ`, `RabbitMQ`, `SqlTransport`, `Core`). - -Check `components/nugetAlias.txt` to confirm the NuGet alias for the component (e.g. `IBMMQ: NServiceBus.Transport.IBMMQ`). The alias is the prefix used in versioned directory names. - -### Step 4 — Draft the sample.md - -Write a draft following the structure and rules below. Present it clearly labelled as **Draft sample.md**. - -### Step 5 — Wait for feedback - -After presenting the draft, **stop and wait**. -- If the user approves or says "go" / "looks good", save the file to `samples//sample.md` using the Write tool and confirm. -- If they request edits, apply them and show the revised draft. - -### Step 6 — Wire the sample into the site - -After saving `sample.md`, run through every check below in order. Fix any issues found before moving to the next check. Report all findings clearly. - -#### 6a — Snippet markers - -For every `snippet: KEY` directive in `sample.md`, verify that a corresponding `#region KEY` / `#endregion` block exists somewhere in the `.cs` files inside the sample folder. - -**Important**: the engine does not extract snippets by class name. Every key — including ones named after a class like `MyHandler` — requires an explicit `#region`/`#endregion` marker. Class name matching does not work. - -If any are missing, add the `#region`/`#endregion` markers around the relevant class body or method. The region name must match the snippet key exactly (case-insensitive). - -**Also check the reverse**: every `#region` in every `.cs` file inside the versioned directory must be referenced by a `snippet:` in `sample.md`. Any unreferenced region causes a `RedundantSnippets` exception at render time. Either add a `snippet:` reference for it or remove the `#region`/`#endregion` markers. - -#### 6b — Versioned directory - -The docs engine resolves which component version to render a sample for by looking for directories matching `{NugetAlias}_{MajorVersion}` (e.g. `IBMMQ_1`, `Core_10`, `Rabbit_10`) directly inside the sample folder (same level as `sample.md`). - -Check that such a directory exists: - -``` -samples/// - {NugetAlias}_{MajorVersion}/ ← versioned directory (e.g. IBMMQ_1, Rabbit_10, Core_10) - MySolution.sln - MyProject/ - MyProject.csproj - sample.md -``` - -If the solution directory exists but is not named with the versioned convention: - -- Rename it to `{NugetAlias}_{MajorVersion}/` using `mv` (requires VS to not have the solution open — if locked, tell the user to close VS first) -- The major version number comes from the transport package version in `.csproj` - -If there is an extra nesting level (e.g. `IBMMQ_1/OldName/solution files`), move the solution files up one level so they sit directly inside `IBMMQ_1/`. - -#### 6c — Package reference - -Open every `.csproj` inside the versioned directory. The transport's NuGet package (e.g. `NServiceBus.Transport.IBMMQ`) must be listed as a `PackageReference`. - -If it is missing, add it. Use the same version as found in `Snippets/{NugetAlias}/{NugetAlias}_1/{NugetAlias}_1.csproj` (the snippets project for that component), which always pins the canonical version. - -#### 6d — prerelease.txt - -If the transport package version contains a prerelease suffix (e.g. `-alpha.1`, `-beta.2`), the versioned directory must contain an empty `prerelease.txt` file. - -Check: `ls samples///{NugetAlias}_{Version}/prerelease.txt` - -If missing, create it as an empty file. - -#### 6e — Category index - -Every transport-level sample folder (`samples//`) must have an `index.md` so the category appears in the Samples left-hand navigation menu. - -Check that `samples//index.md` exists. If it does not, create it with this exact content (no `component:` field — adding it breaks rendering): - -```yaml ---- -title: Samples -reviewed: ---- -``` - -Compare with `samples/msmq/index.md` or `samples/rabbitmq/index.md` for the expected format. - -#### 6f — Related links (optional, confirm before doing) - -Offer to add `samples//` to the `related:` list in `transports//index.md` so the sample appears as a related link on the transport docs page. - -**Only do this after all previous checks pass** - the engine throws a `Could not find referenced` exception at render time if the sample page is not yet fully indexed (which requires steps 6a-6e to be complete). Ask the user to confirm before adding the link. - -#### 6g — Restart the docs engine - -**Always remind the user to restart the docs engine after wiring.** The `SolutionDownloadMetadata` (the dictionary of downloadable solution zips) is built once at startup. If the versioned directory (`IBMMQ_1/`, etc.) was created or renamed after the engine started, it will not be in the dictionary and every request to that sample page will throw a `KeyNotFoundException`. A restart is always required when a new versioned directory is added. - ---- - -## Frontmatter - -```yaml ---- -title: -summary: -reviewed: -component: -related: -- -- ---- -``` - -Rules: -- `title`: phrase like "IBM MQ EBCDIC interoperability", not "EBCDIC sample" -- `summary`: start with a gerund ("Receiving...", "Demonstrating...", "Configuring...") -- `reviewed`: always set to today's date -- `component`: must match the exact key used in the transport/component docs -- `related`: link to the transport index page, and any NServiceBus concept pages the sample relies on (pipeline, features, sagas, etc.) - ---- - -## Body structure - -``` -<1–2 paragraph introduction — what the sample does and why it matters> - -[Optional: bullet list of what the sample includes, if there are multiple projects] - -## How it works ← high-level concept explanation (skip if obvious) - -## Prerequisites ← only if non-trivial setup is needed (Docker, broker, DB, etc.) - -## Running the sample ← step-by-step instructions, what to observe - -## Code walk-through ← main section; one H3 per key concept - -### - -<1–2 sentences explaining what this does and why> - -snippet: - - -``` - ---- - -## Writing rules - -- **Introduction**: state what the sample demonstrates in the first sentence. Don't say "this sample shows how to show". Say what it does. -- **No filler**: skip phrases like "In this sample, we will explore..." — just describe. -- **Snippet references**: use `snippet: KEY` to embed code. The build system requires a `#region KEY` / `#endregion` block in a `.cs` file - it does **not** extract by class name alone. Every snippet key in `sample.md` must have a corresponding region marker in the source, including handler classes. Never paste raw code unless it is a short inline example that supplements a snippet. -- **Inline code**: use backtick formatting for class names, method names, interface names, header keys, and config values. -- **Tables**: use markdown tables for structured data like message layouts, field mappings, or config options. -- **Links**: two different rules depending on context: - - Inline body links: `.md` extension **required** — `/path/to/page.md`. Omitting it causes a `Path does not exist` render error. - - `related:` frontmatter entries: `.md` extension **forbidden** — `path/to/page`. Including it causes an `Invalid related '...'. Ends with a '.md'` startup exception. - - External links use standard markdown `[text](url)`. -- **Numbered lists**: use for sequential steps (decoding steps, setup steps). Bullet lists for unordered items. -- **Tone**: plain, technical, direct. No marketing language. Write for a developer who wants to understand the code, not be sold on the platform. -- **Hyphen not em dash**: use `-` not `—`. - ---- - -## What to include in the code walk-through - -Cover every class or concept a reader would not immediately understand from the code alone: - -| Element | Include when | -|---|---| -| Feature registration | Always, if a custom `Feature` is used | -| Envelope/pipeline behavior | Always - these are non-obvious extension points | -| Endpoint configuration | When non-standard options are set | -| Message handlers | When the handler does more than a trivial reply/publish | -| Message contracts | Only if the shape is important (e.g. fixed-length, versioned) | -| External system setup | When the sample interacts with a broker/DB/legacy system | - -Skip boilerplate that any NServiceBus developer would recognise (standard `EndpointConfiguration`, basic `IHandleMessages` implementations with a single `Reply`). - ---- - -## Example output - -See `samples/ibmmq/ebcdic/sample.md` for a complete reference of the expected output format and level of detail. - ---- - -## Output summary (after Step 6) - -Report the outcome of each wiring check: - -```text -✓ sample.md saved to samples//sample.md -✓ Snippet markers: all present -✓ Versioned directory: IBMMQ_1/ exists -✓ Package reference: NServiceBus.Transport.IBMMQ added to Sales.csproj -✓ prerelease.txt: created -✓ Category index: samples/ibmmq/index.md exists -⚠ Related link in transports/ibmmq/index.md: skipped (confirm when ready) -⚠ Restart the docs engine to pick up the new versioned directory. -``` diff --git a/samples/ibmmq/request-reply/IBMMQ_2/Client/Client.csproj b/samples/ibmmq/request-reply/IBMMQ_1/Client/Client.csproj similarity index 100% rename from samples/ibmmq/request-reply/IBMMQ_2/Client/Client.csproj rename to samples/ibmmq/request-reply/IBMMQ_1/Client/Client.csproj diff --git a/samples/ibmmq/request-reply/IBMMQ_2/Client/OrderResponseHandler.cs b/samples/ibmmq/request-reply/IBMMQ_1/Client/OrderResponseHandler.cs similarity index 100% rename from samples/ibmmq/request-reply/IBMMQ_2/Client/OrderResponseHandler.cs rename to samples/ibmmq/request-reply/IBMMQ_1/Client/OrderResponseHandler.cs diff --git a/samples/ibmmq/request-reply/IBMMQ_2/Client/Program.cs b/samples/ibmmq/request-reply/IBMMQ_1/Client/Program.cs similarity index 100% rename from samples/ibmmq/request-reply/IBMMQ_2/Client/Program.cs rename to samples/ibmmq/request-reply/IBMMQ_1/Client/Program.cs diff --git a/samples/ibmmq/request-reply/IBMMQ_2/Server/PlaceOrderHandler.cs b/samples/ibmmq/request-reply/IBMMQ_1/Server/PlaceOrderHandler.cs similarity index 100% rename from samples/ibmmq/request-reply/IBMMQ_2/Server/PlaceOrderHandler.cs rename to samples/ibmmq/request-reply/IBMMQ_1/Server/PlaceOrderHandler.cs diff --git a/samples/ibmmq/request-reply/IBMMQ_2/Server/Program.cs b/samples/ibmmq/request-reply/IBMMQ_1/Server/Program.cs similarity index 100% rename from samples/ibmmq/request-reply/IBMMQ_2/Server/Program.cs rename to samples/ibmmq/request-reply/IBMMQ_1/Server/Program.cs diff --git a/samples/ibmmq/request-reply/IBMMQ_2/Server/Server.csproj b/samples/ibmmq/request-reply/IBMMQ_1/Server/Server.csproj similarity index 100% rename from samples/ibmmq/request-reply/IBMMQ_2/Server/Server.csproj rename to samples/ibmmq/request-reply/IBMMQ_1/Server/Server.csproj diff --git a/samples/ibmmq/request-reply/IBMMQ_2/Shared/PlaceOrderRequest.cs b/samples/ibmmq/request-reply/IBMMQ_1/Shared/PlaceOrderRequest.cs similarity index 100% rename from samples/ibmmq/request-reply/IBMMQ_2/Shared/PlaceOrderRequest.cs rename to samples/ibmmq/request-reply/IBMMQ_1/Shared/PlaceOrderRequest.cs diff --git a/samples/ibmmq/request-reply/IBMMQ_2/Shared/Shared.csproj b/samples/ibmmq/request-reply/IBMMQ_1/Shared/Shared.csproj similarity index 100% rename from samples/ibmmq/request-reply/IBMMQ_2/Shared/Shared.csproj rename to samples/ibmmq/request-reply/IBMMQ_1/Shared/Shared.csproj diff --git a/samples/ibmmq/request-reply/IBMMQ_2/docker-compose.yml b/samples/ibmmq/request-reply/IBMMQ_1/docker-compose.yml similarity index 100% rename from samples/ibmmq/request-reply/IBMMQ_2/docker-compose.yml rename to samples/ibmmq/request-reply/IBMMQ_1/docker-compose.yml diff --git a/samples/ibmmq/request-reply/IBMMQ_2/prerelease.txt b/samples/ibmmq/request-reply/IBMMQ_1/prerelease.txt similarity index 100% rename from samples/ibmmq/request-reply/IBMMQ_2/prerelease.txt rename to samples/ibmmq/request-reply/IBMMQ_1/prerelease.txt diff --git a/samples/ibmmq/request-reply/IBMMQ_2/sample.slnx b/samples/ibmmq/request-reply/IBMMQ_1/sample.slnx similarity index 100% rename from samples/ibmmq/request-reply/IBMMQ_2/sample.slnx rename to samples/ibmmq/request-reply/IBMMQ_1/sample.slnx diff --git a/samples/ibmmq/simple/IBMMQ_2/Receiver/MyHandler.cs b/samples/ibmmq/simple/IBMMQ_1/Receiver/MyHandler.cs similarity index 100% rename from samples/ibmmq/simple/IBMMQ_2/Receiver/MyHandler.cs rename to samples/ibmmq/simple/IBMMQ_1/Receiver/MyHandler.cs diff --git a/samples/ibmmq/simple/IBMMQ_2/Receiver/Program.cs b/samples/ibmmq/simple/IBMMQ_1/Receiver/Program.cs similarity index 100% rename from samples/ibmmq/simple/IBMMQ_2/Receiver/Program.cs rename to samples/ibmmq/simple/IBMMQ_1/Receiver/Program.cs diff --git a/samples/ibmmq/simple/IBMMQ_2/Receiver/Properties/launchSettings.json b/samples/ibmmq/simple/IBMMQ_1/Receiver/Properties/launchSettings.json similarity index 100% rename from samples/ibmmq/simple/IBMMQ_2/Receiver/Properties/launchSettings.json rename to samples/ibmmq/simple/IBMMQ_1/Receiver/Properties/launchSettings.json diff --git a/samples/ibmmq/simple/IBMMQ_2/Receiver/Receiver.csproj b/samples/ibmmq/simple/IBMMQ_1/Receiver/Receiver.csproj similarity index 100% rename from samples/ibmmq/simple/IBMMQ_2/Receiver/Receiver.csproj rename to samples/ibmmq/simple/IBMMQ_1/Receiver/Receiver.csproj diff --git a/samples/ibmmq/simple/IBMMQ_2/Sender/Program.cs b/samples/ibmmq/simple/IBMMQ_1/Sender/Program.cs similarity index 100% rename from samples/ibmmq/simple/IBMMQ_2/Sender/Program.cs rename to samples/ibmmq/simple/IBMMQ_1/Sender/Program.cs diff --git a/samples/ibmmq/simple/IBMMQ_2/Sender/Properties/launchSettings.json b/samples/ibmmq/simple/IBMMQ_1/Sender/Properties/launchSettings.json similarity index 100% rename from samples/ibmmq/simple/IBMMQ_2/Sender/Properties/launchSettings.json rename to samples/ibmmq/simple/IBMMQ_1/Sender/Properties/launchSettings.json diff --git a/samples/ibmmq/simple/IBMMQ_2/Sender/Sender.csproj b/samples/ibmmq/simple/IBMMQ_1/Sender/Sender.csproj similarity index 100% rename from samples/ibmmq/simple/IBMMQ_2/Sender/Sender.csproj rename to samples/ibmmq/simple/IBMMQ_1/Sender/Sender.csproj diff --git a/samples/ibmmq/simple/IBMMQ_2/Sender/appsettings.Development.json b/samples/ibmmq/simple/IBMMQ_1/Sender/appsettings.Development.json similarity index 100% rename from samples/ibmmq/simple/IBMMQ_2/Sender/appsettings.Development.json rename to samples/ibmmq/simple/IBMMQ_1/Sender/appsettings.Development.json diff --git a/samples/ibmmq/simple/IBMMQ_2/Sender/appsettings.json b/samples/ibmmq/simple/IBMMQ_1/Sender/appsettings.json similarity index 100% rename from samples/ibmmq/simple/IBMMQ_2/Sender/appsettings.json rename to samples/ibmmq/simple/IBMMQ_1/Sender/appsettings.json diff --git a/samples/ibmmq/simple/IBMMQ_2/Shared/MyMessage.cs b/samples/ibmmq/simple/IBMMQ_1/Shared/MyMessage.cs similarity index 100% rename from samples/ibmmq/simple/IBMMQ_2/Shared/MyMessage.cs rename to samples/ibmmq/simple/IBMMQ_1/Shared/MyMessage.cs diff --git a/samples/ibmmq/simple/IBMMQ_2/Shared/Shared.csproj b/samples/ibmmq/simple/IBMMQ_1/Shared/Shared.csproj similarity index 100% rename from samples/ibmmq/simple/IBMMQ_2/Shared/Shared.csproj rename to samples/ibmmq/simple/IBMMQ_1/Shared/Shared.csproj diff --git a/samples/ibmmq/simple/IBMMQ_2/docker-compose.yml b/samples/ibmmq/simple/IBMMQ_1/docker-compose.yml similarity index 100% rename from samples/ibmmq/simple/IBMMQ_2/docker-compose.yml rename to samples/ibmmq/simple/IBMMQ_1/docker-compose.yml diff --git a/samples/ibmmq/simple/IBMMQ_2/prerelease.txt b/samples/ibmmq/simple/IBMMQ_1/prerelease.txt similarity index 100% rename from samples/ibmmq/simple/IBMMQ_2/prerelease.txt rename to samples/ibmmq/simple/IBMMQ_1/prerelease.txt diff --git a/samples/ibmmq/simple/IBMMQ_2/sample.slnLaunch b/samples/ibmmq/simple/IBMMQ_1/sample.slnLaunch similarity index 100% rename from samples/ibmmq/simple/IBMMQ_2/sample.slnLaunch rename to samples/ibmmq/simple/IBMMQ_1/sample.slnLaunch diff --git a/samples/ibmmq/simple/IBMMQ_2/sample.slnx b/samples/ibmmq/simple/IBMMQ_1/sample.slnx similarity index 100% rename from samples/ibmmq/simple/IBMMQ_2/sample.slnx rename to samples/ibmmq/simple/IBMMQ_1/sample.slnx From d1529134ab4585e97bb8f7581f202c8630c16fdc Mon Sep 17 00:00:00 2001 From: Irina Dominte Date: Tue, 24 Mar 2026 16:02:10 +0200 Subject: [PATCH 19/19] Fixed tests --- .../polymorphic-events/{IBMMQ_2 => IBMMQ_1}/Orders/Orders.csproj | 0 .../polymorphic-events/{IBMMQ_2 => IBMMQ_1}/Orders/Program.cs | 0 .../polymorphic-events/{IBMMQ_2 => IBMMQ_1}/Shared/Messages.cs | 0 .../polymorphic-events/{IBMMQ_2 => IBMMQ_1}/Shared/Shared.csproj | 0 .../{IBMMQ_2 => IBMMQ_1}/Shipping/OrderPlacedHandler.cs | 0 .../polymorphic-events/{IBMMQ_2 => IBMMQ_1}/Shipping/Program.cs | 0 .../{IBMMQ_2 => IBMMQ_1}/Shipping/Shipping.csproj | 0 .../polymorphic-events/{IBMMQ_2 => IBMMQ_1}/docker-compose.yml | 0 .../ibmmq/polymorphic-events/{IBMMQ_2 => IBMMQ_1}/prerelease.txt | 0 samples/ibmmq/polymorphic-events/{IBMMQ_2 => IBMMQ_1}/sample.slnx | 0 10 files changed, 0 insertions(+), 0 deletions(-) rename samples/ibmmq/polymorphic-events/{IBMMQ_2 => IBMMQ_1}/Orders/Orders.csproj (100%) rename samples/ibmmq/polymorphic-events/{IBMMQ_2 => IBMMQ_1}/Orders/Program.cs (100%) rename samples/ibmmq/polymorphic-events/{IBMMQ_2 => IBMMQ_1}/Shared/Messages.cs (100%) rename samples/ibmmq/polymorphic-events/{IBMMQ_2 => IBMMQ_1}/Shared/Shared.csproj (100%) rename samples/ibmmq/polymorphic-events/{IBMMQ_2 => IBMMQ_1}/Shipping/OrderPlacedHandler.cs (100%) rename samples/ibmmq/polymorphic-events/{IBMMQ_2 => IBMMQ_1}/Shipping/Program.cs (100%) rename samples/ibmmq/polymorphic-events/{IBMMQ_2 => IBMMQ_1}/Shipping/Shipping.csproj (100%) rename samples/ibmmq/polymorphic-events/{IBMMQ_2 => IBMMQ_1}/docker-compose.yml (100%) rename samples/ibmmq/polymorphic-events/{IBMMQ_2 => IBMMQ_1}/prerelease.txt (100%) rename samples/ibmmq/polymorphic-events/{IBMMQ_2 => IBMMQ_1}/sample.slnx (100%) diff --git a/samples/ibmmq/polymorphic-events/IBMMQ_2/Orders/Orders.csproj b/samples/ibmmq/polymorphic-events/IBMMQ_1/Orders/Orders.csproj similarity index 100% rename from samples/ibmmq/polymorphic-events/IBMMQ_2/Orders/Orders.csproj rename to samples/ibmmq/polymorphic-events/IBMMQ_1/Orders/Orders.csproj diff --git a/samples/ibmmq/polymorphic-events/IBMMQ_2/Orders/Program.cs b/samples/ibmmq/polymorphic-events/IBMMQ_1/Orders/Program.cs similarity index 100% rename from samples/ibmmq/polymorphic-events/IBMMQ_2/Orders/Program.cs rename to samples/ibmmq/polymorphic-events/IBMMQ_1/Orders/Program.cs diff --git a/samples/ibmmq/polymorphic-events/IBMMQ_2/Shared/Messages.cs b/samples/ibmmq/polymorphic-events/IBMMQ_1/Shared/Messages.cs similarity index 100% rename from samples/ibmmq/polymorphic-events/IBMMQ_2/Shared/Messages.cs rename to samples/ibmmq/polymorphic-events/IBMMQ_1/Shared/Messages.cs diff --git a/samples/ibmmq/polymorphic-events/IBMMQ_2/Shared/Shared.csproj b/samples/ibmmq/polymorphic-events/IBMMQ_1/Shared/Shared.csproj similarity index 100% rename from samples/ibmmq/polymorphic-events/IBMMQ_2/Shared/Shared.csproj rename to samples/ibmmq/polymorphic-events/IBMMQ_1/Shared/Shared.csproj diff --git a/samples/ibmmq/polymorphic-events/IBMMQ_2/Shipping/OrderPlacedHandler.cs b/samples/ibmmq/polymorphic-events/IBMMQ_1/Shipping/OrderPlacedHandler.cs similarity index 100% rename from samples/ibmmq/polymorphic-events/IBMMQ_2/Shipping/OrderPlacedHandler.cs rename to samples/ibmmq/polymorphic-events/IBMMQ_1/Shipping/OrderPlacedHandler.cs diff --git a/samples/ibmmq/polymorphic-events/IBMMQ_2/Shipping/Program.cs b/samples/ibmmq/polymorphic-events/IBMMQ_1/Shipping/Program.cs similarity index 100% rename from samples/ibmmq/polymorphic-events/IBMMQ_2/Shipping/Program.cs rename to samples/ibmmq/polymorphic-events/IBMMQ_1/Shipping/Program.cs diff --git a/samples/ibmmq/polymorphic-events/IBMMQ_2/Shipping/Shipping.csproj b/samples/ibmmq/polymorphic-events/IBMMQ_1/Shipping/Shipping.csproj similarity index 100% rename from samples/ibmmq/polymorphic-events/IBMMQ_2/Shipping/Shipping.csproj rename to samples/ibmmq/polymorphic-events/IBMMQ_1/Shipping/Shipping.csproj diff --git a/samples/ibmmq/polymorphic-events/IBMMQ_2/docker-compose.yml b/samples/ibmmq/polymorphic-events/IBMMQ_1/docker-compose.yml similarity index 100% rename from samples/ibmmq/polymorphic-events/IBMMQ_2/docker-compose.yml rename to samples/ibmmq/polymorphic-events/IBMMQ_1/docker-compose.yml diff --git a/samples/ibmmq/polymorphic-events/IBMMQ_2/prerelease.txt b/samples/ibmmq/polymorphic-events/IBMMQ_1/prerelease.txt similarity index 100% rename from samples/ibmmq/polymorphic-events/IBMMQ_2/prerelease.txt rename to samples/ibmmq/polymorphic-events/IBMMQ_1/prerelease.txt diff --git a/samples/ibmmq/polymorphic-events/IBMMQ_2/sample.slnx b/samples/ibmmq/polymorphic-events/IBMMQ_1/sample.slnx similarity index 100% rename from samples/ibmmq/polymorphic-events/IBMMQ_2/sample.slnx rename to samples/ibmmq/polymorphic-events/IBMMQ_1/sample.slnx