Introduzione
La forza di un'architettura a microservizi sta nella sua semplicità: componenti leggeri e autonomi, esposti tramite API REST e facilmente sfruttabili da qualsiasi servizio o applicazione.
È proprio questo spirito di semplicità che NTi Data Provider offre agli sviluppatori .NET. Bastano poche righe di C# per creare API REST minime, in grado di esporre direttamente le risorse IBM i (tabelle DB2, comandi CL o programmi RPG) a tutte le applicazioni, siano esse interfacce web Angular o React, applicazioni mobili o servizi di terze parti.
Questo tutorial mostra come implementare un'API minima in .NET in modo molto rapido e semplice, senza livelli complessi. L'obiettivo è quello di fornire una risorsa tecnica che possa essere utilizzata e riprodotta in pochi minuti, per dare una misura concreta del potenziale e della facilità d'uso di NTi in un moderno approccio a microservizi.
Il codice completo di questo esempio è disponibile in fondo a questa pagina.
1️⃣ Inizializzare un'API minima in .NET 8
Per iniziare rapidamente con la vostra API REST minima, basta aprire Visual Studio e seguire questi semplici passaggi:
- Fare clic su Nuovo progetto > Applicazione web ASP.NET CORE.
- Selezionare il framework .NET 8 > deselezionare Use Controllers per sfruttare il modello minimalista.
In questa fase, Visual Studio genera automaticamente un file Programma.cs
pronto all'uso, con Swagger configurato in modo predefinito. Questo significa che siete pronti a implementare subito i vostri endpoint API. Non resta che aggiungere NTi Data Provider e Dapper.
Aprite la console e installate i seguenti pacchetti:
dotnet add package Aumerial.Data.Nti
dotnet add package Dapper
Quindi fare riferimento ad essi nel progetto:
using Aumerial.Data.Nti;
using Dapper;
È quindi necessario definire la stringa di connessione nel file program.cs
, in modo da potersi connettere a DB2 for i e utilizzare NTi:
string connectionString = "server=MON_IBMI;user=MON_USER;password=MON_PASSWORD;";
In questo esempio minimalista, non stiamo deliberatamente utilizzando un servizio di dependency injection per la connessione.
2️⃣ Operazioni CRUD su DB2 for i con NTi + Dapper
Un'API REST (Representational State Transfer) è un'interfaccia di comunicazione che consente a diversi sistemi di comunicare in modo semplice e standardizzato, utilizzando richieste HTTP standard (GET POST PUT DELETE). Ogni risorsa esposta da un'API REST ha un URL specifico chiamato endpoint
.
Qui implementeremo le quattro operazioni di base note come CRUD, che corrispondono alle azioni comuni sui dati:
- CREATE: creare una nuova risorsa ->
POST
- READ: leggere una o più risorse esistenti ->
GET
- UPDATE: aggiornare una risorsa esistente->
PUT
- DELETE: eliminare una risorsa esistente ->
DELETE
Nel nostro esempio, eseguiremo uno script in ACS (IBM i Access Client Solutions) per creare un database di prova GARAGEQ
, nonché la tabella CARS
con un set di dati iniziale:
-- Creazione dello SCHEMA GARAGEQ
CREATE SCHEMA GARAGEQ;
-- Creazione della tabella 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)
);
-- Inserimento di un set di dati
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'),
Una volta eseguito correttamente lo script, sul lato C# dobbiamo creare una struttura che corrisponda esattamente ai campi definiti nella nostra tabella 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; }
}
}
Implementazione degli endpoint
CRUD
GET : Recuperare tutte le auto (GET /cars)
Restituisce tutte le voci della tabella DB2
app.MapGet("/cars", () =>
{
using var conn = new NTiConnection(connectionString);
conn.Open();
var cars = conn.Query("SELECT * FROM GARAGEQ.CARS").ToList();
return Results.Ok(cars);
})
.WithName("GetAllCar")
.WithOpenApi();
POST : Aggiungere una nuova auto (POST /auto).
Aggiunge una nuova voce alla nostra tabella DB2. L'auto da creare viene fornita dall'utente nel corpo della richiesta (in formato 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 : Aggiornare un'auto ( (PUT /cars/)
Modifica un'auto esistente nella nostra tabella DB2. L'ID dell'auto da modificare è specificato nell'URL ({id})
e i nuovi dati sono inviati nel corpo JSON della richiesta.
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 : Cancellare un'auto (DELETE /cars/).
Elimina un'auto esistente inserendo il suo ID nell'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();
Ogni endpoint apre una connessione dedicata con NTi.
La query SQL viene semplicemente eseguita con Dapper tramite conn.Query<T>()
(per le selezioni) o conn.Execute()
(per gli inserimenti, le modifiche e le cancellazioni). Dapper gestisce automaticamente la mappatura tra i risultati SQL e l'oggetto C# Car.
E qui non c'è nessuna sovrapposizione, nessun controller, funziona e si può usare subito.
3️⃣ Eseguire un comando CL con controllo del sistema
Uno dei vantaggi di NTi è che non si limita all'accesso al database: è anche possibile eseguire comandi CL (Command Language) direttamente dall'API .NET.
Il metodo ExecuteClCommand()
consente di controllare il sistema IBM i emettendo qualsiasi comando, proprio come se si usasse un terminale 5250.
Prima di qualsiasi esecuzione, è spesso consigliabile convalidare l'ordine. Questo è il ruolo di CheckCLCommand()
- disponibile tramite l'estensione Toolbox - che simula l'esecuzione sul lato IBM i per rilevare eventuali errori di sintassi o parametri mancanti.
app.MapPost("/execute/ClCommand", (string command) =>
{
using var conn = new NTiConnection(connectionString);
conn.Open();
conn.CheckCLCommand(command);
conn.ExecuteClCommand(command);
return $"comando eseguito con successo";
})
.WithName("ExecuteClCommand")
.WithOpenApi();
4️⃣ Cambiare dinamicamente la libreria corrente
Su IBM i, la libreria corrente definisce il contesto in cui vengono eseguiti i comandi e i programmi. La modifica dinamica di questa libreria in un'API può essere utile per isolare i processi, puntare a un ambiente specifico o adattare il contesto dell'utente in base al chiamante.
NTi semplifica questa operazione con il metodo ChangeDataBase(string)
, che definisce dinamicamente la libreria corrente per la sessione attiva.
app.MapPost("/ChangeLibraryOnIbmi", (string libraryName) =>
{
using var conn = new NTiConnection(connectionString);
conn.Open();
conn.ChangeDatabase(libraryName);
return conn.Database;
})
.WithName("ChnageCurrentLibrary")
.WithOpenApi();
Si noti che
ChangeDatabase()
agisce sulla sessione corrente, quella del lavoro NTi.
5️⃣ Richiamo di un programma RPG per aggiornare il nome di un cliente
NTi consente anche di chiamare un programma RPG direttamente dall'API .NET, passando parametri digitati (stringa, int, ecc.), proprio come si farebbe da un green screen o da un batch. Possiamo quindi incapsulare i processi aziendali esistenti in un'API REST, senza dover riscrivere tutta la logica in C#.
Immaginiamo un programma RPG chiamato PGMCUST01
, situato nella libreria NORTHWIND
, che si aspetta due parametri:
- Un identificativo del cliente (id) di 5 caratteri alfanumerici
- Un nuovo nome
(Nome)
- alfanumerico, lungo 30 caratteri
Ecco come esporre questa chiamata di programma tramite un endpoint REST:
app.MapPost("/customer/{id}/name", (string id, Customer customer) =>
{
using var conn = new NTiConnection(connectionString);
conn.Open();
var parameters = new List
{
new NTiProgramParameter(id, 5),
new NTiProgramParameter(customer.Name, 30 )
};
conn.ExecuteClCommand("CHGCURLIB NORTHWIND");
conn.CallProgram("NORTHWIND", "PGMCUST01", parameters)
})
.WithName("CallRPGProgram")
.WithOpenApi();
È un modo molto pratico per modernizzare gradualmente le applicazioni IBM, senza dover ricodificare tutte le regole aziendali. Potete presentare i vostri programmi RPG come veri e propri servizi riutilizzabili che possono essere integrati in un portale web o in un'applicazione mobile.
7️⃣ Swagger e la documentazione automatica
Non appena si crea un progetto minimo con Visual Studio, si noterà che Swagger è attivato per impostazione predefinita. Genera automaticamente una documentazione interattiva della nostra API REST, accessibile direttamente da un browser.
Questa interfaccia facilita lo sviluppo: possiamo testare i nostri endpoint in tempo reale, controllare i parametri attesi, osservare le risposte restituite o persino visualizzare la struttura completa degli oggetti utilizzati, come nel caso dei nostri modelli Car
o Customer
.
Swagger svolge anche un ruolo chiave nel consumo dell'API sul lato client: la documentazione generata consente agli strumenti esterni o agli sviluppatori front-end di generare automaticamente il codice per consumare gli endpoint, senza dover indovinare la struttura delle chiamate.
Conclusione
Possiamo vedere che con .NET 8, Dapper e NTi Data Provider, è possibile costruire in pochi minuti un'API REST completa, leggera ma perfettamente funzionale, in grado di interagire in modo nativo con un ambiente IBM i.
Senza overlay e senza framework complessi, ogni funzionalità - dall'esecuzione di query SQL alla chiamata di un programma RPG, all'esecuzione sicura di comandi CL - può essere esposta in modo molto semplice, stabile e interoperabile.
Sia che stiate realizzando un POC, creando servizi tecnici interni o avviando una strategia di modernizzazione progressiva, questa base API minimalista è un ottimo punto di partenza: chiara, modulare e pienamente allineata ai principi dei microservizi.
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=server;user=user;password=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("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("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
{
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();
}
}
}