# IBM MQ EBCDIC interoperability 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 **Receiver** 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. ## 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 `DEV.RECEIVER` 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. ```cs using var qm = new MQQueueManager("QM1", props); using var queue = qm.AccessQueue("DEV.RECEIVER", MQC.MQOO_OUTPUT); Console.WriteLine("Sending a COBOL-like, fixed length EBCDIC text encoded message..."); 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 = 500;// Set character set to IBM500 EBCDIC msg.Format = MQC.MQFMT_NONE; msg.Write(body); queue.Put(msg); ``` ### Registering the mutator `EbcdicMutator` is registered directly in endpoint configuration: ```cs //in order to use IBM500 EBCDIC encoding, we need to register the code page provider Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); endpointConfiguration.RegisterMessageMutator(new FixedLengthEBCDICToJsonMutator()); ``` ### 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/index.md). ```cs sealed class FixedLengthEBCDICToJsonMutator : IMutateIncomingTransportMessages { // EBCDIC International (Latin-1) code page static readonly Encoding Ebcdic = Encoding.GetEncoding("IBM500"); const int RecordLength = 70; 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!; 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"] = "DEV.RECEIVER"; context.Headers["NServiceBus.OriginatingEndpoint"] = "LegacyMainframe"; context.Headers["NServiceBus.OriginatingMachine"] = "MAINFRAME"; return Task.CompletedTask; } } ``` 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: ```cs sealed class PlaceOrderHandler(ILogger logger) : IHandleMessages { public async Task Handle(PlaceOrder message, IMessageHandlerContext context) { Console.WriteLine("In the handler"); logger.LogInformation(""" Published PlaceOrder OrderId = {OrderId}, Product = {Product}, Quantity = {Quantity} """, message.OrderId, message.Product, message.Quantity); } } ```