Connexion

NTiConnection implémente le contrat standard DbConnection. L'ouverture, CreateCommand, les transactions, GetSchema et les événements s'utilisent donc comme avec n'importe quel autre provider ADO.NET. Cette page ne redocumente pas ce contrat commun, elle décrit ce qui est propre à NTi et à IBM i.

Cycle de vie : configuration figée à l'ouverture

Toute la configuration, la chaîne de connexion, les propriétés, les fournisseurs MFA, doit être posée avant d'appeler Open ou OpenAsync. Une fois la connexion ouverte, elle ne bouge plus. La session physique qui a été créée à ce moment-là, et qui sera peut-être rendue au pool ensuite pour être réutilisée, continue de fonctionner avec cette configuration initiale, même si vous modifiez encore les propriétés de l'objet NTiConnection après coup : ces changements n'ont alors plus aucun effet. Pour appliquer un nouveau réglage, il faut fermer la connexion et la rouvrir avec la configuration voulue, ou en créer une nouvelle.

Le contrat ADO.NET prévoit toutefois une exception à cette règle. Le schéma courant peut être changé à chaud, avec ChangeDatabase ou ChangeDatabaseAsync, l'équivalent d'un SET CURRENT SCHEMA.

using System;
using Aumerial.Data.Nti;

await using var connection = new NTiConnection(
    "server=MYIBMI;user=MYUSER;password=MYPASSWORD;default schema=MYLIB");
await connection.OpenAsync();
Console.WriteLine(connection.Database);           // MYLIB
await connection.ChangeDatabaseAsync("OTHERLIB"); // SET CURRENT SCHEMA à chaud

La variante synchrone ChangeDatabase existe sur toutes les cibles. ChangeDatabaseAsync, elle, est disponible partout sauf sur .NET Framework, dont la classe de base ne l'expose pas.

Open et OpenAsync : annulation réelle

OpenAsync est asynchrone de bout en bout, et pas seulement en apparence. L'attente d'une session dans le pool, la connexion TCP ou TLS, le signon et l'éventuel facteur MFA honorent tous le jeton d'annulation de l'appelant, sans jamais basculer en sync-over-async en coulisses. Open reste disponible pour le code synchrone, et suit exactement le même chemin protocolaire, juste sans le côté asynchrone.

using System;
using System.Threading;
using Aumerial.Data.Nti;

using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
await using var connection = new NTiConnection(
    "server=MYIBMI;user=MYUSER;password=MYPASSWORD;pooling=true");
await connection.OpenAsync(cts.Token);

Un détail compte particulièrement ici. Annuler le jeton pendant qu'une opération est en cours casse la connexion, et c'est voulu. La trame réseau qui était en train de transiter est perdue, et NTi refuse de tenter une resynchronisation silencieuse de la session, ce qui pourrait la laisser dans un état incohérent. L'appel lève donc une OperationCanceledException qui porte le jeton de l'appelant, et il faut rouvrir la connexion pour continuer à travailler. Avec le pool de connexions actif, cette réouverture ne coûte presque rien, puisqu'une autre session déjà disponible dans le pool prend simplement le relais.

Pooling : inactif par défaut

Le pool de connexions est inactif par défaut. Vous l'activez avec pooling=true, ce qui est fortement recommandé pour les charges web. Une fois actif, Open et OpenAsync récupèrent une session déjà validée dans le pool au lieu de refaire tout le travail de connexion TCP, TLS et signon. Close et DisposeAsync, de leur côté, rendent la session au pool plutôt que de la fermer réellement.

Chaque configuration distincte a son propre pool. L'identité client fait partie de la clé de ce pool, avec application name, client accounting, client user identifier et client program identifier. Deux identités distinctes ne partagent donc jamais le même pool.

using Aumerial.Data.Nti;

var connectionString =
    "server=MYIBMI;user=MYUSER;password=MYPASSWORD;pooling=true;max pool size=20";

await using (var connection = new NTiConnection(connectionString))
{
    await connection.OpenAsync();  // prend une session du pool, ou en ouvre une
}                                  // DisposeAsync rend la session au pool

// Purge programmatique
await using var probe = new NTiConnection(connectionString);
NTiConnection.ClearPool(probe);    // vide le pool correspondant à cette configuration
NTiConnection.ClearAllPools();     // vide tous les pools du processus

Timeouts : illimités par défaut

Tous les délais sont illimités par défaut, 0 valant infini. Ni l'établissement de la connexion ni l'exécution d'une commande ne sont donc bornés tant que vous ne le demandez pas explicitement. Trois leviers permettent de poser une limite :

  • connect timeout, en secondes, borne l'établissement de la connexion.
  • CommandTimeout, en secondes lui aussi, se règle commande par commande. C'est la propriété standard du contrat DbCommand, et elle reste illimitée par défaut (0).
  • Le jeton d'annulation reste, sur la voie async, la borne universelle.
using System;
using System.Threading;
using Aumerial.Data.Nti;

await using var connection = new NTiConnection(
    "server=MYIBMI;user=MYUSER;password=MYPASSWORD;connect timeout=15");
using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(2));
await connection.OpenAsync(cts.Token);

await using var command = connection.CreateCommand();
command.CommandText = "SELECT COUNT(*) FROM MYLIB.ORDERS";
command.CommandTimeout = 60;   // par commande, en secondes ; 0 = illimité (défaut)
Console.WriteLine(await command.ExecuteScalarAsync(cts.Token));

