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() synchronouslyDefine 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,QUSRTVUSand the finalDLTUSRSPCall go through the command job, so they see the sameQTEMP/NTILOBJfor the whole sequence. An SQL query on the same connection, however, will never see this User Space. Theconn.DatabaseJobandconn.CommandJobproperties 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
CPFxxxxerror message ID as CHAR(7) at offset 8 of the structure: testerrorCode.GetInt(4), then readerrorCode.GetString(8, 7). Alternative approach: pass bytes provided = 0. The API then signals the error with an escape message and the call throws anNTiCommandExceptioncarrying 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 (
NTILOBJinQTEMP). - The second parameter is the starting position of the read, 1-based: the value
1means "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 startslistSize- total size of the list in the User SpacenumberOfEntries- number of returned objectsentrySize- 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 whitespaceGetInt(offset)- retrieves a binary numeric value (BINARY(4))GetDTSTimestamp(offset)- NTi-specific method that converts an 8-byte IBM i *DTS timestamp into a .NETDateTime
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
OperationCanceledExceptioncarrying your token. You then need to reopen the connection.
What's next?
- Call a program : RPG program call with input/output parameters
- Stored procedure : SQL stored procedure call with Dapper and DataReader
- NTiProgramParameter : complete parameter class reference