Create a minimalist REST API with .NET, C# and NTi DataProvider on IBM i/AS/400

Quentin DESTRADE

Thursday 17 April 2025

  • Tutorials

Discover how to create a minimal REST API with .NET 8 and NTi to interact with an IBM i / AS/400 system: CRUD SQL, CL commands, and RPG program calls.

Main image of the article “Create a minimalist REST API with .NET, C# and NTi DataProvider on IBM i/AS/400”

Introduction

The strength of a microservices architecture lies in its simplicity: lightweight, autonomous components, exposed via REST APIs, and easily exploitable by any service or application.

This is precisely the spirit of simplicity that NTi Data Provider offers to .NET developers. A few lines of C# are all it takes to create minimal REST APIs, capable of directly exposing your IBM i resources (DB2 tables, CL commands or RPG programs) to all your applications, whether Angular or React web interfaces, mobile applications or third-party services.

This tutorial shows you how to implement a minimal API in .NET quickly and simply, without complex layers. The goal is to provide a technical resource that can be used and reproduced in just a few minutes, in order to measure in concrete terms the potential and ease of use of NTi in a modern microservices approach.

The complete code for this example is available at the bottom of this page.

1️⃣ Initializing a minimal API in .NET 8

To get started quickly with your minimal REST API, simply open Visual Studio and follow these simple steps:

  • Click on New project > Application web ASP.NET CORE.
  • Select the .NET 8 framework > uncheck Use Controllers to take advantage of the minimalist model.

At this stage, Visual Studio automatically generates a ready-to-use Program.cs file with Swagger configured by default. This means you're immediately ready to implement your API endpoints. All that remains now is to add NTi Data Provider and Dapper.

Open your console and install the following packages:

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

Then reference them in your project:

using Aumerial.Data.Nti;
using Dapper;

We then need to define the connection string in the program.cs file, to be able to connect to DB2 for i and use NTi:

string connectionString = "server=MY-IBMI;user=MY_USER;password=MY_PASSWORD;";

Here, in this minimalist example, we're deliberately not using a dependency injection service for the connection.

2️⃣ CRUD operations on DB2 for i with NTi + Dapper

A REST API (Representational State Transfer) is a communication interface that enables different systems to communicate in a simple, standardized way, via standard HTTP requests (GET POST PUT DELETE). Each resource exposed by a REST API has a specific URL called endpoint.

Here we'll implement the four basic operations called CRUD, corresponding to common actions on data:

  • CREATE: create a new resource -> POST
  • READ: read one or more existing resources-> GET
  • UPDATE: update an existing resource -> PUT
  • DELETE: delete an existing resource -> DELETE

In our example, we'll run a script in ACS (IBM i Access Client Solutions) to create a test database GARAGEQ, as well as the CARS table with an initial data set:

-- Creation of SCHEMA GARAGEQ
CREATE SCHEMA GARAGEQ;

-- Creation of the CARS table
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)
);

-- Inserting a dataset
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'),

Once the script has been correctly executed, on the C# side we need to create a structure that corresponds exactly to the fields defined in our DB2 table:

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

Implementing CRUD endpoints

GET : Retrieve all cars (GET /cars).

Returns all entries in the DB2 table

 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 : Add a new car (POST /cars)

Adds a new entry to our DB2 table. The car to be created is supplied by the user in the request body (in JSON format).

  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 : Update a car (PUT /cars/{id})

Modifies an existing car in our DB2 table. The ID of the car to be modified is specified in the URL ({id}), and the new data is sent in the JSON body of the request.

 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 : Delete a car (DELETE /cars/{id})

Deletes an existing car via its ID passed in the 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();

Each endpoint opens a dedicated connection with NTi.

The SQL query is simply executed with Dapper via conn.Query<T>() (for selections) or conn.Execute() (for insertions, modifications, deletions). Dapper automatically manages the mapping between SQL results and the C# Car object.

And here, there's no overlay, no controller, it just works and can be used straight away.

3️⃣ Execute a CL command with system check

One of the advantages of NTi is that it's not limited to database access: you can also execute CL (Command Language) commands directly from your .NET API.

The ExecuteClCommand() method lets you control the IBM i system by issuing any command, just as you would from a 5250 terminal.

Prior to execution, it is often advisable to validate the order. This is the role of CheckCLCommand() - available via the Toolbox extension - which simulates execution on the IBM i side to detect any syntax errors or missing parameters.

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

4️⃣ Dynamically change the current library

On IBM i, the current library defines the context in which commands and programs are executed. Dynamically changing this library in an API can be useful for isolating processes, targeting a specific environment, or adapting the user context according to the caller.

NTi simplifies this with the ChangeDataBase(string) method, which dynamically defines the current library for the active session.

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

     conn.ChangeDatabase(libraryName);

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

Note that ChangeDatabase() acts on the current session, that of the NTi job.

5️⃣ Call an RPG program to update a customer name

NTi also lets you call an RPG program directly from your .NET API, passing typed parameters (string, int, etc.) as you would from a green screen or batch. So you can encapsulate existing business processes in a REST API, without having to rewrite all the logic in C#.

Let's imagine an RPG program named PGMCUST01, located in the NORTHWIND library, which expects two parameters:

  • A customer ID (id) — 5-character alphanumeric
  • A new name (Name) — 30-character alphanumeric

Here's how to expose this program call via a REST endpoint:

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

It's a very practical way of gradually modernizing your IBM applications, without having to recode all your business rules. You expose your RPG programs as genuine reusable services, which can be integrated into a web portal or mobile app.

7️⃣ Swagger and automatic documentation

As soon as you've created your minimal project in Visual Studio, you'll have noticed that Swagger is activated by default. It automatically generates interactive documentation of our REST API, accessible directly from a browser.

This interface makes development easier: we can test our endpoints in real time, checking expected parameters, observing returned responses, or even visualizing the complete structure of the objects used, as with our Car or Customer models.

Swagger also plays a key role in client-side API consumption: the documentation generated enables external tools or front-end developers to automatically generate code to consume endpoints, without having to guess the structure of the calls.

Conclusion

We can see that with .NET 8, Dapper and NTi Data Provider, it's possible to build a complete, lightweight yet perfectly functional REST API in just a few minutes, capable of interacting natively with an IBM i environment.

With no overlay, no complex frameworks, every single feature — from executing SQL queries to calling an RPG program, via secure CL command execution — can be exposed in a very simple, stable and interoperable way.

Whether you're building a POC, creating in-house technical services or initiating a progressive modernization strategy, this minimalist API base is an excellent starting point. — clear, modular and fully aligned with microservice principles.

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=MY_IBMI;user=MY_USER;password=MY_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();
        }
    }
}
Back