If you have ever heard "AS/400" and thought "that thing from the 80s", this article is for you. IBM i (the modern name of the AS/400) is still running payroll, banking and logistics in thousands of companies. Sooner or later, a .NET developer is asked to talk to it.

By the end of this article you will know how to:

  • get a free IBM i account and look around it;
  • create a data queue (the IBM i message queue) and read it from .NET;
  • call a COBOL program from C# and get its answer back;
  • let that COBOL program publish a message that your .NET service consumes;
  • see the whole thing in a small web page.

Everything below was run for real against a free server. The code is a small open-source library plus a demo web app.

1. The vocabulary in two minutes

  • IBM i / AS/400: a server with its own operating system. Same machine family, different names over the years.
  • Library: roughly a schema or a folder. Objects live inside libraries (MYLIB/MYQUEUE).
  • 5250 / green screen: the text terminal used to work on an IBM i.
  • Data queue (*DTAQ): a message queue built into the system. Programs write messages, other programs read them.
  • ILE COBOL / RPG / CL: the languages of the platform. COBOL is common for business logic.
  • EBCDIC: the character encoding of IBM i. Not ASCII. This will bite you (section 6).

The idea of this article: a data queue is a simple, reliable way to connect old and new code. A COBOL program drops a message in the queue; a modern .NET service picks it up. Neither side needs to know how the other is built.

2. Get a free IBM i and look around

PUB400 is a free public IBM i server run by volunteers. Create an account on the site, then connect with a 5250 terminal emulator. A free open-source one is tn5250j (a Java application). Connect to pub400.com, port 23 (or 992 with SSL), and sign in.

You land on the Main menu. Everything is typed on the command line (===>):

