DOCUMENTATION NTi

Calling the QUSLOBJ API in .NET with NTi

Introduction

The IBM i offers numerous system APIs for accessing operating system resources. Some of these APIs directly return usable data in the form of structures or streams, while others require the use of a User Space to store the results before they can be used. The QUSLOBJ (List Objects) API belongs to this second category: it doesn't directly return the list of objects as output, but writes the results to a User Space, a temporary memory zone. This mechanism is common in IBM i APIs that handle dynamic data occurrences, as it enables large volumes of information to be retrieved without strict limitations on the size of the results.

The aim of this example is to show you how to successfully implement a method with NTi to call this API from .NET, and how to interpret IBM documentation to correctly build input structures and analyze return data.

The complete implementation source code is available at the end of this example.
You can follow each step to understand how it works, then refer to it for an overview and direct use.

Step 1 - Choosing an API: QSYS.QUSLOBJ

To use the QUSLOBJ API, the first thing to do is consult its documentation.

The main steps are as follows:

  • Create a User Space to store results via the QUSCRTUS API.
  • Call the QUSLJOB API, specifying the expected input parameters.
  • Read results stored in UserSpace via the QUSRTVUS API.
  • Browse returned objects and map them to .NET objects.

Step 2 - Reading IBM documentation

Before implementing the call to QUSLOBJ, it's important to understand what this API expects as parameters and what it returns as output.

📥 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 our 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 Error structure (optional)

📥 Output parameters:

The QUSLOBJ API does not directly return the list of objects as output parameters. Instead, it stores the results in a User Space, a temporary memory area that we need to specify as input.

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

It is created before the call to QUSLOBJ using the QUSCRTUS API. Once QUSLOBJ has been run, the listed objects are written to this User Space. To retrieve the results, we use the QUSRTVUS API to read its contents.

We won't go into the details of the implementation here, but remember that all returned data is stored in this User Space and must be analyzed according to the structure defined by IBM.

Step 3 - Implementing the QUSLOBJ call method with NTi

We now know how QUSLOBJ works and how to interpret the results stored in User Space. Now let's implement our method in C# with NTi.

1️⃣ Data model definition

First of all, we need to define a data model to represent each object returned by the API. We use the OBJL0400 format to retrieve detailed information about the objects. We therefore create a C# class to reflect this structure:

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 int AspNumber { get; set; }
    public string? Owner { get; set; }
    public DateTime CreationDateTime { get; set; }
    public DateTime ChangeDateTime { get; set; }
    ...
}

2️⃣ Definition of the main method

We define the method we're going to build to retrieve a list of objects from a given library. The API expects libraryName, objectName and objectType as parameters.

public static List<ListObjectsInformation> RetrieveObjectList(
    string libraryName,
    string objectName = "*ALL",
    string objectType = "*ALL"
)
{
}

3️⃣ UserSpace creation

Before calling QUSLOBJ, we need to create a User Space in QTEMP, as seen above, which will allow us to retrieve the generated object list. The QUSCRTUS API is used to create this User Space. It needs to be supplied with several parameters, including :

  • The User Space name, here NTILOBJ in QTEMP.
  • An initial size that will be dynamically adjusted according to the data returned.
  • An initialization value, here 0x00, which means a blank space.
  • A generic attribute and a description.

