Appeler une procédure stockée SQL IBM i en C# (.NET) avec NTi
Introduction
Les procédures stockées permettent de centraliser et d'encapsuler la logique métier dans un environnement sécurisé et optimisé. Elles déplacent une partie de la logique applicative vers le serveur de base de données, réduisant la complexité du code côté client, améliorant les performances en diminuant le trafic réseau, et renforçant la sécurité en limitant l'accès direct aux tables.
Ce tutoriel montre comment appeler une procédure stockée SQL IBM i depuis une application C# (.NET) en utilisant NTi.
La procédure utilisée provient de la documentation officielle IBM i, la DB2 for i SQL Reference (page 1141). Elle calcule la médiane des salaires du personnel et retourne la liste des employés dont le salaire est supérieur à cette médiane.
Trois points sont couverts :
- Approche classique avec un DataReader
- Approche simplifiée avec Dapper, un micro-ORM léger
- Procédures à plusieurs result sets avec NextResultAsync
Étape 1 - Préparer l'environnement IBM i
Avant d'appeler la procédure depuis .NET, il faut comprendre ce qu'elle fait, quels éléments elle nécessite (tables, données), et préparer l'environnement IBM i.
Voici le code SQL de la procédure, issu du manuel DB2 for i SQL Reference (page 1141) :
CREATE PROCEDURE MEDIAN_RESULT_SET (OUT medianSalary DECIMAL(7,2))
LANGUAGE SQL
DYNAMIC RESULT SETS 1
BEGIN
DECLARE v_numRecords INTEGER DEFAULT 1;
DECLARE v_counter INTEGER DEFAULT 0;
DECLARE c1 CURSOR FOR
SELECT salary
FROM staff
ORDER BY salary;
DECLARE c2 CURSOR WITH RETURN FOR
SELECT name, job, salary
FROM staff
WHERE salary > medianSalary
ORDER BY salary;
DECLARE EXIT HANDLER FOR NOT FOUND
SET medianSalary = 6666;
SET medianSalary = 0;
SELECT COUNT(*) INTO v_numRecords FROM staff;
OPEN c1;
WHILE v_counter < (v_numRecords / 2 + 1) DO
FETCH c1 INTO medianSalary;
SET v_counter = v_counter + 1;
END WHILE;
CLOSE c1;
OPEN c2;
END
Cette procédure calcule le salaire médian des employés. Elle retourne ce salaire via un paramètre de sortie medianSalary, et ouvre un curseur c2 qui retourne la liste des employés dont le salaire dépasse cette médiane.
| Paramètre | Type | Direction | Description |
|---|---|---|---|
| medianSalary | Decimal(7,2) | OUT | Salaire médian calculé par la procédure |
💡 Il n'y a pas de paramètre en entrée. La procédure effectue son calcul directement à partir de la table
staff.
Pour que la procédure fonctionne, une table staff doit exister dans le même schéma, avec au moins les colonnes salary (DECIMAL), name et job.
Créer la bibliothèque
Créez une bibliothèque MDSALARY pour isoler les éléments de test. CRTLIB est une commande CL, pas une instruction SQL. Dans l'Exécuteur de scripts SQL d'ACS, une commande CL se préfixe par CL: et se termine par ; comme le reste du script :
CL: CRTLIB MDSALARY;Définir le schéma courant
Définissez le schéma courant pour que toutes les instructions SQL suivantes s'y réfèrent automatiquement :
SET CURRENT SCHEMA = MDSALARY;
Désormais, chaque table ou procédure créée sera automatiquement placée dans la bibliothèque MDSALARY.
Créer la table STAFF
Créez la table staff selon les colonnes requises :
CREATE TABLE staff (
name VARCHAR(50),
job VARCHAR(50),
salary DECIMAL(7,2)
);
name- nom de l'employéjob- fonctionsalary- salaire
Insérer un jeu de données
Insérez quelques données représentatives pour calculer un salaire médian pertinent :
INSERT INTO staff (name, job, salary) VALUES ('Alice', 'Manager', 2000.00);
INSERT INTO staff (name, job, salary) VALUES ('Bob', 'Clerk', 3000.00);
INSERT INTO staff (name, job, salary) VALUES ('Charlie', 'Analyst', 4000.00);
INSERT INTO staff (name, job, salary) VALUES ('David', 'Developer', 5000.00);
INSERT INTO staff (name, job, salary) VALUES ('Eve', 'Designer', 6000.00);
INSERT INTO staff (name, job, salary) VALUES ('Frank', 'Tester', 7000.00);Script complet prêt à exécuter dans ACS
Copiez-collez ce script dans l'outil Exécuteur de scripts SQL d'ACS et exécutez-le en une seule fois :
-- Création de la bibliothèque (commande CL : préfixe CL: et point-virgule final)
CL: CRTLIB MDSALARY;
-- Définition du schéma courant
SET CURRENT SCHEMA = MDSALARY;
-- Création de la table STAFF
CREATE TABLE staff (
name VARCHAR(50),
job VARCHAR(50),
salary DECIMAL(7,2)
);
-- Insertion des données de test
INSERT INTO staff (name, job, salary) VALUES ('Alice', 'Manager', 2000.00);
INSERT INTO staff (name, job, salary) VALUES ('Bob', 'Clerk', 3000.00);
INSERT INTO staff (name, job, salary) VALUES ('Charlie', 'Analyst', 4000.00);
INSERT INTO staff (name, job, salary) VALUES ('David', 'Developer', 5000.00);
INSERT INTO staff (name, job, salary) VALUES ('Eve', 'Designer', 6000.00);
INSERT INTO staff (name, job, salary) VALUES ('Frank', 'Tester', 7000.00);
-- Création de la procédure stockée MEDIAN_RESULT_SET
CREATE PROCEDURE MEDIAN_RESULT_SET (OUT medianSalary DECIMAL(7,2))
LANGUAGE SQL
DYNAMIC RESULT SETS 1
BEGIN
DECLARE v_numRecords INTEGER DEFAULT 1;
DECLARE v_counter INTEGER DEFAULT 0;
DECLARE c1 CURSOR FOR
SELECT salary FROM staff ORDER BY salary;
DECLARE c2 CURSOR WITH RETURN FOR
SELECT name, job, salary FROM staff WHERE salary > medianSalary ORDER BY salary;
DECLARE EXIT HANDLER FOR NOT FOUND SET medianSalary = 6666;
SET medianSalary = 0;
SELECT COUNT(*) INTO v_numRecords FROM staff;
OPEN c1;
WHILE v_counter < (v_numRecords / 2 + 1) DO
FETCH c1 INTO medianSalary;
SET v_counter = v_counter + 1;
END WHILE;
CLOSE c1;
OPEN c2;
END;Vérifier sur l'IBM i
Vérifiez que la bibliothèque MDSALARY existe, qu'elle contient la table staff avec les données insérées, et la procédure MEDIAN_RESULT_SET (type *PGM).

