API REST mínima .NET C# en IBM i/AS400 con NTi DataProvider

Aprenda a crear una API REST mínima con .NET 8 y NTi para interactuar con un sistema IBM i / AS/400: CRUD SQL, comandos CL y llamadas a programas RPG.

Imagen ilustrativa del artículo

Introducción

La fuerza de una arquitectura de microservicios reside en su simplicidad: componentes ligeros y autónomos, expuestos mediante API REST y fácilmente explotables por cualquier servicio o aplicación.

Es precisamente este espíritu de simplicidad el que NTi Data Provider ofrece a los desarrolladores .NET. Basta con unas pocas líneas de C# para crear API REST mínimas, capaces de exponer directamente sus recursos IBM i (tablas DB2, comandos CL o programas RPG) a todas sus aplicaciones, ya sean interfaces web Angular o React, aplicaciones móviles o servicios de terceros.

Este tutorial muestra cómo implementar una API mínima en .NET de forma muy rápida y sencilla, sin capas complejas. El objetivo es proporcionar un recurso técnico que se pueda utilizar y reproducir en tan solo unos minutos, para darle una idea concreta del potencial y la facilidad de uso de NTi en un enfoque moderno de microservicios.

El código completo de este ejemplo está disponible al final de esta página.

1️⃣ Inicializar una API mínima en .NET 8

Para empezar rápidamente con su API REST mínima, sólo tiene que abrir Visual Studio y seguir estos sencillos pasos:

  • Haga clic en Nuevo proyecto > Aplicación web ASP.NET CORE.
  • Seleccione el marco .NET 8 > desmarque Usar controladores para aprovechar el modelo minimalista.

En esta fase, Visual Studio genera automáticamente un archivo Program.cs listo para usar con Swagger configurado por defecto. Esto significa que estás listo para implementar tus puntos finales de API de inmediato. Ahora todo lo que tienes que hacer es añadir el NTi Data Provider y Dapper.

Abra su consola e instale los siguientes paquetes:

dotnet add package Aumerial.Data.Nti
dotnet add package Dapper

A continuación, haz referencia a ellos en tu proyecto:

using Aumerial.Data.Nti;
using Dapper;

A continuación, debe definir la cadena de conexión en el archivo program.cs, para poder conectarse a DB2 for i y utilizar NTi:

string connectionString = "server=server;user=user;password=password;";

Aquí, en este ejemplo minimalista, deliberadamente no estamos utilizando un servicio de inyección de dependencia para la conexión.

2️⃣ Operaciones CRUD en DB2 para i con NTi + Dapper

Una API REST (Representational State Transfer) es una interfaz de comunicación que permite a diferentes sistemas comunicarse de forma sencilla y estandarizada, utilizando peticiones HTTP estándar (GET POST PUT DELETE). Cada recurso expuesto por una API REST tiene una URL específica denominada endpoint.

Aquí implementaremos las cuatro operaciones básicas conocidas como CRUD, correspondientes a acciones comunes sobre los datos:

  • CREATE: crear un nuevo recurso-> POST
  • READ: leer uno o varios recursos existentes -> GET
  • UPDATE: actualizar un recurso existente-> PUT
  • DELETE: eliminar un recurso existente -> DELETE

En nuestro ejemplo, vamos a ejecutar un script en ACS (IBM i Access Client Solutions) para crear una base de datos de prueba GARAGEQ, así como la tabla CARS con un conjunto de datos inicial:

-- Creación del SCHEMA GARAGEQ
CREATE SCHEMA GARAGEQ;

-- Creación de la tabla CARS con algunos vehículos de ejemplo
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)
);

-- Insertar un conjunto de datos
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 vez ejecutado correctamente el script, en la parte de C# tenemos que crear una estructura que se corresponda exactamente con los campos definidos en nuestra tabla 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; }
    }
}

Implementación de puntos finales CRUD

GET : Recuperar todos los coches (GET /cars).

Devuelve todas las entradas de la tabla 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 : Añadir un nuevo coche (POST /cars)

Añade una nueva entrada a nuestra tabla DB2. El coche a crear es suministrado por el usuario en el cuerpo de la petición (en 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 : Actualizar un coche (PUT /cars/)

Modifica un coche existente en nuestra tabla DB2. El ID del coche a modificar se especifica en la URL ({id}), y los nuevos datos se envían en el cuerpo JSON de la petición.

 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 : Borrar un coche (DELETE /cars/))

Borra un coche existente introduciendo su ID en la 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();

Cada punto final abre una conexión dedicada con NTi.

