Getting Started
Architecture
NServiceBus
Transports
Persistence
ServiceInsight
ServicePulse
ServiceControl
Monitoring
Modernization
Samples

IBM MQ EBCDIC interoperability

Component: NServiceBus
NuGet Package: NServiceBus.Transport.IBMMQ 1.x
Target Version: NServiceBus 1.x
Standard support for version 1.x of NServiceBus has expired. For more information see our Support Policy.

This sample demonstrates how to receive fixed-length 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

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 and cannot be deserialized by the endpoint without prior transformation.

The NServiceBus message mutator pipeline extension point intercepts raw incoming messages before they reach the message handler pipeline. 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 and no NServiceBus metadata.

The record layout is:

BytesFieldEncoding
0-35OrderIdEBCDIC code page 500, 36 chars
36-65ProductEBCDIC code page 500, space-padded to 30 chars
66-69QuantityBig-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.

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:

//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.

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<byte>(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 configured on the endpoint.
  4. Adds headers 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 command using standard NServiceBus APIs - it has no awareness of the EBCDIC origin of the message:

sealed class PlaceOrderHandler(ILogger<PlaceOrderHandler> logger)
    :IHandleMessages<PlaceOrder>
{
    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);
    }
}

Related Articles