Appeler un programme IBM i (AS/400) en C# (.NET) avec NTi

Introduction

Ce tutoriel montre comment appeler un programme IBM i (AS/400) depuis une application C# (.NET) en utilisant NTi Data Provider.

Appeler un programme IBM i depuis une application .NET permet de réutiliser la logique métier existante (RPG, COBOL, CL) sans avoir à la réécrire. Les programmes legacy s'intègrent ainsi tels quels dans des applications modernes.

Avec NTi, vous pouvez depuis du code C# :

  • définir les paramètres d'entrée, de sortie et d'entrée/sortie avec NTiProgramParameter
  • appeler un programme IBM i avec CallProgramAsync (ou CallProgram en synchrone)
  • récupérer les données retournées directement depuis les buffers de sortie

💡 Les API système IBM i sont des programmes ordinaires du point de vue de l'appelant. Tout ce qui suit s'applique donc aussi à QWCRSVAL, QUSLOBJ et consorts, voir Appeler une API système.

NTi fonctionne à partir de la V5R4 de l'IBM i (V7R4 ou plus récent recommandé). L'appel de programme existe en synchrone comme en asynchrone réel de bout en bout.


Description du programme et des paramètres

Le programme MYPGM de la bibliothèque MYLIB attend les paramètres suivants :

Description Type Direction Valeur
Texte 1 CHAR(10) Input Hello
Texte 2 CHAR(10) Input World
Position de départ BYTE(1) Input 0x00
Longueur de la variable retour BYTE(4) Input 128
Variable retour CHAR(*) Output vide
Code d'erreur CHAR(50) InputOutput vide

On souhaite lire différentes valeurs dans la variable retour composée ainsi :

Offset Longueur Description
0 64 Message 1
64 64 Message 2

Étape 1 - Ouvrir la connexion

Déclarez une instance de NTiConnection et ouvrez la connexion. Avec NTi, l'asynchrone est la voie normale, chaque opération possédant une variante async réelle, annulable par CancellationToken.

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

💡 Le pooling de connexions est inactif par défaut. Ajoutez pooling=true à la chaîne de connexion pour les applications web. Le synchrone reste disponible (conn.Open()).


Étape 2 - Créer les paramètres

En vous référant à la description des paramètres ci-dessus, créez la liste des paramètres avec leurs valeurs :

var parms = new List
{
    new NTiProgramParameter("Hello", 10).AsInput(),          // CHAR(10) INPUT
    new NTiProgramParameter("World", 10).AsInput(),          // CHAR(10) INPUT
    new NTiProgramParameter(new byte[] { 0x00 }).AsInput(),  // BYTE(1)  INPUT
    new NTiProgramParameter(128).AsInput(),                  // BYTE(4)  INPUT
    new NTiProgramParameter("", 128).AsOutput(),             // CHAR(128) OUTPUT
    new NTiProgramParameter("", 50)                          // CHAR(50)  INPUT/OUTPUT
};

La direction par défaut est InputOutput (voir ParameterDirection). Les helpers .AsInput(), .AsOutput() et .AsInputOutput() la fixent de façon fluide.


Étape 3 - Appeler le programme

Appelez le programme grâce à la méthode CallProgramAsync() de NTiConnection :

await conn.CallProgramAsync("MYLIB", "MYPGM", parms);

En synchrone : conn.CallProgram("MYLIB", "MYPGM", parms);. Un jeton d'annulation peut être passé en dernier argument. Par contrat, une annulation en cours d'appel casse la connexion (la trame en vol est perdue) et se traduit par une OperationCanceledException portant votre jeton. Il faut alors rouvrir la connexion.


Étape 4 - Récupérer les données retournées

Une fois le programme appelé, récupérez les données retournées dans la variable retour (paramètre n°5) :

string message1 = parms[4].GetString(0, 64);
string message2 = parms[4].GetString(64, 64);

GetString décode le buffer de sortie avec le CCSID du job. Passez un CCSID explicite (GetString(offset, longueur, ccsid)) quand les caractères !, [, ] ou ^ importent, car ils varient d'une page de code EBCDIC à l'autre.


Récepteur de taille variable (CHAR(*), VARCHAR)

Quand la longueur écrite par le programme n'est pas connue à l'avance (paramètre déclaré CHAR(*) côté RPG, retour VARCHAR, structure de taille variable), passez un paramètre VIDE, new NTiProgramParameter(). NTi le déclare au serveur en longueur variable (0xFFFF), et le paramètre revient dimensionné sur la donnée réellement écrite par le programme, GetBytes().Length donnant la taille réelle. Ce comportement est disponible depuis NTi 4.4.14 et natif en 5.x.

using System;
using System.Collections.Generic;
using Aumerial.Data.Nti;
await using var conn = new NTiConnection("server=serverName;user=userName;password=password");
await conn.OpenAsync();
var receiver = new NTiProgramParameter().AsInputOutput();   // récepteur vide : taille variable
var parms = new List { receiver };
await conn.CallProgramAsync("MYLIB", "MYPGM2", parms);
// VARCHAR : préfixe de longueur BINARY(2), puis les données
string value = receiver.GetString(2, receiver.GetShort());
Console.WriteLine(value);

Pour un retour VARCHAR, le buffer commence par un préfixe de longueur sur 2 octets. GetShort() le lit, et GetString(2, longueur) décode le texte qui suit. Un champ CHAR situé dans une structure se lit avec GetString(offset, longueur).


Constructeurs et accesseurs de NTiProgramParameter

Tous les constructeurs acceptent une ParameterDirection finale optionnelle (défaut : InputOutput) :

