Appeler l’API QUSLOBJ en .NET avec NTi

Introduction

L'IBM i propose de nombreuses API système permettant d’accéder aux ressources du système d’exploitation. Certaines renvoient directement des données exploitables, tandis que d’autres nécessitent l’utilisation d’un User Space pour stocker les résultats avant de pouvoir les exploiter.

L’API QUSLOBJ (List Objects) fait partie de cette deuxième catégorie : elle ne retourne pas directement la liste des objets en sortie, mais écrit les résultats dans un User Space, une zone mémoire temporaire. Ce mécanisme est fréquent pour les API IBM i qui gèrent des occurrences de données dynamiques, car il permet de récupérer des volumes importants d’informations sans limitation stricte sur la taille des résultats.

Cet exemple explique comment implémenter une méthode avec NTi pour appeler cette API depuis .NET, et comment interpréter la documentation IBM pour construire correctement les structures d'entrée et analyser les données de retour.

💡Le code source complet de l’implémentation est disponible ici. Suivez chaque étape pour comprendre le fonctionnement, puis référez-vous au récapitulatif pour une utilisation directe.


Étape 1 - Comprendre l'API QSYS.QUSLOBJ

La première étape consiste à consulter la documentation IBM de QUSLOBJ.

L'appel à cette API suit le processus suivant :

  • Créer un User Space pour stocker les résultats via l'API QUSCRTUS.
  • Appeler l'API QUSLOBJ en spécifiant les paramètres d'entrée attendus.
  • Lire les résultats stockés dans le User Space via l'API QUSRTVUS.
  • Parcourir les objets retournés et les mapper en objets .NET.

Étape 2 - Lire la documentation IBM

Avant d’implémenter l’appel à QUSLOBJ, voici ce que cette API attend comme paramètres et ce qu'elle renvoie en sortie.

Paramètres d’entrée

Paramètre Type Direction Description
User Space Char(20) Input Nom (10 caractères) et bibliothèque (10 caractères) où stocker la liste
Format Name Char(8) Input Format de données (OBJL0400 dans notre exemple)
Object & LibraryName Char(20) Input Nom de l’objet (10 caractères) et bibliothèque (10 caractères)
Object Type Char(10) Input Type d’objet (*FILE, *PGM, etc.)
Error Code Char(* ) Input / Output Structure d'erreur (optionnelle)

Paramètres de sortie

L’API QUSLOBJ ne retourne pas directement la liste des objets en tant que paramètres de sortie. Elle stocke les résultats dans un User Space, une zone mémoire temporaire que nous devons spécifier en entrée.

Un User Space est un objet système utilisé pour stocker de grandes quantités de données, notamment les résultats renvoyés par certaines API IBM i.

  • Il est créé avant l’appel à QUSLOBJ à l’aide de l’API QUSCRTUS.
  • Une fois QUSLOBJ exécuté, les objets listés sont écrits dans ce User Space.
  • Pour récupérer les résultats, on utilise l’API QUSRTVUS qui permet de lire son contenu.

💡 Toutes les données retournées sont stockées dans ce User Space et devront être analysées en respectant la structure définie par IBM.


Étape 3 - Implémenter la méthode d'appel à QUSLOBJ

Définir le modèle de données

Définissez une classe C# représentant chaque objet retourné par l'API. Le format OBJL0400 est utilisé pour récupérer des informations détaillées :

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; }
    ...
}

Ouvrir la connexion

Déclarez une instance de NTiConnection et ouvrez la connexion :

using var conn = new NTiConnection("server=serverName;user=userName;password=password");
conn.Open();

Définir la méthode principale

La méthode accepte libraryName, objectName et objectType en paramètres :

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

Créer le User Space