Étape 2 - Appeler la procédure stockée depuis .NET
Créez un projet Blazor Web App en .NET 8 et installez les packages suivants :
dotnet add package Aumerial.Data.Nti
dotnet add package DapperCréer un service de connexion
Créez un service DB2Service.cs pour centraliser la gestion des connexions. Avec NTi, l'asynchrone est la voie normale. L'async est réel de bout en bout (OpenAsync jusqu'à DisposeAsync), sans blocage de thread. La voie synchrone (Open()) reste disponible pour les contextes qui l'exigent.
using System.Threading;
using System.Threading.Tasks;
using Aumerial.Data.Nti;
public class DB2Service
{
private readonly string _connectionString =
"server=MY_SYSTEM;user=MY_USER;password=MY_PASSWORD;pooling=true";
public async Task CreateConnectionAsync(CancellationToken cancellationToken = default)
{
var conn = new NTiConnection(_connectionString);
await conn.OpenAsync(cancellationToken);
return conn;
}
}
💡 Quelques défauts à connaître. Le pool de connexions est inactif par défaut (compatibilité v4), d'où le
pooling=true, fortement recommandé pour une application web. Tous les timeouts sont par ailleurs illimités par défaut (0 = infini), etpersist security info=falseexpurge le mot de passe deConnectionStringdès la connexion ouverte.
Enregistrez ensuite ce service dans Program.cs :
builder.Services.AddSingleton(); Créer l'entité Employee
public class Employee
{
public string Name { get; set; } = "";
public string Job { get; set; } = "";
public decimal Salary { get; set; }
}Injecter le service dans le composant Blazor
Point souvent oublié, le composant doit recevoir le service par la directive @inject, sans quoi Db2Service n'existe pas dans le code du composant. Les noms restent cohérents de bout en bout, avec la classe qui s'appelle DB2Service et l'instance injectée Db2Service.
Créez un composant StoredProcedure.razor :
@page "/stored-procedure"
@rendermode InteractiveServer
@using System.Data
@using Aumerial.Data.Nti
@using Dapper
@inject DB2Service Db2Service
<h3>Salaires au-dessus de la médiane</h3>
<button class="btn btn-primary" @onclick="LoadDataWithDataReader">DataReader</button>
<button class="btn btn-secondary" @onclick="LoadDataWithDapper">Dapper</button>
<p>Salaire médian : @median</p>
<table class="table">
<thead>
<tr><th>Nom</th><th>Poste</th><th>Salaire</th></tr>
</thead>
<tbody>
@foreach (var employee in employees)
{
<tr><td>@employee.Name</td><td>@employee.Job</td><td>@employee.Salary</td></tr>
}
</tbody>
</table>
Les champs et méthodes des deux approches ci-dessous se placent dans le bloc @code du composant :
private decimal median;
private List employees = new(); Méthode 1 - Approche classique (DataReader)
Créez une connexion via Db2Service, configurez une commande NTi pour appeler MEDIAN_RESULT_SET, définissez le paramètre de sortie medianSalary, puis lisez les résultats via un DataReader :
private async Task LoadDataWithDataReader()
{
employees.Clear();
await using var conn = await Db2Service.CreateConnectionAsync();
await using var cmd = new NTiCommand("MDSALARY.MEDIAN_RESULT_SET", conn);
cmd.CommandType = CommandType.StoredProcedure;
var param = new NTiParameter
{
ParameterName = "medianSalary",
Direction = ParameterDirection.Output
};
cmd.Parameters.Add(param);
await using var reader = await cmd.ExecuteReaderAsync();
median = Convert.ToDecimal(param.Value);
while (await reader.ReadAsync())
{
employees.Add(new Employee
{
Name = reader.GetString(0),
Job = reader.GetString(1),
Salary = reader.GetDecimal(2)
});
}
}Méthode 2 - Approche simplifiée (Dapper)
Avec Dapper, définissez le paramètre de sortie via DynamicParameters. Dapper gère automatiquement l'exécution et mappe les résultats directement en liste d'objets Employee :
private async Task LoadDataWithDapper()
{
await using var conn = await Db2Service.CreateConnectionAsync();
var parameters = new DynamicParameters();
parameters.Add("medianSalary", dbType: DbType.Decimal, direction: ParameterDirection.Output);
employees = (await conn.QueryAsync(
"MDSALARY.MEDIAN_RESULT_SET",
parameters,
commandType: CommandType.StoredProcedure)).ToList();
median = parameters.Get("medianSalary");
} Afficher les résultats dans un composant Blazor

Étape 3 - Plusieurs result sets (DYNAMIC RESULT SETS 2)
Une procédure DB2 for i peut retourner plusieurs result sets. Il suffit de déclarer DYNAMIC RESULT SETS 2 (ou plus), et d'ouvrir plusieurs curseurs WITH RETURN. Côté .NET, on passe d'un result set au suivant avec la méthode ADO.NET standard NextResultAsync (NextResult en synchrone).
Créez une variante de la procédure qui retourne deux listes, les salaires au-dessus de la médiane, puis ceux en dessous ou égaux :
CREATE PROCEDURE MDSALARY.MEDIAN_MULTI (OUT medianSalary DECIMAL(7,2))
LANGUAGE SQL
DYNAMIC RESULT SETS 2
BEGIN
DECLARE v_numRecords INTEGER DEFAULT 1;
DECLARE v_counter INTEGER DEFAULT 0;
DECLARE c1 CURSOR FOR
SELECT salary FROM staff ORDER BY salary;
-- Result set 1 : salaires au-dessus de la médiane
DECLARE c2 CURSOR WITH RETURN FOR
SELECT name, job, salary FROM staff WHERE salary > medianSalary ORDER BY salary;
-- Result set 2 : salaires en dessous ou égaux à la médiane
DECLARE c3 CURSOR WITH RETURN FOR
SELECT name, job, salary FROM staff WHERE salary <= medianSalary ORDER BY salary;
DECLARE EXIT HANDLER FOR NOT FOUND SET medianSalary = 6666;
SET medianSalary = 0;
SELECT COUNT(*) INTO v_numRecords FROM staff;
OPEN c1;
WHILE v_counter < (v_numRecords / 2 + 1) DO
FETCH c1 INTO medianSalary;
SET v_counter = v_counter + 1;
END WHILE;
CLOSE c1;
OPEN c2;
OPEN c3;
END;
Les result sets arrivent dans l'ordre d'ouverture des curseurs (c2 puis c3). Côté composant, dans le bloc @code :
private List above = new();
private List belowOrEqual = new();
private async Task LoadMultipleResultSets()
{
above.Clear();
belowOrEqual.Clear();
await using var conn = await Db2Service.CreateConnectionAsync();
await using var cmd = new NTiCommand("MDSALARY.MEDIAN_MULTI", conn);
cmd.CommandType = CommandType.StoredProcedure;
var param = new NTiParameter
{
ParameterName = "medianSalary",
Direction = ParameterDirection.Output
};
cmd.Parameters.Add(param);
await using var reader = await cmd.ExecuteReaderAsync();
median = Convert.ToDecimal(param.Value);
// Premier result set : curseur c2
while (await reader.ReadAsync())
{
above.Add(new Employee
{
Name = reader.GetString(0),
Job = reader.GetString(1),
Salary = reader.GetDecimal(2)
});
}
// Passage au result set suivant : curseur c3
if (await reader.NextResultAsync())
{
while (await reader.ReadAsync())
{
belowOrEqual.Add(new Employee
{
Name = reader.GetString(0),
Job = reader.GetString(1),
Salary = reader.GetDecimal(2)
});
}
}
}
💡 Si le nombre de result sets n'est pas connu à l'avance, bouclez avec
do { ... } while (await reader.NextResultAsync());. Avec Dapper, l'équivalent estQueryMultipleAsync.
Et maintenant ?
- Appeler un programme : appel de programme RPG avec paramètres d'entrée/sortie
- Exécuter une commande CL : exécuter une commande CL et gérer les erreurs
- Connexion : chaîne de connexion, pool, MFA
- NTiConnection : référence complète de la classe de connexion