Constructeur Type IBM i produit
NTiProgramParameter(string valeur, int longueur) et (string, int, int ccsid) CHAR(longueur), complété par des blancs
NTiProgramParameter(string[] valeurs, int longueur) et (string[], int, int ccsid) tableau de CHAR(longueur) (surcharges IEnumerable<string> aussi)
NTiProgramParameter(int valeur) / (int[] valeurs) BINARY(4) / tableau
NTiProgramParameter(short valeur) / (short[] valeurs) BINARY(2) / tableau
NTiProgramParameter(decimal valeur, int précision, int échelle, bool packed = true) décimal packé ou zoné
NTiProgramParameter(byte[] valeur) octets bruts, envoyés tels quels
NTiProgramParameter() récepteur VIDE à taille variable (voir ci-dessus)

Les surcharges fluides Append(...) prolongent un paramètre avec des champs supplémentaires pour composer une structure de données dans un seul buffer, chacune retournant le paramètre pour se chaîner : Append(string, int[, int ccsid]), Append(int), Append(short), Append(decimal, int précision, int échelle[, bool packed]), Append(byte[]), plus les variantes tableau. Les extensions parms.Add(...) reflètent chaque constructeur, parms.Add("ABC", 10).AsInput() créant et ajoutant en un seul appel.

Après l'appel, la lecture se fait sur le buffer de sortie (OutputData) :

Accesseur Lit
GetString() / GetString(int ccsid) / GetString(int offset, int longueur) / GetString(int offset, int longueur, int ccsid) texte
GetInt() / GetInt(int offset) BINARY(4), big-endian
GetShort() / GetShort(int offset) BINARY(2), tel le préfixe de longueur d'un VARCHAR
GetPackedDecimal(int précision, int échelle[, int offset]) décimal packé
GetZonedDecimal(int précision, int échelle[, int offset]) décimal zoné
GetBytes() / GetBytes(int offset, int longueur) octets bruts
GetDTSTimestamp([int offset]) horodatage *DTS sur 8 octets

InputData et OutputData sont des byte[] publics, pour un contrôle manuel complet.


Données binaires et contrat hexadécimal

NTi ne convertit jamais une donnée binaire en texte silencieusement. Sur un paramètre de programme, lisez les segments binaires avec GetBytes(offset, longueur), GetString étant réservé aux segments texte, décodés avec le CCSID du job ou un CCSID explicite. Côté SQL, le contrat est verrouillé, GetString() du data reader sur une colonne binaire (BINARY, VARBINARY, ROWID ou CHAR FOR BIT DATA, CCSID 65535) rendant le contenu en hexadécimal MAJUSCULE, sans séparateur (parité v4). Le mot-clé de chaîne de connexion force translate décode BINARY/VARBINARY/ROWID comme du texte, mais jamais une colonne FOR BIT DATA.


Gestion des erreurs

Un appel qui échoue (programme introuvable, message d'échappement MCH ou CPF) lève une NTiCommandException qui porte la pile de messages du serveur dans Messages (Id, Type, Severity, File, Library, Text, SubstitutionData, Help) :

try
{
    await conn.CallProgramAsync("MYLIB", "MYPGM", parms);
}
catch (NTiCommandException ex)
{
    foreach (var message in ex.Messages)
        Console.WriteLine($"{message.Id} (sévérité {message.Severity}) : {message.Text}");
}

Deux jobs IBM i par connexion

Une connexion NTi ouvre en réalité deux jobs sur l'IBM i :

  • Le job SQL (QZDASOINIT) exécute tout ce qui passe par le SQL.
  • Le job commande (QZRCSRVS) exécute les commandes CL et les appels de programme.

Chaque job possède sa propre QTEMP et sa propre CURLIB. Un objet créé en QTEMP par un programme n'est pas visible du SQL, et inversement. Pour exécuter du CL dans le job SQL (même QTEMP, même liste de bibliothèques), passez par CALL QSYS2.QCMDEXC('...'), au prix du surcoût SQL. Les propriétés conn.DatabaseJob et conn.CommandJob identifient les deux jobs.


Récapitulatif

Code complet pour appeler un programme IBM i depuis .NET avec NTi :

using System;
using System.Collections.Generic;
using Aumerial.Data.Nti;
await using var conn = new NTiConnection("server=serverName;user=userName;password=password");
await conn.OpenAsync();
var parms = new List
{
    new NTiProgramParameter("Hello", 10).AsInput(),          // CHAR(10) INPUT
    new NTiProgramParameter("World", 10).AsInput(),          // CHAR(10) INPUT
    new NTiProgramParameter(new byte[] { 0x00 }).AsInput(),  // BYTE(1)  INPUT
    new NTiProgramParameter(128).AsInput(),                  // BYTE(4)  INPUT
    new NTiProgramParameter("", 128).AsOutput(),             // CHAR(128) OUTPUT
    new NTiProgramParameter("", 50)                          // CHAR(50)  INPUT/OUTPUT
};
try
{
    await conn.CallProgramAsync("MYLIB", "MYPGM", parms);
    string message1 = parms[4].GetString(0, 64);
    string message2 = parms[4].GetString(64, 64);
    Console.WriteLine($"{message1} {message2}");
}
catch (NTiCommandException ex)
{
    foreach (var message in ex.Messages)
        Console.WriteLine($"{message.Id} (sévérité {message.Severity}) : {message.Text}");
}

Et maintenant ?

Reconnexion au serveur...

La connexion au serveur a été perdue. La page va se recharger.