As 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 necessary.

    int initialSize = 10000;
    bool spaceAllocated = false;

    while (!spaceAllocated)
    {
        // Create User Space with current size
        var initialParameters = new List<NTiProgramParameter>
        {
            new NTiProgramParameter("NTILOBJ", 10).Append("QTEMP", 10), // User Space name
            new NTiProgramParameter("QUSLOBJ", 10), // Extension name
            new NTiProgramParameter(initialSize), // Initial size
            new NTiProgramParameter(new byte[] { 0x00 }), // Initialization value
            new NTiProgramParameter("*ALL", 10), // Attributes
            new NTiProgramParameter("List Object Information Userspace", 50) // Description
        };

        conn.CallProgram("QSYS", "QUSCRTUS", initialParameters);
    

4️⃣ Call QUSLOBJ API Create UserSpace

Once the UserSpace has been created, we can call the QUSLOBJ API to retrieve the list of objects. The API expects several parameters as input, which must be correctly formatted to avoid errors.

The QUSLOBJ API expects parameters in the form of fixed-length chars:

  • 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").
  • 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 error structure is a 16-character string, empty here.

Once these parameters have been defined, we call our program and execute the API.

var parameters = new List<NTiProgramParameter>
{
    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
    new NTiProgramParameter("", 16) // Error structure (optional)
};

conn.CallProgram("QSYS", "QUSLOBJ", parameters);

5️⃣ Retrieving results

Once QUSLOBJ has been run, the results are stored in User Space. To retrieve them, we use the QUSRTVUS API to read their contents.

  • The first parameter always designates the User Space (NTILOBJ in QTEMP).
  • The second parameter is a fixed value (1), indicating the retrieval format.
  • The third parameter corresponds to the allocated User Space size, defined dynamically above.
  • Finally, the last parameter is an empty string the size of the User Space, marked as output (.AsOutput()), where the data will be written.
var finalParameters = new List<NTiProgramParameter>
{
    new NTiProgramParameter("NTILOBJ", 10).Append("QTEMP", 10),
    new NTiProgramParameter(1),
    new NTiProgramParameter(initialSize),
    new NTiProgramParameter("", initialSize).AsOutput() // Data recovery
};

conn.CallProgram("QSYS", "QUSRTVUS", finalParameters);

6️⃣ Analysis of returned data

We now need to extract and analyze the data contained in the User Space. The results are returned in the last parameter defined as output when calling QUSRTVUS. General information is retrieved: the data offset, the number of entries and the size of each entry. These values enable us to understand where and how to browse the list of returned objects. The general data structure for list APIs is available here.

  • The offsetToData start offset indicates where the list of objects actually begins.
  • The number of entries returned numberOfEntries indicates how many objects are listed.
  • The size of an entry entrySize which lets us know how many bytes are allocated to each object.
  • Total data size listSize, which corresponds to the space occupied in User Space.

The QUSLOBJ API can return an error code CPFxxxx if it encounters a problem. This code is stored in parameters[4], which corresponds to the 5ᵉ parameter of our call. We therefore need to extract it and clean it up to avoid control characters.

string error = new string(parameters[4].GetString(0, 16)
    .Where(c => !char.IsControl(c))
    .ToArray())
    .Trim();

var listOfParameters = finalParameters[3]; // 4th parameter (indexed from 0)
var offsetToData = listOfParameters.GetInt(0x7C);  // Data start offset
var numberOfEntries = listOfParameters.GetInt(0x84); // Number of entries returned
var entrySize = listOfParameters.GetInt(0x88); // Size of each entry
var listSize = listOfParameters.GetInt(0x80);   // Total data size

Next, we check that the space allocated for UserSpace is sufficient. If the total size of the listSize + offsetToData data exceeds the size initially allocated, you need to delete the User Space, increase its size and call QUSLOBJ again.

var totalSize = listSize + offsetToData;

if (totalSize > initialSize)
{
    conn.ExecuteClCommand("DLTUSRSPC QTEMP/NTILOBJ");
    initialSize = totalSize;
}

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

The approach is to loop through the list of objects, based on the number of entries returned. At each iteration, the exact position of the current object is calculated by adding the start offset of the data to the index of the entry multiplied by the size of an entry. This allows you to point directly at the object you wish to analyze.

To extract the information, we use methods adapted to the type of data:

  • GetString(offset, length).Trim() for text fields, which extracts a fixed-length string of characters, removing unnecessary spaces.
  • GetInt(offset) for binary numerical values stored in 4 or 8 bytes, no length required.
  • GetDTSTimestamp(offset) is an NTi-specific method for converting an IBM i timestamp into a .NET date.

Once the values have been extracted, they are stored in a ListObjectsInformation object, then added to the results list.

var result = new List<ListObjectsInformation>();

if (numberOfEntries > 0)
{
    for (int i = 0; i < numberOfEntries; i++)
    {

        // Calculate offset of current entry in User Space
        int currentOffset = offsetToData + (i * entrySize);

        result.Add(new ListObjectsInformation
        {
            ObjectName = listOfParameters.GetString(currentOffset, 10).Trim(),   // offset 0, length 10
            LibraryName = listOfParameters.GetString(currentOffset + 10, 10).Trim(), // offset 10, length 10
            ObjectType = listOfParameters.GetString(currentOffset + 20, 10).Trim(),  // offset  20, length 10
            InformationStatus = listOfParameters.GetString(currentOffset + 30, 1).Trim(), // offset 30, length 1
            ExtendedAttribute = listOfParameters.GetString(currentOffset + 31, 10).Trim(), // offset 31, length 10
            TextDescription = listOfParameters.GetString(currentOffset + 41, 50).Trim(), // offset, length 50
            UserDefinedAttribute = listOfParameters.GetString(currentOffset + 91, 10).Trim(), // offset, length 10
            AspNumber = listOfParameters.GetInt(currentOffset + 108), // offset 108, binary 4
            Owner = listOfParameters.GetString(currentOffset + 112, 10).Trim(), // offset 112, length 10
            ObjectDomain = listOfParameters.GetString(currentOffset + 122, 2).Trim(), // offset 122, length 2
            CreationDateTime = listOfParameters.GetDTSTimestamp(currentOffset + 124), // offset 124, specific NTi format (timestamp)
            ChangeDateTime = listOfParameters.GetDTSTimestamp(currentOffset + 132), // offset 132, specific NTi format (timestamp)
            StorageStatus = listOfParameters.GetString(currentOffset + 140, 10).Trim() // offset 140, length 10
        });
    }
}

Once all the data has been extracted and stored as C# objects, it's important to clean up the memory by deleting the temporary User Space. Although User Space is stored in QTEMP (and therefore deleted at the end of the session), it's best to delete it immediately after use to avoid unnecessary overload if several calls are made continuously.

We use the CL DLTUSRSPC command to delete the specified User Space. Once deleted, the method returns a list of extracted objects. If no objects have been found by the API, an empty list is returned to ensure clean results management and avoid possible errors in downstream processing.

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

If no entries have been retrieved, we return an empty list:

return new List<ListObjectsInformation>();

Conclusion

With this last step, our method is fully functional. It follows a structured process that enables :

  • Dynamic creation of a User Space.
  • Calling QUSLOBJ to retrieve the list of objects.
  • Data extraction according to the structure defined by IBM.
  • Dynamic memory management, adjusting the size of the User Space if necessary.
  • Transformation of results into usable C# objects.
  • Systematic deletion of User Space after use.
public static List<ListObjectsInformation> RetrieveObjectList(
    string libraryName,
    string objectName = "*ALL",
    string objectType = "*ALL"
    )
{
    int initialSize = 10000;
    bool spaceAllocated = false;

    List<ListObjectsInformation> listResult = new List<ListObjectsInformation>();
    string errorCode = null;

    while (!spaceAllocated)
    {
        // 1- Création user space
        var initialParameters = new List<NTiProgramParameter>
        {
            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)
        };
        conn.CallProgram("QSYS", "QUSCRTUS", initialParameters);

        // 2- Appel de l'API QUSLOBJ

        var parameters = new List<NTiProgramParameter>
        {
            new NTiProgramParameter("NTILOBJ", 10).Append("QTEMP", 10),
            new NTiProgramParameter("OBJL0400", 8),
            new NTiProgramParameter(objectName, 10).Append(libraryName, 10),
            new NTiProgramParameter(objectType, 10),
            new NTiProgramParameter("", 16)
        };
        conn.CallProgram("QSYS", "QUSLOBJ", parameters);

        // 3. Récupération des données

        var finalParameters = new List<NTiProgramParameter>
        {
            new NTiProgramParameter("NTILOBJ", 10).Append("QTEMP", 10),
            new NTiProgramParameter(1),
            new NTiProgramParameter(initialSize),
            new NTiProgramParameter("", initialSize).AsOutput()
        };
        conn.CallProgram("QSYS", "QUSRTVUS", finalParameters);

        string error = new string(parameters[4].GetString(0, 16).Where(c => !char.IsControl(c)).ToArray()).Trim();

        var listOfParameters = finalParameters[3];
        var offsetToData = listOfParameters.GetInt(0x7C);
        var numberOfEntries = listOfParameters.GetInt(0x84);
        var entrySize = listOfParameters.GetInt(0x88);
        var listSize = listOfParameters.GetInt(0x80);


        var totalSize = listSize + offsetToData;

        if(totalSize > initialSize)
        {
            conn.ExecuteClCommand("DLTUSRSPC QTEMP/NTILOBJ");
            initialSize = totalSize;
        }

        else
        {
            spaceAllocated = true;
            var result = new List<ListObjectsInformation>();

            if (numberOfEntries <= 0)
            {
                return new List<ListObjectsInformation>();
            }

            for(int i = 0; i < numberOfEntries; i++)
            {
                int currentOffset = offsetToData + (i * entrySize);

                result.Add(new ListObjectsInformation
                {
                    ObjectName = listOfParameters.GetString(currentOffset, 10).Trim(),
                    LibraryName = listOfParameters.GetString(currentOffset + 10 , 10).Trim(),
                    ObjectType = listOfParameters.GetString(currentOffset + 20, 10).Trim(),
                    InformationStatus = listOfParameters.GetString(currentOffset + 30, 1).Trim(),
                    ExtendedAttribute = listOfParameters.GetString(currentOffset + 31, 10).Trim(),
                    TextDescription = listOfParameters.GetString(currentOffset + 41, 50).Trim(),
                    UserDefinedAttribute = listOfParameters.GetString(currentOffset + 91, 10).Trim(),
                    AspNumber = listOfParameters.GetInt(currentOffset + 108),
                    Owner = listOfParameters.GetString(currentOffset + 112, 10).Trim(),
                    ObjectDomain = listOfParameters.GetString(currentOffset + 122, 2).Trim(),
                    CreationDateTime = listOfParameters.GetDTSTimestamp(currentOffset + 124),
                    ChangeDateTime = listOfParameters.GetDTSTimestamp(currentOffset + 132),
                    StorageStatus = listOfParameters.GetString(currentOffset + 140, 10).Trim(),
                    CompressionStatus = listOfParameters.GetString(currentOffset + 150 , 10).Trim(),
                    AllowChangeByProgram = listOfParameters.GetString(currentOffset + 151, 1).Trim(),
                    ChangedByProgram = listOfParameters.GetString(currentOffset + 152, 1).Trim(),
                    ObjectAuditing = listOfParameters.GetString(currentOffset + 153, 10).Trim(),
                    IsDigitallySigned = listOfParameters.GetString(currentOffset + 163, 1).Trim(),
                    IsSystemTrustedSigned = listOfParameters.GetString(currentOffset + 164, 1).Trim(),
                    HasMultipleSignatures = listOfParameters.GetString(currentOffset + 165, 1).Trim(),
                    LibraryAspNumber = listOfParameters.GetInt(currentOffset + 168),
                    SourceFileName = listOfParameters.GetString(currentOffset + 172, 10).Trim(),
                    SourceFileLibrary = listOfParameters.GetString(currentOffset + 182, 10).Trim(),
                    SourceFileMember = listOfParameters.GetString(currentOffset + 192, 10).Trim(),
                    SourceFileUpdatedDateTime = listOfParameters.GetString(currentOffset + 202, 13).Trim(),
                    CreatorUserProfile = listOfParameters.GetString(currentOffset + 215, 10).Trim(),
                    CreationSystem = listOfParameters.GetString(currentOffset + 225, 8).Trim(),
                    SystemLevel = listOfParameters.GetString(currentOffset + 233, 9).Trim(),
                    Compiler = listOfParameters.GetString(currentOffset + 242, 16).Trim(),
                    ObjectLevel = listOfParameters.GetString(currentOffset + 258, 8).Trim(),
                    IsUserChanged = listOfParameters.GetString(currentOffset + 266, 1).Trim(),
                    LicensedProgram = listOfParameters.GetString(currentOffset + 267, 16).Trim(),
                    PTF = listOfParameters.GetString(currentOffset + 283, 10).Trim(),
                    APAR = listOfParameters.GetString(currentOffset + 293, 10).Trim(),
                    PrimaryGroup = listOfParameters.GetString(currentOffset + 303, 10).Trim(),
                    IsOptimallyAligned = listOfParameters.GetString(currentOffset + 315, 1).Trim(),
                    PrimaryAssociatedSpaceSize = listOfParameters.GetInt(currentOffset + 316)
                });
            }
            conn.ExecuteClCommand("DLTUSRSPC QTEMP/NTILOBJ");
            return result;
        }
    }
    return new List<ListObjectsInformation>();
}