La force d'une architecture microservices réside dans sa simplicité : des composants légers, autonomes, exposés via des API REST et consommables par n'importe quel service ou application.
NTi Data Provider s'inscrit naturellement dans cette logique. En quelques lignes de C#, il est possible d'exposer directement les ressources IBM i (tables DB2, commandes CL, programmes RPG) sous forme d'endpoints REST prêts à l'emploi.
Ce tutoriel montre comment construire cette API en .NET 8, sans surcouche ni framework complexe. Une base technique reproductible en quelques minutes, conçue pour mesurer concrètement la simplicité de NTi dans une approche microservices.
Le code complet de cet exemple est disponible en bas de cette page.
Étape 1 - Initialiser une API minimale en .NET 8
Créer le projet depuis la ligne de commande :
dotnet new webapi -n myApp --use-minimal-apis --framework net8.0
cd myApp
Le projet est généré avec un fichier Program.cs prêt à l'emploi et Swagger configuré par défaut. Il ne reste plus qu'à ajouter les packages nécessaires :
dotnet add package Aumerial.Data.Nti
dotnet add package Aumerial.Toolbox
dotnet add package Dapper
Puis les référencer dans Program.cs :
using Aumerial.Data.Nti;
using Aumerial.Toolbox;
using Dapper;
Enfin, définir la chaîne de connexion dans Program.cs :
string connectionString = "server=Server;user=User;password=Pwd;";
Dans cet exemple minimaliste, l'injection de dépendances pour la connexion est volontairement écartée.
Étape 2 - Opérations CRUD sur DB2 for i avec NTi et Dapper
Une API REST expose des ressources via des requêtes HTTP classiques (GET, POST, PUT, DELETE). Les quatre opérations de base, appelées CRUD, correspondent aux actions courantes sur des données :
- CREATE : créer une nouvelle ressource →
POST - READ : lire une ou plusieurs ressources →
GET - UPDATE : mettre à jour une ressource existante →
PUT - DELETE : supprimer une ressource existante →
DELETE
Pour cet exemple, un script ACS crée la base de test GARAGEQ et la table CARS avec un jeu de données initial :
-- Création du SCHEMA GARAGEQ
CREATE SCHEMA GARAGEQ;
-- Création de la table CARS
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'),
('Volkswagen', 'Caddy', 2015, 'Bleu'),
('Skoda', 'Octavia Combi RS', 2017, 'Blanc'),
('Toyota', 'Yaris', 2022, 'Jaune');
Côté C#, une classe Car correspond exactement aux champs de la table DB2 :
namespace myApp
{
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 :
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 :
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 :
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 - supprimer une voiture :
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 avec Dapper via conn.Query<T>() pour les sélections, ou conn.Execute() pour les insertions, modifications et suppressions. Dapper gère automatiquement le mapping entre les résultats SQL et l'objet C# Car.
Aucune surcouche, aucun controller.
Étape 3 - Exécuter une commande CL avec vérification système
NTi ne se limite pas à l'accès base de données : il permet aussi d'exécuter des commandes CL directement depuis une API .NET. La méthode ExecuteClCommand() permet de piloter l'IBM i en lançant n'importe quelle commande, comme depuis un terminal 5250.
Avant toute exécution, CheckCLCommand() (disponible via l'extension Toolbox) permet de valider la commande côté IBM i pour détecter d'éventuelles erreurs de syntaxe ou paramètres manquants, sans l'exécuter :
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();Étape 4 - Changer dynamiquement la bibliothèque courante
Sur IBM i, la bibliothèque courante définit le contexte dans lequel les commandes et programmes sont exécutés. La méthode ChangeDatabase() de NTi permet de la modifier dynamiquement dans la session active :
app.MapPost("/ChangeLibraryOnIbmi", (string libraryName) =>
{
using var conn = new NTiConnection(connectionString);
conn.Open();
conn.ChangeDatabase(libraryName);
return conn.Database;
})
.WithName("ChangeCurrentLibrary")
.WithOpenApi();
ChangeDatabase()agit uniquement sur la session en cours, celle du job NTi.
Étape 5 - Appeler un programme RPG pour mettre à jour un nom client
NTi permet d'appeler directement un programme RPG depuis une API .NET, avec passage de paramètres typés, comme depuis un écran vert ou un batch. Les traitements métiers existants peuvent ainsi être encapsulés dans une API REST, sans réécrire toute la logique en C#.
Le programme RPG PGMCUST01, situé dans la bibliothèque NORTHWIND, 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);
return Results.Ok($"Nom du client ID {id} mis à jour avec succès : {customer.Name}.");
})
.WithName("CallRPGProgram")
.WithOpenApi();Étape 6 - Swagger et documentation automatique
Swagger est activé par défaut dans le template webapi généré par dotnet new webapi. Il génère automatiquement une documentation interactive de l'API REST, accessible directement depuis un navigateur. Cette interface permet de tester les endpoints en temps réel, vérifier les paramètres attendus, observer les réponses retournées, et visualiser la structure complète des objets utilisés comme 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.
Conclusion
Avec .NET 8, Dapper et NTi Data Provider, il est possible de construire en quelques minutes une API REST complète, légère et parfaitement fonctionnelle, capable d'interagir nativement avec un environnement IBM i.
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 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 aligné avec les principes du microservice.
using Aumerial.Data.Nti;
using Dapper;
using Aumerial.Toolbox;
namespace myApp
{
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=Server;user=User;password=Pwd;";
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();
// EXECUTE 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("ChangeCurrentLibrary")
.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();
}
}
}Quentin Destrade