La consulta SQL se ejecuta simplemente con Dapper mediante conn.Query<T>() (para selecciones) o conn.Execute() (para inserciones, modificaciones, eliminaciones). Dapper gestiona automáticamente la correspondencia entre los resultados SQL y el objeto Car de C#.

Y aquí, no hay superposición, no hay controlador, simplemente funciona y se puede utilizar de inmediato.

3️⃣ Ejecutar un comando CL con comprobación del sistema

Una de las ventajas de NTi es que no se limita al acceso a bases de datos: también puede ejecutar comandos CL (lenguaje de comandos) directamente desde su API .NET.

El método ExecuteClCommand() le permite controlar el sistema IBM i emitiendo cualquier comando, tal y como lo haría desde un terminal 5250.

Antes de cualquier ejecución, suele ser aconsejable validar la orden. Esta es la función de CheckCLCommand() - disponible a través de la extensión Toolbox - que simula la ejecución en el lado IBM i para detectar cualquier error de sintaxis o parámetros que falten.

  app.MapPost("/execute/ClCommand", (string command) =>
  {
      using var conn = new NTiConnection(connectionString);
      conn.Open();
      conn.CheckCLCommand(command);
      conn.ExecuteClCommand(command);
      return $"comando ejecutado correctamente";
  })
  .WithName("ExecuteClCommand")
  .WithOpenApi();

4️⃣ Cambiar dinámicamente la biblioteca actual

En IBM i, la biblioteca actual define el contexto en el que se ejecutan los comandos y programas. Cambiar esta biblioteca dinámicamente en una API puede ser útil para aislar procesos, dirigirse a un entorno específico o adaptar el contexto de usuario en función de la persona que llama.

NTi simplifica esto con el método ChangeDataBase(string), que define dinámicamente la biblioteca actual para la sesión activa.

 app.MapPost("/ChangeLibraryOnIbmi", (string libraryName) =>
 {
     using var conn = new NTiConnection(connectionString);
     conn.Open();

     conn.ChangeDatabase(libraryName);

     return conn.Database;
 })
 .WithName("ChnageCurrentLibrary")
 .WithOpenApi();

Tenga en cuenta que ChangeDatabase() actúa sobre la sesión actual, la del trabajo NTi.

5️⃣ Llamada a un programa RPG para actualizar el nombre de un cliente

NTi también le permite llamar a un programa RPG directamente desde su API .NET, pasando parámetros tipificados (string, int, etc.), tal como lo haría desde una pantalla verde o un lote. De este modo, puede encapsular los procesos empresariales existentes en una API REST, sin tener que reescribir toda la lógica en C#.

Imaginemos un programa RPG llamado PGMCUST01, ubicado en la librería NORTHWIND, que espera dos parámetros:

  • Una identificación de cliente (id) — Código alfanumérico de 5 caracteres
  • Un nuevo nombre (Name) — Código alfanumérico de 30 caracteres

A continuación se explica cómo exponer esta llamada de programa a través de un punto final 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();

Es una forma muy práctica de modernizar gradualmente sus aplicaciones IBM, sin tener que recodificar todas sus reglas de negocio. Puede presentar sus programas RPG como auténticos servicios reutilizables que pueden integrarse en un portal web o una aplicación móvil.

7️⃣ Swagger y documentación automática

En cuanto crees un proyecto mínimo con Visual Studio, te habrás dado cuenta de que Swagger está activado por defecto. Esto genera automáticamente documentación interactiva de nuestra API REST, accesible directamente desde un navegador.

Esta interfaz facilita el desarrollo: podemos probar nuestros endpoints en tiempo real, comprobar los parámetros esperados, observar las respuestas devueltas, o incluso ver la estructura completa de los objetos utilizados, como con nuestros modelos Car o Customer.

Swagger también desempeña un papel clave en el consumo de la API en el lado del cliente: la documentación generada permite a las herramientas externas o a los desarrolladores front-end generar automáticamente código para consumir los endpoints, sin tener que adivinar la estructura de las llamadas.

Conclusión

Podemos ver que con .NET 8, Dapper y NTi Data Provider, es posible construir en pocos minutos una API REST completa, ligera pero perfectamente funcional, capaz de interactuar de forma nativa con un entorno IBM i.

Sin superposiciones ni marcos de trabajo complejos, todas las funciones -desde la ejecución de consultas SQL hasta la llamada a un programa RPG, pasando por la ejecución segura de comandos CL- pueden exponerse de forma muy sencilla, estable e interoperable. Tanto si está construyendo una POC, creando servicios técnicos internos o iniciando una estrategia de modernización progresiva, esta base de API minimalista es un excelente punto de partida: clara, modular y totalmente alineada con los principios de microservicio.

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("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();
        }
    }
}