DOCUMENTATION NTi

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 de ces API renvoient directement des données exploitables sous forme de structures ou de flux, 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.

L’objectif de cet exemple est de vous expliquer comment réussir à implémenter correctement 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 à la fin de cet exemple.
Vous pouvez suivre chaque étape pour comprendre le fonctionnement, puis vous y référer pour une vue d’ensemble et une utilisation directe.

Etape 1 - Choix d'une API: QSYS.QUSLOBJ

Pour utiliser l’API QUSLOBJ, la première chose à faire est d'aller consulter sa documentation.

Les principales étapes à suivre sont les suivantes.

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

Etape 2 - Lecture de la documentation IBM

Avant d’implémenter l’appel à QUSLOBJ, il est important de comprendre 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.

Nous n’entrerons pas ici dans le détail de l’implémentation, mais retenez que toutes les données retournées sont stockées dans ce User Space et qu’elles devront être analysées en respectant la structure définie par IBM.

Etape 3 - Implémentation de la méthode d'appel à QUSLOBJ avec NTi

Nous savons maintenant comment fonctionne QUSLOBJ et comment interpréter les résultats stockés dans le User Space. Passons maintenant à l’implémentation de notre méthode en C# avec NTi.

1️⃣ Définition du modèle de données

Avant toute chose, nous devons définir un modèle de données qui représentera chaque objet retourné par l’API. Nous utilisons le format OBJL0400 pour récupérer des informations détaillées sur les objets. On crée donc une classe C# qui reflètera cette 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️⃣ Définition de la méthode principale

On définit la méthode que nous allons construire pour récupérer une liste d'objets depuis une bibliothèque donnée. L'API attend en paramètre libraryName, objectName, et objectType.

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

3️⃣ Création du UserSpace

Avant d'appeler QUSLOBJ, on doit créer un User Space dans QTEMP comme vu précédemment qui nous permettra de récupérer la liste d'objet générée. L’API QUSCRTUS est utilisée pour créer ce User Space. Il faut lui fournir plusieurs paramètres, dont :

  • Le nom du User Space, ici NTILOBJ dans QTEMP.
  • Une taille initiale qui sera ajustée dynamiquement en fonction des données renvoyées.
  • Une valeur d’initialisation, ici 0x00, qui signifie un espace vierge.
  • Un attribut générique et une description.

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
            new NTiProgramParameter("*ALL", 10), // Attributs
            new NTiProgramParameter("List Object Information Userspace", 50) // Description
        };

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

4️⃣ Appel de l’API QUSLOBJ Création du UserSpace

Le UserSpace étant créé, on peut appeler l'API QUSLOBJ pour récupérer la liste des des objets. L’API attend plusieurs paramètres en entrée, qui doivent être correctement formatés pour éviter des erreurs.

L’API QUSLOBJ 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.

Une fois ces paramètres définis on appelle notre programme et on exécute l’API.

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

5️⃣ Récupération des résultats

Une fois QUSLOBJ exécuté, les résultats sont stockés dans le User Space. Pour les récupérer, on utilise l’API QUSRTVUS qui va permettre de lire son contenu.

  • Le premier paramètre désigne toujours 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.
  • Enfin, 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);

6️⃣ Analyse des données retournées

Nous devons maintenant extraire et analyser les données contenues dans la User Space. Les résultats sont renvoyés dans le dernier paramètre défini comme sortie lors de l’appel à QUSRTVUS. On récupère les informations générales : l’offset des données, le nombre d’entrées et la taille de chaque entrée. Ces valeurs nous permettent de comprendre où et comment parcourir la liste des objets retournés. La strucure générale des données pour les API de liste est disponible ici.

  • L’offset du début des données offsetToData indique où commence réellement la liste des objets.
  • Le nombre d’entrées retournées numberOfEntries indique combien d’objets sont listés.
  • La taille d’une entrée entrySize qui nous permet de savoir combien d’octets sont alloués à chaque objet.
  • La taille totale des données listSize qui correspond à l’espace occupé 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], qui correspond au 5ᵉ paramètre de notre appel. Nous devons donc l’extraire et le nettoyer 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

Ensuite, on s'assure que l'espace alloué pour le UserSpace est suffisant. Si la taille totale des données listSize + offsetToData dépasse la taille initialement allouée, il faut supprimer le User Space, augmenter sa taille et relancer l’appel à QUSLOBJ.

var totalSize = listSize + offsetToData;

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

Si l’espace est suffisant, on parcourt 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, on calcule 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 que l’on veut analyser.

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

  • GetString(offset, length).Trim() pour les champs texte, qui extrait une chaîne de caractères de longueur fixe en supprimant les espaces inutiles.
  • GetInt(offset) pour les valeurs numériques binaires stockées en 4 ou 8 octets, sans nécessiter de longueur.
  • GetDTSTimestamp(offset) est une méthode spécifique à NTi qui permet de convertir un timestamp IBM i en une date exploitable coté .NET.

Une fois les valeurs extraites, elles sont stockées dans un objet ListObjectsInformation, puis ajoutées à la liste de résultats.

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 toutes les données extraites et stockées sous forme d'objets C#, il est important de nettoyer la mémoire en supprimant le User Space temporaire. Bien que le User Space soit stocké dans QTEMP (et donc supprimé à la fin de la session), il est préférable de le supprimer immédiatement après utilisation pour éviter toute surcharge inutile si plusieurs appels sont effectués en continu.

Nous utilisons la commande CL DLTUSRSPC pour supprimer le User Space spécifié. Une fois cette suppression effectuée, la méthode retourne la liste des objets extraits. Si aucun objet n’a été trouvé par l’API, une liste vide est renvoyée afin d’assurer une gestion propre des résultats et d'éviter d’éventuelles erreurs dans les traitements en aval.

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

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

return new List<ListObjectsInformation>();

Conclusion

Avec cette dernière étape, notre méthode est entièrement fonctionnelle. Elle suit un processus structuré qui permet :

  • La création dynamique d’un User Space.
  • L’appel à QUSLOBJ pour récupérer la liste des objets.
  • L’extraction des données en respectant la structure définie par IBM.
  • La gestion dynamique de la mémoire en ajustant la taille du User Space si nécessaire.
  • La transformation des résultats en objets C# exploitables.
  • La suppression systématique du User Space après utilisation.
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>();
}