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..f514d7d765d --- /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 + } +} diff --git a/Snippets/IBMMQ/IBMMQ_1/prerelease.txt b/Snippets/IBMMQ/IBMMQ_1/prerelease.txt new file mode 100644 index 00000000000..e69de29bb2d diff --git a/components/components.yaml b/components/components.yaml index 6f7368ea2fe..db4d0b66ff8 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: Particular + Category: Transport + ProjectUrl: https://github.com/Particular/NServiceBus.Transport.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..91382948d89 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/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/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/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/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/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..5f9a3262ff3 --- /dev/null +++ b/samples/ibmmq/ebcdic/IBMMQ_1/Sales/Program.cs @@ -0,0 +1,33 @@ +using Microsoft.Extensions.Hosting; +using NServiceBus; +using NServiceBus.MessageMutator; +using NServiceBus.Transport.IBMMQ; + +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"; + }); + #region EbcdicMutatorRegistration + endpointConfiguration.RegisterMessageMutator(new EbcdicMutator()); + #endregion + + var routing = endpointConfiguration.UseTransport(transport); + endpointConfiguration.EnableInstallers(); + + 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..f9ced1b8024 --- /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..ad1c6f6a328 --- /dev/null +++ b/samples/ibmmq/ebcdic/sample.md @@ -0,0 +1,72 @@ +--- +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/message-mutators +- 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 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 + +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. + +### Registering the mutator + +`EbcdicMutator` is registered directly in endpoint configuration: + +snippet: EbcdicMutatorRegistration + +### Decoding the EBCDIC message + +`EbcdicMutator` implements `IMutateIncomingTransportMessages`. It is called for every incoming message before the message is dispatched to a [message handler](/nservicebus/handlers/). + +snippet: EbcdicMutator + +The mutator: + +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 + +`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 mutator 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. + + diff --git a/samples/ibmmq/polymorphic-events/IBMMQ_1/Orders/Orders.csproj b/samples/ibmmq/polymorphic-events/IBMMQ_1/Orders/Orders.csproj new file mode 100644 index 00000000000..d58acadc7bb --- /dev/null +++ b/samples/ibmmq/polymorphic-events/IBMMQ_1/Orders/Orders.csproj @@ -0,0 +1,20 @@ + + + + Exe + net10.0 + enable + enable + + + + + + + + + + + + + diff --git a/samples/ibmmq/polymorphic-events/IBMMQ_1/Orders/Program.cs b/samples/ibmmq/polymorphic-events/IBMMQ_1/Orders/Program.cs new file mode 100644 index 00000000000..b5706de20e2 --- /dev/null +++ b/samples/ibmmq/polymorphic-events/IBMMQ_1/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_1/Shared/Messages.cs b/samples/ibmmq/polymorphic-events/IBMMQ_1/Shared/Messages.cs new file mode 100644 index 00000000000..bc6cac4e701 --- /dev/null +++ b/samples/ibmmq/polymorphic-events/IBMMQ_1/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_1/Shared/Shared.csproj b/samples/ibmmq/polymorphic-events/IBMMQ_1/Shared/Shared.csproj new file mode 100644 index 00000000000..18940df908b --- /dev/null +++ b/samples/ibmmq/polymorphic-events/IBMMQ_1/Shared/Shared.csproj @@ -0,0 +1,13 @@ + + + + net10.0 + enable + enable + + + + + + + diff --git a/samples/ibmmq/polymorphic-events/IBMMQ_1/Shipping/OrderPlacedHandler.cs b/samples/ibmmq/polymorphic-events/IBMMQ_1/Shipping/OrderPlacedHandler.cs new file mode 100644 index 00000000000..da121644aa7 --- /dev/null +++ b/samples/ibmmq/polymorphic-events/IBMMQ_1/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_1/Shipping/Program.cs b/samples/ibmmq/polymorphic-events/IBMMQ_1/Shipping/Program.cs new file mode 100644 index 00000000000..c7b21ac73f0 --- /dev/null +++ b/samples/ibmmq/polymorphic-events/IBMMQ_1/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_1/Shipping/Shipping.csproj b/samples/ibmmq/polymorphic-events/IBMMQ_1/Shipping/Shipping.csproj new file mode 100644 index 00000000000..d58acadc7bb --- /dev/null +++ b/samples/ibmmq/polymorphic-events/IBMMQ_1/Shipping/Shipping.csproj @@ -0,0 +1,20 @@ + + + + Exe + net10.0 + enable + enable + + + + + + + + + + + + + diff --git a/samples/ibmmq/polymorphic-events/IBMMQ_1/docker-compose.yml b/samples/ibmmq/polymorphic-events/IBMMQ_1/docker-compose.yml new file mode 100644 index 00000000000..1b7d15dee55 --- /dev/null +++ b/samples/ibmmq/polymorphic-events/IBMMQ_1/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_1/prerelease.txt b/samples/ibmmq/polymorphic-events/IBMMQ_1/prerelease.txt new file mode 100644 index 00000000000..e69de29bb2d diff --git a/samples/ibmmq/polymorphic-events/IBMMQ_1/sample.slnx b/samples/ibmmq/polymorphic-events/IBMMQ_1/sample.slnx new file mode 100644 index 00000000000..75ac85a493b --- /dev/null +++ b/samples/ibmmq/polymorphic-events/IBMMQ_1/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_1/Client/Client.csproj b/samples/ibmmq/request-reply/IBMMQ_1/Client/Client.csproj new file mode 100644 index 00000000000..d58acadc7bb --- /dev/null +++ b/samples/ibmmq/request-reply/IBMMQ_1/Client/Client.csproj @@ -0,0 +1,20 @@ + + + + Exe + net10.0 + enable + enable + + + + + + + + + + + + + diff --git a/samples/ibmmq/request-reply/IBMMQ_1/Client/OrderResponseHandler.cs b/samples/ibmmq/request-reply/IBMMQ_1/Client/OrderResponseHandler.cs new file mode 100644 index 00000000000..725f41eacfa --- /dev/null +++ b/samples/ibmmq/request-reply/IBMMQ_1/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_1/Client/Program.cs b/samples/ibmmq/request-reply/IBMMQ_1/Client/Program.cs new file mode 100644 index 00000000000..02448de5aa5 --- /dev/null +++ b/samples/ibmmq/request-reply/IBMMQ_1/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_1/Server/PlaceOrderHandler.cs b/samples/ibmmq/request-reply/IBMMQ_1/Server/PlaceOrderHandler.cs new file mode 100644 index 00000000000..0c89516a0ad --- /dev/null +++ b/samples/ibmmq/request-reply/IBMMQ_1/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_1/Server/Program.cs b/samples/ibmmq/request-reply/IBMMQ_1/Server/Program.cs new file mode 100644 index 00000000000..a73e091d6a7 --- /dev/null +++ b/samples/ibmmq/request-reply/IBMMQ_1/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_1/Server/Server.csproj b/samples/ibmmq/request-reply/IBMMQ_1/Server/Server.csproj new file mode 100644 index 00000000000..d58acadc7bb --- /dev/null +++ b/samples/ibmmq/request-reply/IBMMQ_1/Server/Server.csproj @@ -0,0 +1,20 @@ + + + + Exe + net10.0 + enable + enable + + + + + + + + + + + + + diff --git a/samples/ibmmq/request-reply/IBMMQ_1/Shared/PlaceOrderRequest.cs b/samples/ibmmq/request-reply/IBMMQ_1/Shared/PlaceOrderRequest.cs new file mode 100644 index 00000000000..7f45545f6a6 --- /dev/null +++ b/samples/ibmmq/request-reply/IBMMQ_1/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_1/Shared/Shared.csproj b/samples/ibmmq/request-reply/IBMMQ_1/Shared/Shared.csproj new file mode 100644 index 00000000000..18940df908b --- /dev/null +++ b/samples/ibmmq/request-reply/IBMMQ_1/Shared/Shared.csproj @@ -0,0 +1,13 @@ + + + + net10.0 + enable + enable + + + + + + + diff --git a/samples/ibmmq/request-reply/IBMMQ_1/docker-compose.yml b/samples/ibmmq/request-reply/IBMMQ_1/docker-compose.yml new file mode 100644 index 00000000000..1b7d15dee55 --- /dev/null +++ b/samples/ibmmq/request-reply/IBMMQ_1/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_1/prerelease.txt b/samples/ibmmq/request-reply/IBMMQ_1/prerelease.txt new file mode 100644 index 00000000000..e69de29bb2d diff --git a/samples/ibmmq/request-reply/IBMMQ_1/sample.slnx b/samples/ibmmq/request-reply/IBMMQ_1/sample.slnx new file mode 100644 index 00000000000..69954f1ca6d --- /dev/null +++ b/samples/ibmmq/request-reply/IBMMQ_1/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_1/Receiver/MyHandler.cs b/samples/ibmmq/simple/IBMMQ_1/Receiver/MyHandler.cs new file mode 100644 index 00000000000..a3c7237a04d --- /dev/null +++ b/samples/ibmmq/simple/IBMMQ_1/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_1/Receiver/Program.cs b/samples/ibmmq/simple/IBMMQ_1/Receiver/Program.cs new file mode 100644 index 00000000000..a18d4268bc3 --- /dev/null +++ b/samples/ibmmq/simple/IBMMQ_1/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_1/Receiver/Properties/launchSettings.json b/samples/ibmmq/simple/IBMMQ_1/Receiver/Properties/launchSettings.json new file mode 100644 index 00000000000..f981b6f91c8 --- /dev/null +++ b/samples/ibmmq/simple/IBMMQ_1/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_1/Receiver/Receiver.csproj b/samples/ibmmq/simple/IBMMQ_1/Receiver/Receiver.csproj new file mode 100644 index 00000000000..c2daee5d5c6 --- /dev/null +++ b/samples/ibmmq/simple/IBMMQ_1/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_1/Sender/Program.cs b/samples/ibmmq/simple/IBMMQ_1/Sender/Program.cs new file mode 100644 index 00000000000..f3457227ce8 --- /dev/null +++ b/samples/ibmmq/simple/IBMMQ_1/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_1/Sender/Properties/launchSettings.json b/samples/ibmmq/simple/IBMMQ_1/Sender/Properties/launchSettings.json new file mode 100644 index 00000000000..c238eae6ed2 --- /dev/null +++ b/samples/ibmmq/simple/IBMMQ_1/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_1/Sender/Sender.csproj b/samples/ibmmq/simple/IBMMQ_1/Sender/Sender.csproj new file mode 100644 index 00000000000..c2daee5d5c6 --- /dev/null +++ b/samples/ibmmq/simple/IBMMQ_1/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_1/Sender/appsettings.Development.json b/samples/ibmmq/simple/IBMMQ_1/Sender/appsettings.Development.json new file mode 100644 index 00000000000..3c98c4159b0 --- /dev/null +++ b/samples/ibmmq/simple/IBMMQ_1/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_1/Sender/appsettings.json b/samples/ibmmq/simple/IBMMQ_1/Sender/appsettings.json new file mode 100644 index 00000000000..b2dcdb67421 --- /dev/null +++ b/samples/ibmmq/simple/IBMMQ_1/Sender/appsettings.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/samples/ibmmq/simple/IBMMQ_1/Shared/MyMessage.cs b/samples/ibmmq/simple/IBMMQ_1/Shared/MyMessage.cs new file mode 100644 index 00000000000..9fe449163d9 --- /dev/null +++ b/samples/ibmmq/simple/IBMMQ_1/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_1/Shared/Shared.csproj b/samples/ibmmq/simple/IBMMQ_1/Shared/Shared.csproj new file mode 100644 index 00000000000..9e7e5378532 --- /dev/null +++ b/samples/ibmmq/simple/IBMMQ_1/Shared/Shared.csproj @@ -0,0 +1,14 @@ + + + + net10.0 + enable + enable + Messages + + + + + + + diff --git a/samples/ibmmq/simple/IBMMQ_1/docker-compose.yml b/samples/ibmmq/simple/IBMMQ_1/docker-compose.yml new file mode 100644 index 00000000000..1498322c4a2 --- /dev/null +++ b/samples/ibmmq/simple/IBMMQ_1/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_1/prerelease.txt b/samples/ibmmq/simple/IBMMQ_1/prerelease.txt new file mode 100644 index 00000000000..e69de29bb2d diff --git a/samples/ibmmq/simple/IBMMQ_1/sample.slnLaunch b/samples/ibmmq/simple/IBMMQ_1/sample.slnLaunch new file mode 100644 index 00000000000..f14ea1d670a --- /dev/null +++ b/samples/ibmmq/simple/IBMMQ_1/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_1/sample.slnx b/samples/ibmmq/simple/IBMMQ_1/sample.slnx new file mode 100644 index 00000000000..f7bd0d7e2a0 --- /dev/null +++ b/samples/ibmmq/simple/IBMMQ_1/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. diff --git a/transports/ibmmq/connection-settings.md b/transports/ibmmq/connection-settings.md new file mode 100644 index 00000000000..0c802c4f1ef --- /dev/null +++ b/transports/ibmmq/connection-settings.md @@ -0,0 +1,132 @@ +--- +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 +--- + +## Basic connection + +The transport connects to an IBM MQ queue manager using a host, port, and SVRCONN channel: + +snippet: ibmmq-basic-connection + +### Defaults + +|Setting|Default| +|:---|---| +|Host|`localhost`| +|Port|`1414`| +|Channel|`DEV.ADMIN.SVRCONN`| +|QueueManagerName|Empty (local default queue manager named QM1)| + +## Authentication + +User credentials can be provided to authenticate with the queue manager: + +snippet: ibmmq-authentication + +## Application name + +The application name appears in IBM MQ monitoring tools and is useful for identifying connections: + +snippet: ibmmq-application-name + +If not specified, the application name defaults to the entry assembly name. + +## High availability + +For high availability scenarios with multi-instance queue managers, provide a connection name list instead of a single host and port: + +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. + +> [!NOTE] +> 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, configure the SSL key repository and cipher specification. The cipher must match the `SSLCIPH` attribute on the SVRCONN channel. + +snippet: ibmmq-ssl-tls + +### Key repository options + +|Value|Description| +|:---|---| +|`*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 + +Verify the queue manager's certificate distinguished name for additional security: + +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. + +## Topic naming + +Topics are named using a configurable `TopicNaming` strategy. The default uses a prefix of `DEV` and the fully qualified type name. + +### Custom topic prefix + +To change the prefix: + +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: + +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, you need to configure a sanitizer: + +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. + +## Message processing settings + +### Polling interval + +The wait interval controls how long each poll waits for a message before returning: + +snippet: ibmmq-polling-interval + +|Setting|Default|Range| +|:---|---|---| +|MessageWaitInterval|5000 ms|100–30,000 ms| + +### Maximum message size + +Should match or be less than the queue manager's `MAXMSGL` setting: + +snippet: ibmmq-max-message-size + +|Setting|Default|Range| +|:---|---|---| +|MaxMessageLength|4 MB|1 KB – 100 MB| + +### Character set + +The Coded Character Set Identifier (CCSID) used for message text encoding. The default is UTF-8 (1208), which is recommended for most scenarios. + +snippet: ibmmq-character-set + +## Message persistence + +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 new file mode 100644 index 00000000000..462512a536f --- /dev/null +++ b/transports/ibmmq/index.md @@ -0,0 +1,41 @@ +--- +title: IBM MQ Transport +summary: Integrate NServiceBus with IBM MQ for enterprise messaging on mainframe and distributed platforms +reviewed: 2026-03-23 +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. + +## Transport at a glance + +|Feature | | +|:--- |--- +|Transactions |None, ReceiveOnly, SendsAtomicWithReceive +|Pub/Sub |Native +|Timeouts |Not natively supported +|Large message bodies |Up to 100 MB (configurable) +|Scale-out |Competing consumer +|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 +|Queue and topic names |Limited to 48 characters +|Delayed delivery |No. Requires an external timeout storage mechanism. + +## Configuring the endpoint + +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. \ No newline at end of file diff --git a/transports/ibmmq/native-integration.md b/transports/ibmmq/native-integration.md new file mode 100644 index 00000000000..d01584bd0c1 --- /dev/null +++ b/transports/ibmmq/native-integration.md @@ -0,0 +1,72 @@ +--- +title: Native integration +summary: How to integrate NServiceBus endpoints with native IBM MQ applications +reviewed: 2026-03-23 +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. + +## Message structure + +NServiceBus messages on IBM MQ consist of a standard message descriptor (MQMD) and custom properties stored in MQRFH2 headers. + +### MQMD fields + +The transport maps NServiceBus concepts to the following MQMD fields on send: + +|MQMD field|Usage| +|:---|---| +|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| +|ReplyToQueueName|Set from the `NServiceBus.ReplyToAddress` header| + +### NServiceBus headers + +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 `__` +- 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. 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: + +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: + +|Property name|Description| +|:---|---| +|`NServiceBus_x002EEnclosedMessageTypes`|The fully qualified .NET type name of the message| +|`NServiceBus_x002EContentType`|The MIME type of the body (e.g., `application/json`)| +|`NServiceBus_x002EMessageId`|A unique identifier (typically a GUID)| + +> [!WARNING] +> 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 message body contains the serialized payload, and NServiceBus headers are available as MQRFH2 properties. + +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/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. diff --git a/transports/ibmmq/operations-scripting.md b/transports/ibmmq/operations-scripting.md new file mode 100644 index 00000000000..0a44ab37e09 --- /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..10be55f02e9 --- /dev/null +++ b/transports/ibmmq/topology.md @@ -0,0 +1,79 @@ +--- +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 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) + +Commands are sent directly to the destination queue by name. No topics 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, 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, 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 durable subscription is deleted from the queue manager. + +## Polymorphism + +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: + +```csharp +public interface IOrderEvent { } +public class OrderPlaced : IOrderEvent { } +public class OrderCancelled : IOrderEvent { } +``` + +Subscribing to `IOrderEvent` creates subscriptions for both `OrderPlaced` and `OrderCancelled`: + +```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 strategy. The default uses a prefix (default: `DEV`) and the fully qualified type name: + +|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 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 new file mode 100644 index 00000000000..347441f4dc2 --- /dev/null +++ b/transports/ibmmq/transactions.md @@ -0,0 +1,46 @@ +--- +title: Transaction support +summary: Transaction modes supported by the IBM MQ transport +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 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 outgoing send/publish operations are committed or rolled back as a single unit of work. + +snippet: ibmmq-sends-atomic-with-receive + +> [!NOTE] +> Messages sent outside of a handler (e.g., via `IMessageSession`) are not included in the atomic operation. + +## Receive only + +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. + +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. + +## Unreliable (transactions disabled) + +In `None` mode, messages are consumed without any transactional guarantees. If processing fails, the message is lost. + +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. 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 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/). 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) | ✖ | ✔ | ✔ | ✔ |