API REST minimale .NET C# su IBM i/AS400 con NTi DataProvider

Erfahren Sie, wie Sie eine minimale REST-API mit .NET 8 und NTi erstellen, um mit einem IBM i / AS/400-System zu interagieren: SQL CRUD, CL-Befehle und RPG-Programmaufrufe.

Illustrationsbild zu Artikel

Introduction

Die Stärke einer Microservice-Architektur liegt in ihrer Einfachheit: leichte, eigenständige Komponenten, die über REST-APIs offengelegt werden und von jedem Dienst oder jeder Anwendung leicht genutzt werden können.

Genau diesen Geist der Einfachheit bietet NTi Data Provider den .NET-Entwicklern. Mit ein paar Zeilen C# können Sie minimale REST-APIs erstellen, die Ihre IBM i-Ressourcen (DB2-Tabellen, CL-Befehle oder RPG-Programme) direkt allen Ihren Anwendungen aussetzen können, seien es Angular- oder React-Webschnittstellen, mobile Anwendungen oder Dienste von Drittanbietern.

Dieses Tutorial zeigt Ihnen konkret, wie Sie sehr schnell und einfach eine minimale API in .NET implementieren können, ohne komplexe Schichten. Ziel ist es, eine technische Ressource bereitzustellen, die innerhalb weniger Minuten nutzbar und reproduzierbar ist, um das Potenzial und die Einfachheit der Nutzung von NTi in einem modernen Microservices-Ansatz konkret zu messen.

Den vollständigen Code für dieses Beispiel finden Sie unten auf dieser Seite.

1️⃣ Initialisierung einer minimalen API in .NET 8

Um Ihre minimale REST-API schnell zu starten, müssen Sie nur Visual Studio öffnen und diesen einfachen Schritten folgen:

  • Klicken Sie auf Neues Projekt > Webapplikation ASP.NET CORE..
  • Wählen Sie das Framework .NET 8 > entfernen Sie das Häkchen bei Controller verwenden, um das minimalistische Modell zu nutzen.

In diesem Schritt erzeugt Visual Studio automatisch eine fertige Program.cs-Datei mit standardmäßig konfiguriertem Swagger. So sind Sie direkt bereit, Ihre API-Endpunkte zu implementieren. Jetzt müssen Sie nur noch NTi Data Provider sowie Dapper hinzufügen.

Öffnen Sie Ihre Konsole und installieren Sie die folgenden Pakete :

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

Verweisen Sie sie dann in Ihrem Projekt:

using Aumerial.Data.Nti;
using Dapper;

Anschließend muss in der Datei program.cs die Verbindungszeichenfolge festgelegt werden, damit eine Verbindung zu DB2 for i hergestellt und NTi verwendet werden kann:

string connectionString = "server=MON_IBMI;user=MON_USER;password=MON_PASSWORD;";

Hier, in diesem minimalistischen Beispiel, werden wir absichtlich keinen Dependency Injection Service für die Verbindung verwenden.

2️⃣ CRUD-Operationen auf DB2 for i mit NTi + Dapper

Eine REST-API (Representational State Transfer) ist eine Kommunikationsschnittstelle, über die verschiedene Systeme einfach und standardisiert über klassische HTTP-Anfragen (GET POST PUT DELETE) miteinander kommunizieren können. Jede von einer REST-API ausgestellte Ressource hat eine bestimmte URL, die als Endpunkt bezeichnet wird.

Wir werden hier die vier grundlegenden Operationen namens CRUD implementieren, die den üblichen Aktionen mit Daten entsprechen:

  • CREATE: Erstellen einer neuen Ressource -> POST.
  • READ: eine oder mehrere vorhandene Ressourcen lesen -> GET
  • UPDATE: eine vorhandene Ressource aktualisieren -> PUT
  • DELETE: eine vorhandene Ressource löschen-> DELETE

In unserem Beispiel werden wir ein Skript in ACS (IBM i Access Client Solutions) ausführen, um eine Testdatenbank GARAGEQ sowie die Tabelle CARS mit einem anfänglichen Datensatz zu erstellen:

-- Erstellung des SCHEMA GARAGEQ
CREATE SCHEMA GARAGEQ;

-- Erstellen der Tabelle CARS mit einigen Beispielfahrzeugen
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)
);

-- Einfügen eines Datensatzes
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'),

Nachdem das Skript korrekt ausgeführt wurde, muss auf der C#-Seite eine Struktur erstellt werden, die genau den Feldern entspricht, die in unserer DB2-Tabelle definiert sind:

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; }
    }
}

Implementierung von Endpoints CRUD

GET : Alle Autos abrufen (GET /Autos).

Gibt alle in der Tabelle DB2 vorhandenen Einträge zurück

 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 : Ein neues Auto hinzufügen (POST /Autos).

Fügt einen neuen Eintrag in unsere DB2-Tabelle ein. Das zu erstellende Auto wird vom Benutzer im Körper der Abfrage(im JSON-Format) angegeben.

  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 : Ein Auto aktualisieren (PUT /autos/).

Ändert ein vorhandenes Auto in unserer DB2-Tabelle. Die ID des zu ändernden Autos wird in der URL ({id}) angegeben, und die neuen Daten werden im JSON-Body der Anfrage gesendet.

 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 : Ein Auto löschen (DELETE /autos/).

Löscht ein vorhandenes Auto über seine in der URL übergebene ID.

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

Jeder Endpunkt öffnet eine dedizierte Verbindung zu NTi.

