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(ouCallProgramen 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 ?
- Appeler un programme de service : appeler une procédure exportée avec CallServiceProgram
- Exécuter une commande CL : exécuter une commande CL et gérer les erreurs
- Appeler une API système : appel d'API IBM i via un User Space
- NTiProgramParameter : référence complète de la classe de paramètres