Call the QUSLOBJ API from .NET with NTi

Introduction

IBM i provides many system APIs for accessing operating system resources. Some return data directly, while others require a User Space to store results before they can be processed.

The QUSLOBJ API (List Objects) falls into this second category. Rather than returning the object list directly, it writes the results into a User Space, a temporary memory area. This pattern is common for IBM i APIs that handle data sets of variable size, as it allows large volumes of information to be retrieved without strict size limitations.

With NTi, system APIs are called like ordinary programs, through CallProgram and NTiProgramParameter. This example covers how to implement a method to call this API from .NET, and how to read the IBM documentation to correctly build the input structures and parse the returned data.

💡 The complete source code is available here. Follow each step to understand how it works, then refer to the summary for direct use.


Step 1 - Understand the QSYS/QUSLOBJ API

Start by reading the IBM documentation for QUSLOBJ.

Calling this API follows this process:

  • Create a User Space to store the results using the QUSCRTUS API.
  • Call the QUSLOBJ API with the expected input parameters.
  • Read the results stored in the User Space using the QUSRTVUS API.
  • Iterate over the returned objects and map them to .NET objects.

Step 2 - Read the IBM documentation

Before calling QUSLOBJ, here is what the API expects as input parameters and what it returns.

Input parameters

Parameter Type Direction Description
User Space Char(20) Input Name (10 characters) and library (10 characters) where to store the list
Format Name Char(8) Input Data format (OBJL0400 in this example)
Object & LibraryName Char(20) Input Object name (10 characters) and library (10 characters)
Object Type Char(10) Input Object type (*FILE, *PGM, etc.)
Error Code Char(*) Input / Output Standard API error structure (optional)

The standard API error structure starts with two BINARY(4) counters: bytes provided (the size of the structure you supply) and bytes available (filled in by the API on return, 0 if no error), followed by the error message ID as CHAR(7) at offset 8, then one reserved byte. We will come back to it at call time.

Output parameters

QUSLOBJ does not return the object list directly as output parameters. It stores the results in a User Space, a temporary memory area that must be specified as an input parameter.

A User Space is a system object used to store large amounts of data, including results returned by certain IBM i APIs.

  • It is created before calling QUSLOBJ using the QUSCRTUS API.
  • Once QUSLOBJ has run, the listed objects are written into this User Space.
  • To retrieve the results, the QUSRTVUS API is used to read its content.

💡 All returned data is stored in this User Space and must be parsed according to the structure defined by IBM.


Step 3 - Build the QUSLOBJ call method

Define the data model

Define a C# class representing each object returned by the API. The OBJL0400 format is used to retrieve detailed information:

using System;
public class ListObjectsInformation
{
    public string? ObjectName { get; set; }
    public string? LibraryName { get; set; }
    public string? ObjectType { get; set; }
    public string? InformationStatus { get; set; }
    public string? ExtendedAttribute { get; set; }
    public string? TextDescription { get; set; }
    public string? UserDefinedAttribute { get; set; }
    public int AspNumber { get; set; }
    public string? Owner { get; set; }
    public string? ObjectDomain { get; set; }
    public DateTime CreationDateTime { get; set; }
    public DateTime ChangeDateTime { get; set; }
    public string? StorageStatus { get; set; }
    public string? CompressionStatus { get; set; }
    public string? AllowChangeByProgram { get; set; }
    public string? ChangedByProgram { get; set; }
    public string? ObjectAuditing { get; set; }
    public string? IsDigitallySigned { get; set; }
    public string? IsSystemTrustedSigned { get; set; }
    public string? HasMultipleSignatures { get; set; }
    public int LibraryAspNumber { get; set; }
    public string? SourceFileName { get; set; }
    public string? SourceFileLibrary { get; set; }
    public string? SourceFileMember { get; set; }
    public string? SourceFileUpdatedDateTime { get; set; }
    public string? CreatorUserProfile { get; set; }
    public string? CreationSystem { get; set; }
    public string? SystemLevel { get; set; }
    public string? Compiler { get; set; }
    public string? ObjectLevel { get; set; }
    public string? IsUserChanged { get; set; }
    public string? LicensedProgram { get; set; }
    public string? PTF { get; set; }
    public string? APAR { get; set; }
    public string? PrimaryGroup { get; set; }
    public string? IsOptimallyAligned { get; set; }
    public int PrimaryAssociatedSpaceSize { get; set; }
}