Die SQL-Abfrage wird einfach mit Dapper über conn.Query<T>() (für Selektionen) oder conn.Execute() (für Einfügungen, Änderungen, Löschungen) ausgeführt. Dapper übernimmt automatisch das Mapping zwischen den SQL-Ergebnissen und dem C#-Car-Objekt.

Und hier gibt es kein Overlay, keinen Controller, es funktioniert und ist direkt nutzbar.

3️⃣ Ausführen eines CL-Befehls mit Systemprüfung

Einer der Vorteile von NTi ist, dass es sich nicht auf den Datenbankzugriff beschränkt: Es ermöglicht auch die Ausführung von CL-Befehlen (Command Language) direkt aus Ihrer .NET-API.

Mit der Methode ExecuteClCommand() können Sie das IBM i-System steuern, indem Sie einen beliebigen Befehl ausführen, wie Sie es auch von einem 5250-Terminal aus tun würden.

Vor der Ausführung ist es oft ratsam, die Bestellung zu bestätigen. Dies ist die Aufgabe von CheckCLCommand() - verfügbar über die Toolbox-Erweiterung -, mit dem die Ausführung auf der IBM i-Seite simuliert werden kann, um mögliche Syntaxfehler oder fehlende Parameter zu erkennen.

  app.MapPost("/execute/ClCommand", (string command) =>
  {
      using var conn = new NTiConnection(connectionString);
      conn.Open();
      conn.CheckCLCommand(command);
      conn.ExecuteClCommand(command);
      return $"erfolgreich ausgeführter Befehl";
  })
  .WithName("ExecuteClCommand")
  .WithOpenApi();

4️⃣ Dynamisches Ändern der aktuellen Bibliothek

Auf IBM i definiert die aktuelle Bibliothek (current library) den Kontext, in dem Befehle und Programme ausgeführt werden. Das dynamische Ändern dieser Bibliothek in einer API kann nützlich sein, um Verarbeitungen zu isolieren, eine bestimmte Umgebung anzusprechen oder den Benutzerkontext an den Aufrufer anzupassen.

NTi vereinfacht dies mit der Methode ChangeDataBase(string), die dynamisch die aktuelle Bibliothek der aktiven Sitzung definiert.

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

     conn.ChangeDatabase(libraryName);

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

Beachten Sie, dass ChangeDatabase() auf die aktuelle Sitzung, die des NTi-Jobs, wirkt.

5️⃣ Ein RPG-Programm aufrufen, um einen Kundennamen zu aktualisieren

NTi ermöglicht es Ihnen auch, ein RPG-Programm direkt von Ihrer .NET-API aus aufzurufen, wobei typisierte Parameter (string, int, etc.) übergeben werden, wie man es von einem Greenscreen oder einem Batch aus tun würde. Man kann also bestehende Geschäftsverarbeitungen in eine REST-API einkapseln, ohne die gesamte Logik in C# neu schreiben zu müssen.

Stellen wir uns ein RPG-Programm namens PGMCUST01 vor, das sich in der Bibliothek NORTHWIND befindet und zwei Parameter erwartet:

  • Eine Kundenkennung (id) - alphanumerisch, 5 Zeichen lang
  • Ein neuer Name (Name) - alphanumerisch mit 30 Zeichen

So stellen Sie diesen Programmaufruf über einen REST-Endpunkt aus :

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

Dies ist sehr praktisch, um Ihren IBM-Anwendungsbestand schrittweise zu modernisieren, ohne alle Geschäftsregeln neu zu codieren. Sie stellen Ihre RPG-Programme als echte, wiederverwendbare Dienste dar, die in ein Webportal oder eine mobile App integriert werden können.

7️⃣ Swagger und automatische Dokumentation

Schon beim Erstellen des Minimalprojekts mit Visual Studio haben Sie sicher bemerkt, dass Swagger standardmäßig aktiviert ist. Damit wird automatisch eine interaktive Dokumentation unserer REST-API erzeugt, auf die direkt über einen Browser zugegriffen werden kann.

Diese Schnittstelle bietet Komfort bei der Entwicklung: Wir können unsere Endpunkte in Echtzeit testen, die erwarteten Parameter überprüfen, die zurückgegebenen Antworten beobachten oder auch die vollständige Struktur der verwendeten Objekte anzeigen, wie bei unseren Modellen Car oder Customer.

Swagger spielt auch eine Schlüsselrolle bei der clientseitigen API-Konsumierung: Die generierte Dokumentation ermöglicht es externen Tools oder Frontend-Entwicklern, automatisch Code zu generieren, um die Endpunkte zu konsumieren, ohne die Struktur der Aufrufe erraten zu müssen.

Schlussfolgerung

Man sieht, dass es mit .NET 8, Dapper und NTi Data Provider möglich ist, in wenigen Minuten eine vollständige, schlanke, aber voll funktionsfähige REST-API zu bauen, die nativ mit einer IBM i-Umgebung interagieren kann.

Ohne Overlay, ohne komplexe Frameworks kann jede Funktionalität - von der Ausführung von SQL-Abfragen über den Aufruf eines RPG-Programms bis hin zur sicheren Ausführung von CL-Befehlen - auf sehr einfache, stabile und interoperable Weise dargestellt werden. Ob Sie einen POC durchführen, interne technische Dienste einrichten oder eine schrittweise Modernisierungsstrategie einleiten wollen, diese minimalistische API-Basis ist ein hervorragender Ausgangspunkt - übersichtlich, modular und vollständig auf die Prinzipien des Microservice abgestimmt.

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