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.APIWMQ - 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. queue using the native IBM. API. The message carries no MQRFH2 headers 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. is set to 500 (EBCDIC) and MQMessage. 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:
- Returns early for records that are not exactly 70 bytes - allowing the message to proceed unchanged.
- Decodes the three fixed-length fields from EBCDIC bytes into .NET types using
Encoding.andGetEncoding(500) BinaryPrimitives..ReadInt32BigEndian - Replaces
context.with a JSON-encoded body compatible with theBody SystemJsonSerializerconfigured on the endpoint. - Adds headers to
context.that provide all the NServiceBus routing metadata the pipeline requires: message type, content type, intent, reply-to address, and originating endpoint.Headers
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);
}
}