Introduction
La force d’une architecture microservices réside dans sa simplicité: des composants légers et autonomes, exposés via des API REST, et facilement exploitables par n’importe quel service ou application.
C’est précisément cet esprit de simplicité que propose NTi Data Provider aux développeurs .NET. Quelques lignes de C# suffisent pour créer des API REST minimales, capable d’exposer directement vos ressources IBM i (tables DB2, commandes CL ou programme RPG) à l’ensemble de vos applicatifs, qu’il s’agisse d’interface web Angular, React, d’applications mobiles ou de services tiers.
Ce tutoriel vous montre concrètement comment implémenter très rapidement et simplement une API minimale en .NET, sans couches complexes. L’objectif est de fournir une ressource technique utilisable et reproductible en quelques minutes, afin de mesurer concrètement le potentiel et la simplicité d'utilisation de NTi dans une approche microservices moderne.
Le Code complet de cet exemple est disponible en bas de cette page
1️⃣ Initialiser une API minimale en .NET 8
Pour démarrer rapidement votre API REST minimale, il suffit d'ouvrir Visual Studio, et de suivre ces quelques étapes simple:
- Cliquer sur Nouveau projet > Application web ASP.NET CORE.
- Séléctionner le framework .NET 8 > décocher Utiliser des Controller pour profiter du modèle minimaliste.
À cette étape, Visual Studio génère automatiquement un fichier Program.cs
prêt à l'emploi avec Swagger configuré par défaut. Vous êtes ainsi directement prêt à implémenter vos endpoints API. Il ne reste maintenant plus qu'à ajouter NTi Data Provider, ainsi que Dapper.
Ouvrez votre console et installez les packages suivants :
dotnet add package Aumerial.Data.Nti
dotnet add package Dapper
Puis, référencez les dans votre projet:
using Aumerial.Data.Nti;
using Dapper;
Il faut ensuite définir la chaîne de connexion dans le fichier program.cs
, pour pouvoir se connecter à DB2 for i et utiliser NTi:
string connectionString = "server=MON_IBMI;user=MON_USER;password=MON_PASSWORD;";
Ici, dans cet exemple minimaliste, nous n'utiliserons volontairement pas de service d'injection de dépendance pour la connexion.
2️⃣ Opérations CRUD sur DB2 for i avec NTi + Dapper
Une API REST (Representational State Transfer), c'est une interface de communication permettant à des systèmes différents de dialoguer simplement et de manière standardisée, via des requêtes HTTP classiques (GET POST PUT DELETE). Chaque ressource exposée par une API REST possède une URL spécifique appelée endpoint
.
Nous allons ici implémenter les quatres opérations de base appelées CRUD, correspondant aux actions courantes sur des données:
- CREATE: créer une nouvelle ressources ->
POST
- READ: lire une ou plusieurs ressources existantes ->
GET
- UPDATE: mettre à jour une ressource existante ->
PUT
- DELETE: supprimer une ressource existante ->
DELETE
Dans notre exemple, nous allons exécuter un script dans ACS (IBM i Access Client Solutions) afin de créer une base test GARAGEQ
, ainsi que la table CARS
avec un jeu de donnée initial:
-- Création du SCHEMA GARAGEQ
CREATE SCHEMA GARAGEQ;
-- Création de la table CARS avec quelques véhicules en exemple
CREATE TABLE GARAGEQ.CARS(ID INT GENERATED ALWAYS AS IDENTITY (START WITH 1 INCREMENT BY 1),
BRAND VARCHAR(50),
MODEL VARCHAR(50),
YEAR INT,
COLOR VARCHAR(30),
PRIMARY KEY (ID)
);
-- Insertion d'un jeu de données
INSERT INTO GARAGEQ.CARS (BRAND, MODEL, YEAR, COLOR) VALUES
('Peugeot', '308', 2021, 'Anthracite'),
('Volswagen', 'Caddy', 2015, 'Bleu'),
('Skoda', 'Octavia Combi RS', 2017, 'Blanc');
('Toyota', 'Yaris', 2022, 'Jaune'),
Une fois le script correctement exécuté, côté C# il faut créer une structure, qui puisse correspondre exactement au champs défini dans notre Table DB2:
namespace WebAPIQ
{
public class Car
{
public int Id { get; set; }
public string? Brand { get; set; }
public string? Model { get; set; }
public int Year { get; set; }
public string? Color { get; set; }
}
}
Implémentation des Endpoints
CRUD
GET : Récupérer toutes les voitures (GET /voitures)
Retourne toutes les entrées présentes dans la table DB2
app.MapGet("/cars", () =>
{
using var conn = new NTiConnection(connectionString);
conn.Open();
var cars = conn.Query<Car>("SELECT * FROM GARAGEQ.CARS").ToList();
return Results.Ok(cars);
})
.WithName("GetAllCar")
.WithOpenApi();
POST : Ajouter une nouvelle voiture (POST /voitures)
Ajoute une nouvelle entrée dans notre table DB2. La voiture à créer est fournie par l'utilisateur dans le corps de la requête(au format JSON).
app.MapPost("/cars", (Car car) =>
{
using var conn = new NTiConnection(connectionString);
conn.Open();
string addCarSql = "INSERT INTO GARAGEQ.CARS (BRAND, MODEL, YEAR, COLOR) VALUES (?, ?, ?, ?)";
conn.Execute(addCarSql, new { car.Brand, car.Model, car.Year, car.Color });
return Results.Created("/cars", car);
})
.WithName("AddNewCar")
.WithOpenApi();
PUT : Mettre à jour une voiture (PUT /voitures/{id})
Modifie une voiture existante dans notre table DB2. L'ID de la voiture à modifier est précisé dans l'URL ({id})
, et les nouvelles données sont envoyées dans le corps JSON de la requête.
app.MapPut("/cars/{id}", (int id, Car car) =>
{
using var conn = new NTiConnection(connectionString);
conn.Open();
string updateCar = @"UPDATE GARAGEQ.CARS SET BRAND = ?, MODEL = ?, YEAR = ?, COLOR = ? WHERE ID = ?";
int carModified = conn.Execute(updateCar, new { car.Brand, car.Model, car.Year, car.Color, id });
})
.WithName("UpdateCarById")
.WithOpenApi();
DELETE : Supprimer une voiture (DELETE /voitures/{id})
Supprime une voiture existante via son ID passé dans l'URL.
app.MapDelete("/cars/{id}", (int id) =>
{
using var conn = new NTiConnection(connectionString);
conn.Open();
int carDeleted= conn.Execute("DELETE FROM GARAGEQ.CARS WHERE ID = ?", new { id });
return carDeleted;
})
.WithName("DeleteCarById")
.WithOpenApi();
Chaque endpoint
ouvre une connexion dédiée avec NTi.
La requête SQL est exécutée simplement avec Dapper via conn.Query<T>()
(pour les sélections) ou conn.Execute()
(pour les insertions, modifications, suppressions). Dapper gère automatiquement le mapping entre les résultats SQL et l’objet C# Car.
Et la, aucune surcouche, aucun controller, ca fonctionne et c'est directement exploitable.
3️⃣ Exécuter une commande CL avec vérification système
L’un des avantages de NTi c'est qu'il ne se limite pas à l’accès base de données : il permet aussi d’exécuter des commandes CL (Command Language) directement depuis votre API .NET.
La méthode ExecuteClCommand()
vous permet de piloter le système IBM i en lançant n’importe quelle commande, comme vous le feriez depuis un terminal 5250.
Avant toute exécution, il est souvent recommandé de valider la commande. C’est le rôle de CheckCLCommand()
— disponible via l’extension Toolbox — qui permet de simuler l’exécution côté IBM i pour détecter d’éventuelles erreurs de syntaxe ou paramètres manquants.
app.MapPost("/execute/ClCommand", (string command) =>
{
using var conn = new NTiConnection(connectionString);
conn.Open();
conn.CheckCLCommand(command);
conn.ExecuteClCommand(command);
return $"commande exécutée avec succès";
})
.WithName("ExecuteClCommand")
.WithOpenApi();
4️⃣ Changer dynamiquement la librairie courante
Sur IBM i, la librairie courante (current Library) définit le contexte dans lequel les commandes et programmes sont exécutés. Changer dynamiquement cette librairie dans une API peut être utile pour isoler les traitements, cibler un environnement précis, ou adapter le contexte utilisateur selon l'appelant.
NTi simplifie cela avec la méthode ChangeDataBase(string)
qui définie dynamiquement la librairie courante de la session activee
app.MapPost("/ChangeLibraryOnIbmi", (string libraryName) =>
{
using var conn = new NTiConnection(connectionString);
conn.Open();
conn.ChangeDatabase(libraryName);
return conn.Database;
})
.WithName("ChnageCurrentLibrary")
.WithOpenApi();
A noter que
ChangeDatabase()
agit sur la session en cours, celle du job NTi.
5️⃣ Appeler un programme RPG pour mettre à jour un nom client
NTi vous permet également d’appeler directement un programme RPG depuis votre API .NET, avec passage de paramètres typés (string, int, etc.), comme on le ferait depuis un écran vert ou un batch. On peut donc encapsuler des traitements métiers existants dans une API REST, sans avoir à réécrire toute la logique en C#.
Imaginons un programme RPG nommé PGMCUST01
, situé dans la librairie NORTHWIND
, qui attend deux paramètres :
- Un identifiant client
(id)
— alphanumérique de 5 caractères - Un nouveau nom
(Name)
— alphanumérique de 30 caractères
Voici comment exposer cet appel de programme via un endpoint REST :
app.MapPost("/customer/{id}/name", (string id, Customer customer) =>
{
using var conn = new NTiConnection(connectionString);
conn.Open();
var parameters = new List<NTiProgramParameter>
{
new NTiProgramParameter(id, 5),
new NTiProgramParameter(customer.Name, 30 )
};
conn.ExecuteClCommand("CHGCURLIB NORTHWIND");
conn.CallProgram("NORTHWIND", "PGMCUST01", parameters)
})
.WithName("CallRPGProgram")
.WithOpenApi();
C'est très pratique pour moderniser progressivement votre patrimoine applicatif IBM, sans recoder toute les règles métiers. Vous exposez vos programmes RPG comme de véritable service réutilisables, intégrables dans un portail web ou une app mobile.
7️⃣ Swagger et documentation automatique
Dès la création du projet minimal avec Visual Studio, vous avez surement remarqué que Swagger est activé par défaut. Il permet de générer automatiquement une documentation interactive de notre API REST, accessible directement depuis un navigateur.
Cette interface offre un confort de développement: on peut tester nos endpoint en temps réel, vérifier les paramètres attendus, observer les réponses retournées, ou encore visualiser la structure complète des objets utilisés, comme pour nos models Car
ou Customer
.
Swagger joue également un rôle clé pour la consommation de l' API côté client : la documentation générée permet à des outils externes ou à des développeurs front-end de générer automatiquement du code pour consommer les endpoints, sans avoir à deviner la structure des appels.
Conclusion
On peut voir qu’avec .NET 8, Dapper et NTi Data Provider, il est possible de construire en quelques minutes une API REST complète, légère, mais parfaitement fonctionnelle, capable d’interagir nativement avec un environnement IBM i.
Sans surcouche, sans frameworks complexes, chaque fonctionnalité — de l’exécution de requêtes SQL à l’appel d’un programme RPG, en passant par l’exécution sécurisée de commandes CL — peut être exposée de manière très simple, stable, et interopérable.
Que ce soit pour réaliser un POC, créer des services techniques internes ou initier une stratégie de modernisation progressive, cette base d’API minimaliste est un excellent point de départ — clair, modulaire, et totalement aligné avec les principes du microservice.
using Aumerial.Data.Nti;
using Dapper;
using Aumerial.Toolbox;
using System.Reflection;
namespace WebAPIQ
{
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
string connectionString = "server=MON_IBMI;user=MON_USER;password=MON_PASSWORD;";
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
/// GET ALL
app.MapGet("/cars", () =>
{
using var conn = new NTiConnection(connectionString);
conn.Open();
var cars = conn.Query<Car>("SELECT * FROM GARAGEQ.CARS").ToList();
return cars;
})
.WithName("GetAllCar")
.WithOpenApi();
// GET BY ID
app.MapGet("/cars/{id}", (int id) =>
{
using var conn = new NTiConnection(connectionString);
conn.Open();
var carById = conn.QuerySingleOrDefault<Car>("SELECT * FROM GARAGEQ.CARS WHERE ID = ?", new { id });
return carById;
})
.WithName("GetCarById")
.WithOpenApi();
// POST
app.MapPost("/cars", (Car car) =>
{
using var conn = new NTiConnection(connectionString);
conn.Open();
string addCarSql = "INSERT INTO GARAGEQ.CARS (BRAND, MODEL, YEAR, COLOR) VALUES (?, ?, ?, ?)";
conn.Execute(addCarSql, new { car.Brand, car.Model, car.Year, car.Color });
return Results.Created("/cars", car);
})
.WithName("AddNewCar")
.WithOpenApi();
// PUT
app.MapPut("/cars/{id}", (int id, Car car) =>
{
using var conn = new NTiConnection(connectionString);
conn.Open();
string updateCar = @"UPDATE GARAGEQ.CARS SET BRAND = ?, MODEL = ?, YEAR = ?, COLOR = ? WHERE ID = ?";
conn.Execute(updateCar, new { car.Brand, car.Model, car.Year, car.Color, id });
})
.WithName("UpdateCarById")
.WithOpenApi();
// DELETE
app.MapDelete("/cars/{id}", (int id) =>
{
using var conn = new NTiConnection(connectionString);
conn.Open();
int carDeleted= conn.Execute("DELETE FROM GARAGEQ.CARS WHERE ID = ?", new { id });
return carDeleted;
})
.WithName("DeleteCarById")
.WithOpenApi();
// CALL A CL COMMAND
app.MapPost("/execute/ClCommand", (string command) =>
{
using var conn = new NTiConnection(connectionString);
conn.Open();
conn.CheckCLCommand(command);
conn.ExecuteClCommand(command);
return $"commande exécutée avec succès";
})
.WithName("ExecuteClCommand")
.WithOpenApi();
// CHANGE CURRENT LIBRARY
app.MapPost("/ChangeLibraryOnIbmi", (string libraryName) =>
{
using var conn = new NTiConnection(connectionString);
conn.Open();
conn.ChangeDatabase(libraryName);
return conn.Database;
})
.WithName("ChnageCurrentLibrary")
.WithOpenApi();
// CALL AN EXISTING RPG PGM
app.MapPost("/customer/{id}/name", (string id, Customer customer) =>
{
using var conn = new NTiConnection(connectionString);
conn.Open();
var parameters = new List<NTiProgramParameter>
{
new NTiProgramParameter(id, 5),
new NTiProgramParameter(customer.Name, 30 )
};
conn.ExecuteClCommand("CHGCURLIB NORTHWIND");
conn.CallProgram("NORTHWIND", "PGMCUST01", parameters);
return Results.Ok($"Nom du client ID {id} mis à jour avec succès : {customer.Name}.");
})
.WithName("CallRPGProgram")
.WithOpenApi();
app.Run();
}
}
}