Open the connection

Declare a NTiConnection instance and open the connection. Asynchronous open is the normal path in a server application. Open() remains available for synchronous code:

using Aumerial.Data.Nti;
using var conn = new NTiConnection("server=serverName;user=userName;password=password");
await conn.OpenAsync(); // or conn.Open() synchronously

Define the main method

The method takes the open connection as its first parameter, then libraryName, objectName and objectType:

public static List RetrieveObjectList(
    NTiConnection conn,
    string libraryName,
    string objectName = "*ALL",
    string objectType = "*ALL")
{
    // Body built in the following steps
}

Create the User Space

Before calling QUSLOBJ, create a User Space in QTEMP via the QUSCRTUS API to store the generated object list.

Since the size of the returned data is not known in advance, a while loop is used to check whether the allocated space is sufficient and increase it if needed.

QUSCRTUS expects six parameters: the qualified User Space name (CHAR(20)), its extended attribute (CHAR(10)), its initial size (BINARY(4)), the initial value of each byte (CHAR(1)), the public authority granted on the object (CHAR(10), here *ALL) and its description (CHAR(50)).

    int initialSize = 10000;
    while (true)
    {
        // Create the User Space with the current size
        var initialParameters = new List
        {
            new NTiProgramParameter("NTILOBJ", 10).Append("QTEMP", 10),      // User Space name and library
            new NTiProgramParameter("QUSLOBJ", 10),                          // Extended attribute
            new NTiProgramParameter(initialSize),                            // Initial size
            new NTiProgramParameter(new byte[] { 0x00 }),                    // Initial value of each byte
            new NTiProgramParameter("*ALL", 10),                             // Public authority
            new NTiProgramParameter("List Object Information Userspace", 50) // Description
        };
        conn.CallProgram("QSYS", "QUSCRTUS", initialParameters);

💡 The User Space lives in the command job. NTi opens two server jobs per connection: SQL statements run in the database job (QZDASOINIT), while CL commands and program calls run in the command job (QZRCSRVS), and each job has its own QTEMP. QUSCRTUS, QUSLOBJ, QUSRTVUS and the final DLTUSRSPC all go through the command job, so they see the same QTEMP/NTILOBJ for the whole sequence. An SQL query on the same connection, however, will never see this User Space. The conn.DatabaseJob and conn.CommandJob properties identify both jobs.

Call the QUSLOBJ API

The API expects fixed-length Char parameters:

  • The User Space is a 20-character string (10 for the name, 10 for the library), hence .Append("QTEMP", 10) to concatenate.
  • The data format is an 8-character string ("OBJL0400").
  • The object name and library form a 20-character string (.Append(libraryName, 10)).
  • The object type is a 10-character string (*PGM, *FILE, etc.).
  • Finally, the 16-byte error structure: bytes provided as BINARY(4) initialized to 16, bytes available as BINARY(4) filled on return, then 8 bytes for the message ID and the reserved byte.
        // Error structure: bytes provided = 16, bytes available = 0, blank message area
        var errorCode = new NTiProgramParameter(16).Append(0).Append("", 8);
        var parameters = new List
        {
            new NTiProgramParameter("NTILOBJ", 10).Append("QTEMP", 10),      // User Space
            new NTiProgramParameter("OBJL0400", 8),                          // Data format
            new NTiProgramParameter(objectName, 10).Append(libraryName, 10), // Object and library
            new NTiProgramParameter(objectType, 10),                         // Object type
            errorCode                                                        // Error structure
        };
        conn.CallProgram("QSYS", "QUSLOBJ", parameters);

💡 If the API hits a problem (library not found, insufficient authority), it fills bytes available and writes the CPFxxxx error message ID as CHAR(7) at offset 8 of the structure: test errorCode.GetInt(4), then read errorCode.GetString(8, 7). Alternative approach: pass bytes provided = 0. The API then signals the error with an escape message and the call throws an NTiCommandException carrying the job message stack.

        if (errorCode.GetInt(4) > 0) // bytes available: 0 = no error
        {
            string messageId = errorCode.GetString(8, 7); // Message ID, e.g. CPF9810
            conn.ExecuteClCommand("DLTUSRSPC QTEMP/NTILOBJ");
            throw new InvalidOperationException($"QUSLOBJ reported error {messageId}");
        }

Retrieve the results

Use the QUSRTVUS API to read the User Space contents:

  • The first parameter is the User Space (NTILOBJ in QTEMP).
  • The second parameter is the starting position of the read, 1-based: the value 1 means "read from the first byte of the User Space".
  • The third parameter is the number of bytes to read, here the allocated size.
  • The last parameter is the receiver, an empty string of the User Space size, marked as output (.AsOutput()), where the data will be written.
        var finalParameters = new List
        {
            new NTiProgramParameter("NTILOBJ", 10).Append("QTEMP", 10), // User Space
            new NTiProgramParameter(1),                                 // Starting position (1-based)
            new NTiProgramParameter(initialSize),                       // Number of bytes to read
            new NTiProgramParameter("", initialSize).AsOutput()         // Receiver
        };
        conn.CallProgram("QSYS", "QUSRTVUS", finalParameters);

Parse the returned data

First extract the generic header of the User Space. The general data structure for list APIs is documented here.

  • offsetToData - offset where the object list starts
  • listSize - total size of the list in the User Space
  • numberOfEntries - number of returned objects
  • entrySize - number of bytes allocated per object
        var receiver = finalParameters[3]; // 4th parameter (zero-indexed)
        var offsetToData = receiver.GetInt(0x7C);    // Offset where the list starts
        var listSize = receiver.GetInt(0x80);        // Total size of the list
        var numberOfEntries = receiver.GetInt(0x84); // Number of returned entries
        var entrySize = receiver.GetInt(0x88);       // Size of each entry

If the total size exceeds the allocated space, delete the User Space, increase the size and restart the call:

        var totalSize = listSize + offsetToData;
        if (totalSize > initialSize)
        {
            conn.ExecuteClCommand("DLTUSRSPC QTEMP/NTILOBJ");
            initialSize = totalSize;
            continue; // The loop recreates the User Space with the new size
        }

If the space is sufficient, iterate over the object list to extract their data. Each object returned by the API is stored as a fixed-position data block. The structure is defined by IBM and follows a strict layout where each field starts at a specific offset and has a fixed length.

Loop through the object list based on the number of returned entries. At each iteration, calculate the exact position of the current object by adding the data start offset to the entry index multiplied by the entry size. This allows pointing directly to the object being parsed.

Use the appropriate methods depending on the data type:

  • GetString(offset, length).Trim() - extracts a fixed-length string and trims whitespace
  • GetInt(offset) - retrieves a binary numeric value (BINARY(4))
  • GetDTSTimestamp(offset) - NTi-specific method that converts an 8-byte IBM i *DTS timestamp into a .NET DateTime
        var result = new List();
        for (int i = 0; i < numberOfEntries; i++)
        {
            // Offset of the current entry in the User Space
            int currentOffset = offsetToData + (i * entrySize);
            result.Add(new ListObjectsInformation
            {
                ObjectName = receiver.GetString(currentOffset, 10).Trim(),             // Offset 0, CHAR(10)
                LibraryName = receiver.GetString(currentOffset + 10, 10).Trim(),       // Offset 10, CHAR(10)
                ObjectType = receiver.GetString(currentOffset + 20, 10).Trim(),        // Offset 20, CHAR(10)
                InformationStatus = receiver.GetString(currentOffset + 30, 1).Trim(),  // Offset 30, CHAR(1)
                ExtendedAttribute = receiver.GetString(currentOffset + 31, 10).Trim(), // Offset 31, CHAR(10)
                TextDescription = receiver.GetString(currentOffset + 41, 50).Trim(),   // Offset 41, CHAR(50)
                UserDefinedAttribute = receiver.GetString(currentOffset + 91, 10).Trim(), // Offset 91, CHAR(10)
                AspNumber = receiver.GetInt(currentOffset + 108),                      // Offset 108, BINARY(4)
                Owner = receiver.GetString(currentOffset + 112, 10).Trim(),            // Offset 112, CHAR(10)
                ObjectDomain = receiver.GetString(currentOffset + 122, 2).Trim(),      // Offset 122, CHAR(2)
                CreationDateTime = receiver.GetDTSTimestamp(currentOffset + 124),      // Offset 124, *DTS timestamp
                ChangeDateTime = receiver.GetDTSTimestamp(currentOffset + 132),        // Offset 132, *DTS timestamp
                StorageStatus = receiver.GetString(currentOffset + 140, 10).Trim(),    // Offset 140, CHAR(10)
                CompressionStatus = receiver.GetString(currentOffset + 150, 1).Trim()  // Offset 150, CHAR(1)
                // The summary maps the remaining OBJL0400 fields
            });
        }

💡 Watch the lengths: at offset 150, the compression status is a CHAR(1). Reading 10 bytes there would overflow into the following fields (allow change by program at offset 151, changed by program at offset 152).

Once the data has been extracted and stored as C# objects, delete the temporary User Space and return the result. The for loop naturally handles the "no entries" case by returning an empty list:

        conn.ExecuteClCommand("DLTUSRSPC QTEMP/NTILOBJ");
        return result;

Summary

The complete method, ready to compile. The connection is passed as a parameter and the entry mapping is isolated in a private method, reused by the async variant below. The ListObjectsInformation class is the one from step 3.

using System;
using System.Collections.Generic;
using Aumerial.Data.Nti;
public static class ObjectLister
{
    public static List RetrieveObjectList(
        NTiConnection conn,
        string libraryName,
        string objectName = "*ALL",
        string objectType = "*ALL")
    {
        int initialSize = 10000;
        while (true)
        {
            // 1- Create the User Space
            var initialParameters = new List
            {
                new NTiProgramParameter("NTILOBJ", 10).Append("QTEMP", 10),      // Name and library
                new NTiProgramParameter("QUSLOBJ", 10),                          // Extended attribute
                new NTiProgramParameter(initialSize),                            // Initial size
                new NTiProgramParameter(new byte[] { 0x00 }),                    // Initial value
                new NTiProgramParameter("*ALL", 10),                             // Public authority
                new NTiProgramParameter("List Object Information Userspace", 50) // Description
            };
            conn.CallProgram("QSYS", "QUSCRTUS", initialParameters);
            // 2- Call the QUSLOBJ API
            var errorCode = new NTiProgramParameter(16).Append(0).Append("", 8); // bytes provided = 16
            var parameters = new List
            {
                new NTiProgramParameter("NTILOBJ", 10).Append("QTEMP", 10),      // User Space
                new NTiProgramParameter("OBJL0400", 8),                          // Data format
                new NTiProgramParameter(objectName, 10).Append(libraryName, 10), // Object and library
                new NTiProgramParameter(objectType, 10),                         // Object type
                errorCode                                                        // Error structure
            };
            conn.CallProgram("QSYS", "QUSLOBJ", parameters);
            if (errorCode.GetInt(4) > 0) // bytes available: 0 = no error
            {
                string messageId = errorCode.GetString(8, 7); // Message ID, e.g. CPF9810
                conn.ExecuteClCommand("DLTUSRSPC QTEMP/NTILOBJ");
                throw new InvalidOperationException($"QUSLOBJ reported error {messageId}");
            }
            // 3- Read the User Space
            var finalParameters = new List
            {
                new NTiProgramParameter("NTILOBJ", 10).Append("QTEMP", 10), // User Space
                new NTiProgramParameter(1),                                 // Starting position (1-based)
                new NTiProgramParameter(initialSize),                       // Number of bytes to read
                new NTiProgramParameter("", initialSize).AsOutput()         // Receiver
            };
            conn.CallProgram("QSYS", "QUSRTVUS", finalParameters);
            var receiver = finalParameters[3];
            var offsetToData = receiver.GetInt(0x7C);    // Offset where the list starts
            var listSize = receiver.GetInt(0x80);        // Total size of the list
            var numberOfEntries = receiver.GetInt(0x84); // Number of returned entries
            var entrySize = receiver.GetInt(0x88);       // Size of each entry
            // 4- Grow the User Space if needed
            var totalSize = listSize + offsetToData;
            if (totalSize > initialSize)
            {
                conn.ExecuteClCommand("DLTUSRSPC QTEMP/NTILOBJ");
                initialSize = totalSize;
                continue;
            }
            // 5- Map the entries, then delete the User Space
            var result = ParseEntries(receiver, offsetToData, numberOfEntries, entrySize);
            conn.ExecuteClCommand("DLTUSRSPC QTEMP/NTILOBJ");
            return result;
        }
    }
    private static List ParseEntries(
        NTiProgramParameter receiver, int offsetToData, int numberOfEntries, int entrySize)
    {
        var result = new List();
        for (int i = 0; i < numberOfEntries; i++)
        {
            int currentOffset = offsetToData + (i * entrySize);
            result.Add(new ListObjectsInformation
            {
                ObjectName = receiver.GetString(currentOffset, 10).Trim(),
                LibraryName = receiver.GetString(currentOffset + 10, 10).Trim(),
                ObjectType = receiver.GetString(currentOffset + 20, 10).Trim(),
                InformationStatus = receiver.GetString(currentOffset + 30, 1).Trim(),
                ExtendedAttribute = receiver.GetString(currentOffset + 31, 10).Trim(),
                TextDescription = receiver.GetString(currentOffset + 41, 50).Trim(),
                UserDefinedAttribute = receiver.GetString(currentOffset + 91, 10).Trim(),
                AspNumber = receiver.GetInt(currentOffset + 108),
                Owner = receiver.GetString(currentOffset + 112, 10).Trim(),
                ObjectDomain = receiver.GetString(currentOffset + 122, 2).Trim(),
                CreationDateTime = receiver.GetDTSTimestamp(currentOffset + 124),
                ChangeDateTime = receiver.GetDTSTimestamp(currentOffset + 132),
                StorageStatus = receiver.GetString(currentOffset + 140, 10).Trim(),
                CompressionStatus = receiver.GetString(currentOffset + 150, 1).Trim(), // CHAR(1), not 10
                AllowChangeByProgram = receiver.GetString(currentOffset + 151, 1).Trim(),
                ChangedByProgram = receiver.GetString(currentOffset + 152, 1).Trim(),
                ObjectAuditing = receiver.GetString(currentOffset + 153, 10).Trim(),
                IsDigitallySigned = receiver.GetString(currentOffset + 163, 1).Trim(),
                IsSystemTrustedSigned = receiver.GetString(currentOffset + 164, 1).Trim(),
                HasMultipleSignatures = receiver.GetString(currentOffset + 165, 1).Trim(),
                LibraryAspNumber = receiver.GetInt(currentOffset + 168),
                SourceFileName = receiver.GetString(currentOffset + 172, 10).Trim(),
                SourceFileLibrary = receiver.GetString(currentOffset + 182, 10).Trim(),
                SourceFileMember = receiver.GetString(currentOffset + 192, 10).Trim(),
                SourceFileUpdatedDateTime = receiver.GetString(currentOffset + 202, 13).Trim(),
                CreatorUserProfile = receiver.GetString(currentOffset + 215, 10).Trim(),
                CreationSystem = receiver.GetString(currentOffset + 225, 8).Trim(),
                SystemLevel = receiver.GetString(currentOffset + 233, 9).Trim(),
                Compiler = receiver.GetString(currentOffset + 242, 16).Trim(),
                ObjectLevel = receiver.GetString(currentOffset + 258, 8).Trim(),
                IsUserChanged = receiver.GetString(currentOffset + 266, 1).Trim(),
                LicensedProgram = receiver.GetString(currentOffset + 267, 16).Trim(),
                PTF = receiver.GetString(currentOffset + 283, 10).Trim(),
                APAR = receiver.GetString(currentOffset + 293, 10).Trim(),
                PrimaryGroup = receiver.GetString(currentOffset + 303, 10).Trim(),
                IsOptimallyAligned = receiver.GetString(currentOffset + 315, 1).Trim(),
                PrimaryAssociatedSpaceSize = receiver.GetInt(currentOffset + 316)
            });
        }
        return result;
    }
}

Async variant

Every building block used here exists as real async: OpenAsync, CallProgramAsync and ExecuteClCommandAsync accept a cancellation token and perform no synchronous I/O. This is the recommended path in a server application. Add using System.Threading; and using System.Threading.Tasks; at the top of the file, then extend the ObjectLister class:

    public static async Task> RetrieveObjectListAsync(
        NTiConnection conn,
        string libraryName,
        string objectName = "*ALL",
        string objectType = "*ALL",
        CancellationToken cancellationToken = default)
    {
        int initialSize = 10000;
        while (true)
        {
            // 1- Create the User Space
            var initialParameters = new List
            {
                new NTiProgramParameter("NTILOBJ", 10).Append("QTEMP", 10),
                new NTiProgramParameter("QUSLOBJ", 10),
                new NTiProgramParameter(initialSize),
                new NTiProgramParameter(new byte[] { 0x00 }),
                new NTiProgramParameter("*ALL", 10),
                new NTiProgramParameter("List Object Information Userspace", 50)
            };
            await conn.CallProgramAsync("QSYS", "QUSCRTUS", initialParameters, cancellationToken);
            // 2- Call the QUSLOBJ API
            var errorCode = new NTiProgramParameter(16).Append(0).Append("", 8);
            var parameters = new List
            {
                new NTiProgramParameter("NTILOBJ", 10).Append("QTEMP", 10),
                new NTiProgramParameter("OBJL0400", 8),
                new NTiProgramParameter(objectName, 10).Append(libraryName, 10),
                new NTiProgramParameter(objectType, 10),
                errorCode
            };
            await conn.CallProgramAsync("QSYS", "QUSLOBJ", parameters, cancellationToken);
            if (errorCode.GetInt(4) > 0)
            {
                string messageId = errorCode.GetString(8, 7);
                await conn.ExecuteClCommandAsync("DLTUSRSPC QTEMP/NTILOBJ", cancellationToken);
                throw new InvalidOperationException($"QUSLOBJ reported error {messageId}");
            }
            // 3- Read the User Space
            var finalParameters = new List
            {
                new NTiProgramParameter("NTILOBJ", 10).Append("QTEMP", 10),
                new NTiProgramParameter(1),
                new NTiProgramParameter(initialSize),
                new NTiProgramParameter("", initialSize).AsOutput()
            };
            await conn.CallProgramAsync("QSYS", "QUSRTVUS", finalParameters, cancellationToken);
            var receiver = finalParameters[3];
            var offsetToData = receiver.GetInt(0x7C);
            var listSize = receiver.GetInt(0x80);
            var numberOfEntries = receiver.GetInt(0x84);
            var entrySize = receiver.GetInt(0x88);
            // 4- Grow the User Space if needed
            var totalSize = listSize + offsetToData;
            if (totalSize > initialSize)
            {
                await conn.ExecuteClCommandAsync("DLTUSRSPC QTEMP/NTILOBJ", cancellationToken);
                initialSize = totalSize;
                continue;
            }
            // 5- Map the entries, then delete the User Space
            var result = ParseEntries(receiver, offsetToData, numberOfEntries, entrySize);
            await conn.ExecuteClCommandAsync("DLTUSRSPC QTEMP/NTILOBJ", cancellationToken);
            return result;
        }
    }

💡 Cancelling the token during an exchange breaks the connection by contract (the in-flight frame is lost) and results in an OperationCanceledException carrying your token. You then need to reopen the connection.


What's next?

Reconnecting to the server...

The connection to the server was lost. The page will reload.