Avant d'appeler QUSLOBJ, créez un User Space dans QTEMP via l'API QUSCRTUS qui permettra de récupérer la liste d'objet générée.
La taille des données renvoyées n’étant pas connue à l’avance, une boucle while est utilisée pour vérifier si l’espace alloué est suffisant et l’augmenter si nécessaire.

    int initialSize = 10000;
    bool spaceAllocated = false;

    while (!spaceAllocated)
    {
        // Création du User Space avec la taille actuelle
        var initialParameters = new List<NTiProgramParameter>
        {
            new NTiProgramParameter("NTILOBJ", 10).Append("QTEMP", 10), // Nom du User Space
            new NTiProgramParameter("QUSLOBJ", 10), // Nom d’extension
            new NTiProgramParameter(initialSize), // Taille initiale
            new NTiProgramParameter(new byte[] { 0x00 }), // Valeur d'initialisation (espace vierge)
            new NTiProgramParameter("*ALL", 10), // Attributs
            new NTiProgramParameter("List Object Information Userspace", 50) // Description
        };
        conn.CallProgram("QSYS", "QUSCRTUS", initialParameters);

Appeler l'API QUSLOBJ

L'API attend des paramètres sous forme de Char de longueur fixe :

  • Le User Space est une chaîne de 20 caractères (10 pour le nom, 10 pour la bibliothèque), d’où .Append("QTEMP", 10) pour concaténer.
  • Le format des données est une chaîne de 8 caractères ("OBJL0400").
  • Le nom de l’objet et la bibliothèque forment une chaîne de 20 caractères (.Append(libraryName, 10)).
  • Le type d’objet est une chaîne de 10 caractères (*PGM, *FILE, etc.).
  • Enfin, la structure d’erreur est une chaîne de 16 caractères, vide ici.
var parameters = new List<NTiProgramParameter>
{
    new NTiProgramParameter("NTILOBJ", 10).Append("QTEMP", 10), // User Space
    new NTiProgramParameter("OBJL0400", 8), // Format des données
    new NTiProgramParameter(objectName, 10).Append(libraryName, 10), // Objet et bibliothèque
    new NTiProgramParameter(objectType, 10), // Type d’objet
    new NTiProgramParameter("", 16) // Structure d'erreur (optionnelle)
};

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

Récupérer les résultats

Utilisez l'API QUSRTVUS pour lire le contenu du User Space :

  • Le premier paramètre désigne le User Space (NTILOBJ dans QTEMP).
  • Le second paramètre est une valeur fixe (1), indiquant le format de récupération.
  • Le troisième paramètre correspond à la taille du User Space allouée, définie dynamiquement précédemment.
  • Le dernier paramètre est une chaîne vide de la taille du User Space, marquée en sortie (.AsOutput()), où les données seront écrites.
var finalParameters = new List<NTiProgramParameter>
{
    new NTiProgramParameter("NTILOBJ", 10).Append("QTEMP", 10),
    new NTiProgramParameter(1),
    new NTiProgramParameter(initialSize),
    new NTiProgramParameter("", initialSize).AsOutput() // Récupération des données
};

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

Analyse des données retournées

Extrayez les informations générales du User Space. La structure générale des données pour les API de liste est disponible ici.

  • offsetToData - offset indiquant où commence la liste des objets
  • numberOfEntries - nombre d'objets retournés
  • entrySize - nombre d'octets alloués à chaque objet
  • listSize - taille totale des données dans le User Space

💡L'API QUSLOBJ peut renvoyer un code erreur CPFxxxx si elle rencontre un problème. Ce code est stocké dans parameters[4] (5ᵉ paramètre de l'appel) et doit être extrait et nettoyé pour éviter les caractères de contrôle.

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

var listOfParameters = finalParameters[3];     // 4ème paramètre (indexé à partir de 0)
var offsetToData = listOfParameters.GetInt(0x7C);     // Offset du début des données
var numberOfEntries = listOfParameters.GetInt(0x84);     // Nombre d’entrées renvoyées
var entrySize = listOfParameters.GetInt(0x88);     // Taille de chaque entrée
var listSize = listOfParameters.GetInt(0x80);     // Taille totale des données

Si la taille totale dépasse l'espace alloué, supprimez le User Space, augmentez la taille et relancez l'appel :

var totalSize = listSize + offsetToData;

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

Si l’espace est suffisant, parcourez la liste des objets pour extraire leurs informations. Chaque objet retourné par l’API est stocké sous forme de blocs de données à une position bien précise. La structure est définie par IBM et suit une disposition stricte où chaque champ commence à un offset spécifique et a une longueur fixe.

L’approche consiste à parcourir la liste des objets en boucle, en se basant sur le nombre d’entrées retournées. À chaque itération, calculez la position exacte de l’objet courant en additionnant l’offset de début des données à l’index de l’entrée multiplié par la taille d’une entrée. Cela permet de pointer directement sur l’objet analysé.

Pour extraire les informations, utilisez les méthodes adaptées selon le type de données :

  • GetString(offset, length).Trim() - extrait une chaîne de caractères de longueur fixe en supprimant les espaces inutiles
  • GetInt(offset) - récupère une valeur numérique binaire (4 octets)
  • GetDTSTimestamp(offset) - méthode spécifique à NTi qui convertit un timestamp IBM i en DateTime .NET
var result = new List<ListObjectsInformation>();

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

        // Calcul de l'offset de l'entrée actuelle dans le User Space
        int currentOffset = offsetToData + (i * entrySize);

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

Une fois les données extraites et stockées sous forme d'objets C#, supprimez le User Space temporaire :

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

Si aucune entrée n’a été récupérée, retournez une liste vide :

return new List<ListObjectsInformation>();

Récapitulatif

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>();
}

Et maintenant ?