TLS

ssl=true chiffre les connexions aux trois serveurs hôtes, et les ports par défaut basculent d'eux-mêmes sur leurs variantes TLS (9476, 9471, 9475). Le certificat serveur est validé contre le magasin de confiance de la machine. untrusted=true accepte n'importe quel certificat, quelle que soit sa provenance. Réservez cette option aux environnements de test, jamais à la production.

using Aumerial.Data.Nti;

await using var connection = new NTiConnection(
    "server=MYIBMI;user=MYUSER;password=MYPASSWORD;ssl=true");
await connection.OpenAsync();

MFA : facteur d'authentification supplémentaire

Trois membres permettent de fournir le facteur additionnel, selon sa provenance :

  • AdditionalFactor fournit un facteur statique, un code TOTP que vous avez déjà en main. Il est aussi accessible par les mots-clés additional factor, mfa ou 2fa de la chaîne de connexion.
  • AdditionalFactorProvider est un rappel synchrone qui reçoit un contexte (l'hôte, l'utilisateur). Il est invoqué une fois par session physique, uniquement si le serveur annonce le support MFA, et peut être appelé en concurrence quand le pool grandit. S'il est défini en même temps que la variante asynchrone, c'est lui qui prime.
  • AdditionalFactorAsyncProvider est la voie normale pour aller chercher le facteur dans un coffre de secrets ou via un prompt. Sur OpenAsync, il est attendu nativement avec le jeton de l'appelant, ce qui le rend annulable. Sur Open en synchrone, en revanche, il démarre hors SynchronizationContext avec une attente non bornée, donc c'est à vous de borner vous-même les prompts interactifs.
using System.Threading.Tasks;
using Aumerial.Data.Nti;

await using var connection = new NTiConnection(
    "server=MYIBMI;user=MYUSER;password=MYPASSWORD;pooling=true");

// Voie normale : fournisseur asynchrone, attendu nativement
// avec le jeton de l'appelant sur OpenAsync.
connection.AdditionalFactorAsyncProvider = async (context, cancellationToken) =>
{
    await Task.Yield();  // ici : appel d'un coffre de secrets avec cancellationToken
    return "123456";
};
await connection.OpenAsync();

L'ancien rappel AdditionalFactorCallback, un Func<string> sans contexte, compile toujours mais est marqué [Obsolete]. Mieux vaut migrer vers AdditionalFactorProvider, qui reçoit le contexte d'authentification. Entre les deux, c'est la dernière assignation qui l'emporte.

Deux jobs IBM i par connexion

Une NTiConnection ouverte occupe deux jobs serveur distincts sur l'IBM i :

  • Le SQL s'exécute dans le job du serveur database, QZDASOINIT.
  • Les commandes CL et les appels de programme s'exécutent dans le job du serveur commande, QZRCSRVS.

Chaque job a sa propre QTEMP, sa propre CURLIB et ses propres overrides. Un objet créé dans QTEMP par une commande CL est ainsi invisible du SQL, et réciproquement. Pour exécuter du CL dans le job SQL, et donc dans sa propre QTEMP, passez par le pont CALL QSYS2.QCMDEXC('...'), au prix d'un aller-retour SQL. Les propriétés DatabaseJob et CommandJob donnent le nom qualifié de chaque job pour inspection, via WRKJOB, les journaux ou un audit.

using System;
using Aumerial.Data.Nti;

await using var connection = new NTiConnection(
    "server=MYIBMI;user=MYUSER;password=MYPASSWORD");
await connection.OpenAsync();

// CL : job commande (QZRCSRVS), et donc SA QTEMP
await connection.ExecuteClCommandAsync(
    "CRTDUPOBJ OBJ(ORDERS) FROMLIB(MYLIB) OBJTYPE(*FILE) TOLIB(QTEMP)");

// SQL : job database (QZDASOINIT), avec une AUTRE QTEMP :
// SELECT * FROM QTEMP.ORDERS ne verrait pas l'objet créé ci-dessus.

// Pont : exécuter le CL dans le job SQL (surcoût d'un aller-retour SQL)
await using (var command = connection.CreateCommand())
{
    command.CommandText =
        "CALL QSYS2.QCMDEXC('CRTDUPOBJ OBJ(ORDERS) FROMLIB(MYLIB) OBJTYPE(*FILE) TOLIB(QTEMP)')";
    await command.ExecuteNonQueryAsync();
}

Console.WriteLine(connection.DatabaseJob);  // ex. 123456/QUSER/QZDASOINIT
Console.WriteLine(connection.CommandJob);   // ex. 123457/QUSER/QZRCSRVS

Persist security info

Par défaut, avec persist security info=false, le mot de passe est expurgé de la propriété ConnectionString dès que la connexion est ouverte. Un log ou un debugger qui lit la chaîne ne le verra donc pas. persist security info=true le conserve, réservé aux seuls scénarios de diagnostic qui l'exigent vraiment.

using System;
using Aumerial.Data.Nti;

await using var connection = new NTiConnection(
    "server=MYIBMI;user=MYUSER;password=MYPASSWORD");
await connection.OpenAsync();
// Le mot de passe ne figure plus dans la chaîne exposée
Console.WriteLine(connection.ConnectionString);

La liste exhaustive des mots-clés de chaîne de connexion, avec leurs défauts, fait l'objet de l'article suivant.


Et maintenant ?

Reconnexion au serveur...

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