WRKOBJ OBJ(YOURLIB/*ALL) OBJTYPE(*DTAQ) -- list the data queues of a library CRTDTAQ DTAQ(YOURLIB/MYQUEUE) MAXLEN(30) -- create a data queue 

MAXLEN(30) means a message can be up to 30 bytes. That limit matters later.

Tip. Commands starting with WRK ("work with") open a list; type 5 in front of a line to display it, 4 to delete it.

You can also run SQL from the terminal with STRSQL. IBM i has a large set of ready-made SQL services in the QSYS2 schema, which is exactly what we will use from .NET.

3. Reading a data queue with plain SQL

The surprise: you do not need a special IBM library to read a data queue from .NET. IBM i exposes it as an SQL table function, so any ODBC/ADO.NET connection works:

SELECT MESSAGE_DATA_BINARY, KEY_DATA FROM TABLE( QSYS2.RECEIVE_DATA_QUEUE( DATA_QUEUE => ?, DATA_QUEUE_LIBRARY => ?, REMOVE => 'YES', WAIT_TIME => ? ) ) 

WAIT_TIME makes the call wait for a message instead of polling in a loop. One row back means "here is a message"; no row means "the queue stayed empty".

To write a message from SQL (handy for testing):

CALL QSYS2.SEND_DATA_QUEUE_BINARY( MESSAGE_DATA => ?, DATA_QUEUE => ?, DATA_QUEUE_LIBRARY => ?) 

And to just look at what is in a queue without consuming it:

SELECT * FROM TABLE(QSYS2.DATA_QUEUE_ENTRIES('MYQUEUE','YOURLIB')) 

4. A small .NET library around it

The accompanying library is split into small pieces:

  • an IDataQueueClient that reads a message (the SQL client above implements it);
  • an IMessageCodec<T> that turns bytes into an object;
  • an IMessageHandler<T> with your business logic;
  • an IDeadLetterSink for messages that cannot be processed;
  • a pipeline that wires them together (decode → handle → retry → dead-letter) and a hosted service to run it.

Registering a consumer in an ASP.NET Core app looks like this:

builder.Services.AddSingleton<IDataQueueClient>(_ => new SqlDataQueueClient(OpenAsync)); builder.Services.AddSingleton<IDeadLetterSink, FeedDeadLetterSink>(); builder.Services.AddDataQueueConsumer<DemoMessage, DemoCodec, FeedHandler>( new QueueRef("YOURLIB", "MYQUEUE"), options => options.Wait = TimeSpan.FromSeconds(3)); 

A message here is a fixed-width record, the classic mainframe format: Id (6) + Type (4) + Payload (20) = exactly 30 bytes. The codec cuts it into fields:

public static readonly FixedWidthLayout Layout = new(Text, [6, 4, 20]); public DemoMessage Decode(ReadOnlySpan<byte> payload) { var fields = Layout.Read(payload); return new DemoMessage(fields[0], fields[1], fields[2]); } 

If a message does not have exactly 30 bytes, Decode throws. That is on purpose: the pipeline puts the bytes in the dead-letter store instead of crashing or silently dropping them. The message has already left the queue, so losing it would be the worst outcome.

5. Calling a COBOL program from C

Here is a complete "Hello world" in ILE COBOL that takes a name and returns a greeting and a return code:

 IDENTIFICATION DIVISION. PROGRAM-ID. HELLOCBL. DATA DIVISION. LINKAGE SECTION. 01 P-NAME PIC X(20). 01 P-GREET PIC X(40). 01 P-RC PIC S9(9) COMP-4. PROCEDURE DIVISION USING P-NAME P-GREET P-RC. MAIN. MOVE SPACES TO P-GREET. IF P-NAME = SPACES MOVE 1 TO P-RC MOVE "NAME IS REQUIRED" TO P-GREET ELSE MOVE 0 TO P-RC STRING "HELLO " DELIMITED BY SIZE P-NAME DELIMITED BY SPACE " FROM COBOL" DELIMITED BY SIZE INTO P-GREET END-IF. GOBACK. 

Two things to know about fixed-format COBOL: code starts in column 8, and the parameters of the program are the LINKAGE SECTION items.

Compile it on the IBM i:

CRTBNDCBL PGM(YOURLIB/HELLOCBL) SRCFILE(YOURLIB/QCBLLESRC) SRCMBR(HELLOCBL) 

The wrapper: an SQL procedure around the program

How does .NET call a COBOL program? The best way is to wrap it in an SQL stored procedure. It is a declaration, not new code:

CREATE OR REPLACE PROCEDURE YOURLIB.HELLOCBL (IN P_NAME CHAR(20), OUT P_GREET CHAR(40), OUT P_RC INTEGER) LANGUAGE COBOL PARAMETER STYLE GENERAL EXTERNAL NAME YOURLIB.HELLOCBL 

(Beginner trap: the language keyword is COBOL. CBLLE, the source type you use when compiling, is rejected here.)

Why a wrapper? You keep the COBOL program unchanged, you get typed in/out parameters, and .NET uses the same ODBC connection it already has. No extra server, no extra driver.

From C#:

command.CommandText = "CALL YOURLIB.HELLOCBL(?, ?, ?)"; command.Parameters.Add(new OdbcParameter { OdbcType = OdbcType.Char, Size = 20, Value = name.PadRight(20) }); var greeting = new OdbcParameter { Direction = ParameterDirection.Output, OdbcType = OdbcType.Char, Size = 40 }; var returnCode = new OdbcParameter { Direction = ParameterDirection.Output, OdbcType = OdbcType.Int }; command.Parameters.Add(greeting); command.Parameters.Add(returnCode); await command.ExecuteNonQueryAsync(); // greeting.Value -> "HELLO WORLD FROM COBOL" // returnCode.Value -> 0 

Calling the COBOL program from the web page

6. The other direction: COBOL publishes, .NET consumes

This is the scenario data queues were made for. A second COBOL program writes a message with the system API QSNDDTAQ:

 WORKING-STORAGE SECTION. 01 DQ-NAME PIC X(10) VALUE "MYQUEUE". 01 DQ-LIB PIC X(10) VALUE "YOURLIB". 01 DQ-LEN PIC S9(5) COMP-3 VALUE 30. 01 DQ-DATA. 05 DQ-ID PIC X(6). 05 DQ-TYPE PIC X(4). 05 DQ-PAYLOAD PIC X(20). ... CALL "QSNDDTAQ" USING DQ-NAME DQ-LIB DQ-LEN DQ-DATA. 

Wrapped in the same way, the web page can trigger it. The program writes, and the .NET consumer, which was already waiting on the queue, reads and decodes the message a fraction of a second later:

COBOL publishes into the data queue, .NET decodes it

The gotcha: EBCDIC

My first version of the .NET codec decoded messages as ASCII. It worked for messages sent from .NET (I encoded them as ASCII too) but would have shown garbage for anything written by COBOL, because IBM i stores text in EBCDIC. The letter A is byte 0x41 in ASCII and 0xC1 in EBCDIC.

The fix is one line, using the code page of the IBM i job (37 is US English; yours may differ, for example 500 or 1148):

Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); var text = Encoding.GetEncoding(37); 

You can even see it in the dead-letter screenshot: the text ABC appears as C1C2C3.

A message of the wrong size goes to the dead-letter list

7. See it all in a web page

The demo app is a single ASP.NET Core project. A page lets you:

  1. publish a message from .NET (publish/subscribe);
  2. call the COBOL program and show its answer;
  3. have COBOL publish a message;
  4. watch decoded messages and rejected ones appear live.

Publishing from .NET and seeing the decoded message

The consumer is a normal BackgroundService. The page just polls a small JSON endpoint once per second.

8. Bonus: the ODBC driver and a Docker workaround

You need the IBM i Access ODBC driver. On Windows it ships in IBM i Access Client Solutions, behind an IBM account (and I got stuck in an export-control review at download time). The Linux version, however, is published openly in IBM's package repository.

So I ran the app in a Linux Docker container with unixODBC and the driver, while still developing and debugging from Windows (attaching Visual Studio to the container). The Dockerfile is four lines:

FROM mcr.microsoft.com/dotnet/sdk:10.0 RUN apt-get update && apt-get install -y unixodbc \ && echo "deb [trusted=yes] https://public.dhe.ibm.com/software/ibmi/products/odbc/debs 1.1.0 main" \ > /etc/apt/sources.list.d/ibmi-acs.list \ && apt-get update && apt-get install -y ibm-iaccess 

What I learned

  • A data queue is a good integration point: simple, built in, and fast enough for most needs.
  • SQL is the easiest door into IBM i from .NET. Reading queues, sending messages, calling programs: all just SQL.
  • Wrap COBOL in a stored procedure instead of inventing a new protocol.
  • Plan for bad messages from day one: size limits, encoding, dead-letter.
  • Mind the encoding. EBCDIC will surprise you once, then never again.
  • Don't put your password in the code. Read it from an environment variable.

The full source (library, tests, demo web app, COBOL programs and install script) is on GitHub: github.com/ManuDinicola/IbmiDataQueue

If you found this useful, a clap helps others find it. Questions or corrections are welcome in the comments.

Note: This article was created with the assistance of AI.