Tutorials

Prompt a CL Command on IBM i / AS400 with Blazor and NTi

ByQuentin DESTRADE

Illustration for the article

Detailed content of the article:Prompt a CL Command on IBM i / AS400 with Blazor and NTi

Dynamically generate input interfaces for any IBM i CL command with Blazor and C#, using the RetrieveCommandDefinition() and RetrieveCommandDefinitionXml() methods built into the NTi Toolbox extension. A modern, generic approach as an alternative to 5250 screens.

When it comes to running CL commands (CMD) on IBM i, most users still rely on traditional green screen interfaces (5250). While effective for seasoned operators, these interfaces are often complex and unintuitive for users with little or no IBM i experience. Solutions now exist to modernize, simplify and enhance the user experience.

With NTi and its Toolbox extension, available as a NuGet package, it is possible to query the IBM-provided QCDRCMDD system API directly, to automatically retrieve the full definition of any CL command.

The RetrieveCommandDefinition() method fully encapsulates this operation, automatically handling:

  • The API call
  • Retrieval of the command's XML stream (CDML)
  • Deserialization of that stream into a strongly typed C# object, ready to use in .NET applications.

Depending on the use case, two approaches are available:

  • Retrieve the raw XML directly with RetrieveCommandDefinitionXml() for custom processing, such as via external APIs or analysis tools.

XML stream of the CRTLIB CL command returned by QCDRCMDD

  • Use the structured C# object returned by RetrieveCommandDefinition() to automatically and dynamically generate modern user interfaces, as illustrated in this tutorial with Blazor.

CmdDefinition object deserialized in C#

From a single instruction:

CommandDefinition commandDef = conn.RetrieveCommandDefinition("CRTLIB", "QSYS", false);

A complete, immediately usable description is returned, including:

  • The full list of parameters (mandatory and optional)
  • Their exact types (text, numeric, qualified, list, and so on)
  • Default, special and single values
  • All conditional dependencies (Control Prompt) between parameters
  • Detailed sub-elements of each complex parameter

This ability to dynamically generate the structure of CL commands opens the door to new use cases: creating interactive forms with automatic pre-filling, advanced parameter validation automation, and automatic generation of ready-to-use CL commands.

💡The code presented in this article is provided for illustrative purposes only. It does not represent a definitive or optimized solution for all use cases. It simply demonstrates what can be achieved with NTi Toolbox methods and Blazor.

The scenario: build a dynamic interface for any CL command

To illustrate these possibilities in a concrete way, this tutorial uses a Blazor Web App (.NET 8) with NTi Data Provider and the Toolbox extension. The goal is to build a dynamic, generic interface that allows:

  • Loading the detailed structure of any CL command (standard or custom)
  • Automatically generating the appropriate input fields (text, dropdown, checkboxes, and so on)
  • Dynamically building the complete, ready-to-use CL syntax
  • Executing the validated command directly on IBM i

All presented in a modern interface using IBM's Carbon design system.

Blazor form generated for the CRTLIB CL command

Step 1 - Dynamically load the structure of a command

The first step is to dynamically load the complete structure of a CL command entered by the user. This structure is then used to automatically generate the form input fields.

Prerequisites:

  • Install the Aumerial.Data.Nti NuGet package
  • Install the Aumerial.Toolbox NuGet package
  • Instantiate an NTiConnection pointing to IBM i

As soon as the user enters a command name (for example CRTLIB), the Blazor application queries the IBM i API via NTi and retrieves all the information needed to dynamically build the form.

Internal data structure declarations

In the Blazor component code, several variables manage the form state and store entered values:

// Main object holding the complete command definition
private CommandDefinition? Command;

// The currently selected command
private Command? SelectedCommand;

// Storage for simple values associated with each keyword (Kwd)
private Dictionary<string, string> CommandValues = new();

// Handling of qualified parameters (e.g. File(NAME/LIB))
private Dictionary<string, Dictionary<string, string>> QualifierValues = new();

// Allows entering a custom value (override)
private Dictionary<string, Dictionary<string, string>> QualifierCustom = new();

// Handling of sub-elements for complex ELEM type parameters
private Dictionary<string, Dictionary<string, string>> ElementValues = new();

// Handling of multiple selections via checkboxes
private Dictionary<string, Dictionary<string, bool>> CheckedValues = new();

These dictionaries independently manage each type of data associated with the different parameters.

Dynamic data retrieval and initialization

When a user enters a command and confirms, the RetrieveCommandDefinition() method is called:

try
{
    // Call RetrieveCommandDefinition() from NTiConnection via Toolbox
    Command = await Task.Run(() => DBService.Conn.RetrieveCommandDefinition(CommandName.ToUpper(), "QSYS", true));
    // Check that a valid command was returned
    if (Command != null && Command.CmdDefinition.Any())
    {
        // Select the first returned command
        SelectedCommand = Command.CmdDefinition.FirstOrDefault();
        if (SelectedCommand != null)
        {
            / Reset dictionaries before use
            CommandValues.Clear();
            QualifierValues.Clear();
            QualifierCustom.Clear();
            CheckedValues.Clear();
            ElementValues.Clear();
            // Iterate through parameters for individual initialization
            foreach (var param in SelectedCommand.Parameters)
            {
                // Store the default value, or empty string if not defined
                CommandValues[param.Kwd] = param.Dft ?? string.Empty;
                // Initialize checkboxes for multi-select parameters
                if (param.SpecialValues.Any() && int.TryParse(param.Max, out int max) && max > 1)
                {
                    CheckedValues[param.Kwd] = new Dictionary();
                    foreach (var spcVal in param.SpecialValues.SelectMany(s => s.SpecialValues))
                    {
                        CheckedValues[param.Kwd][spcVal.Value] = false;  // aucune sélection cochée par défaut
                    }
                }
                // Initialize qualified parameters (QUAL type, e.g. FILE(NAME/LIB))
                QualifierValues[param.Kwd] = new Dictionary();
                QualifierCustom[param.Kwd] = new Dictionary();

                int qualIndex = 0;
                foreach (var qual in param.Qualifiers)
                {
                    var key = !string.IsNullOrEmpty(qual.Prompt) ? qual.Prompt : $"{qual.Type}_{qualIndex++}";
                    
                    // For QUAL type parameters, initialize values to empty by default
                    QualifierValues[param.Kwd][key] = param.Type == "QUAL" ? "" : (qual.Dft ?? "");
                    QualifierCustom[param.Kwd][key] = ""; // toujours vide initialement pour une éventuelle saisie utilisateur
                }

                // Initialize sub-elements for complex parameters (ELEM type)
                ElementValues[param.Kwd] = new Dictionary();
                foreach (var elem in param.Elements)
                {
                    ElementValues[param.Kwd][elem.Prompt] = elem.Dft ?? string.Empty;
                }
            }
        }
    }
}
catch (Exception ex)
{
    errorMessage = ex.Message;
}

Once the complete structure is retrieved, each parameter returned by the API is iterated to determine:

  • The default values to display in simple fields
  • Multiple options (checkboxes) and their initial state (unchecked by default)
  • Any qualified values (name/library) to present
  • Sub-elements of complex parameters to display separately

Dynamic Blazor interface with dependent fields

This approach ensures that all necessary information is immediately available, structured and isolated in the dictionaries, to dynamically build an interface suited to any CL command regardless of its complexity.

👉 For more details on the returned structure, see the RetrieveCommandDefinition method documentation.

Step 2 - Dynamically generate input fields

Once the CL command structure is loaded and the state dictionaries are initialized, input fields suited to each parameter can be dynamically generated in the Blazor interface.

Each parameter from SelectedCommand.Parameters is iterated and conditional logic is applied to display the right component based on the type and constraints defined on the IBM i side: simple type, special values, qualified parameter, nested elements, multiple selection, and so on.

Metadata-driven logic

Each parameter is processed in a @foreach loop, and based on its properties (Type, SpecialValues, Elements, and so on), a specific rendering is generated:

@foreach (var param in SelectedCommand.Parameters)
{
    if (param.Type == "QUAL")
    {
        foreach (var key in QualifierValues[param.Kwd].Keys)
        {
            <input class="bx--text-input" @bind="QualifierValues[param.Kwd][key]" placeholder="@key" />
        }
    }
    else if (param.SpecialValues.Any())
    {
        <select class="bx--select-input" @bind="CommandValues[param.Kwd]">
            <option value="">Choisir</option>
            @foreach (var val in param.SpecialValues.SelectMany(s => s.SpecialValues))
            {
                <option value="@val.Value">@val.Value</option>
            }
        </select>
    }
    else if (param.Elements.Any())
    {
        <fieldset>
            @foreach (var elem in param.Elements)
            {
                <input class="bx--text-input" @bind="ElementValues[param.Kwd][elem.Prompt]" placeholder="@elem.Prompt" />
            }
        </fieldset>
    }
    else if (param.Type == "CHAR")
    {
        <input class="bx--text-input" @bind="CommandValues[param.Kwd]" placeholder="@param.Prompt" maxlength="@param.Len" />
    }
}

Dynamic generation of CL input fields

The rendering adapts to the expected IBM i behavior using the metadata returned by the API via NTi:

  • Maximum allowed length - param.Len
  • Hint text - param.Prompt
  • Default values - param.Dft
  • Allowed values - SpecialValues
  • Input groups (qualified, nested elements)

Each field is directly bound to the initialized state dictionaries (CommandValues, QualifierValues, ElementValues, and so on), allowing entered or selected values to be captured immediately.

Handling dependencies

Some commands include conditional dependencies between parameters, known as Control Prompt and exposed via SelectedCommand.Dependencies.

These rules can be used to disable, pre-fill or make certain fields mandatory based on values entered in other fields. For example: if the TYPE parameter is set to *USR, then OWNER becomes required.

💡This tutorial does not cover their handling in the interface in detail, but all the necessary elements are accessible via RetrieveCommandDefinition() to integrate them easily into the Blazor component.

Step 3 - Dynamically build the CL command

At this point, all fields have been filled in and their values are stored in the internal dictionaries. The goal is to dynamically reconstruct the complete CL command syntax as a string ready to be executed on IBM i.

This logic is centralized in the BuildCommandCL() method:

string BuildCommandCL()
{
    var commandCl = SelectedCommand.CommandName.ToUpperInvariant().Trim() + " ";
    foreach (var param in SelectedCommand.Parameters)
    {
        if (!string.IsNullOrWhiteSpace(CommandValues[param.Kwd]))
        {
            string value = CommandValues[param.Kwd].ToUpperInvariant().Trim();
            if (param.Kwd.Equals("TEXT", StringComparison.OrdinalIgnoreCase))
            {
                value = $"'{value}'"; // Le champ TEXT est entre quotes
            }
            commandCl += $"{param.Kwd.ToUpperInvariant()}({value}) ";
        }
        else
        {
            var segment = new List();

            // Éléments imbriqués (ELEM)
            if (ElementValues.TryGetValue(param.Kwd, out var elements))
            {
                var values = elements.Values.Where(v => !string.IsNullOrWhiteSpace(v))
                                            .Select(v => v.ToUpperInvariant().Trim()).ToList();
                if (values.Any())
                {
                    segment.Add($"{param.Kwd.ToUpperInvariant()}({string.Join(" ", values)})");
                }
            }

            // Paramètres qualifiés (QUAL)
            if (QualifierValues.TryGetValue(param.Kwd, out var qualifiers) &&
                QualifierCustom.TryGetValue(param.Kwd, out var customQualifiers))
            {
                var values = qualifiers.Keys.Select(key => !string.IsNullOrWhiteSpace(customQualifiers[key])
                                                           ? customQualifiers[key]
                                                           : qualifiers[key])
                                             .Where(v => !string.IsNullOrWhiteSpace(v)).ToList();

                if ((param.Kwd == "FILE" || param.Kwd == "SRCFILE") && values.Count < 2)
                {
                    // Ne rien générer si l’un des éléments est manquant
                }
                else if (values.Any())
                {
                    var combined = string.Join("/", values.AsEnumerable().Reverse());
                    segment.Add($"{param.Kwd.ToUpperInvariant()}({combined.ToUpperInvariant().Trim()})");
                }
            }

            // Sélections multiples (checkbox)
            if (CheckedValues.TryGetValue(param.Kwd, out var checkDict))
            {
                var checkedValues = checkDict.Where(x => x.Value).Select(x => x.Key.ToUpperInvariant().Trim());
                if (checkedValues.Any())
                {
                    segment.Add($"{param.Kwd.ToUpperInvariant()}({string.Join(",", checkedValues)})");
                }
            }

            if (segment.Any())
            {
                commandCl += string.Join(" ", segment) + " ";
            }
        }
    }
    return commandCl.Trim();
}

The final command is built from the main name (DLTLIB, CRTLIB, and so on), followed by parameters dynamically formatted according to their type:

  • If a simple value is entered via CommandValues, it is added directly, with special handling for the TEXT parameter (wrapped in single quotes)
  • For nested ELEM type parameters, sub-values are assembled separated by a space
  • For qualified QUAL parameters, sub-values are combined (e.g. file/library), taking into account any manual input
  • For multiple values (checkboxes), all selected options are concatenated with a comma, as in OPTION(*SRC,*MBR)

The result is a complete, clean CL command ready to execute on IBM i, exactly as if it had been entered in a 5250 terminal, but dynamically generated from a modern interface.

CL command dynamically built in Blazor

Step 4 - Execute the command on IBM i

Once the command is built, it is validated then executed in two steps inside the SubmitCommand() method:

private async Task SubmitCommand()
{
    messageSubmit = "";
    string commandCL = BuildCommandCL();
    try
    {
        DBService.Conn.CheckCLCommand(commandCL);
        DBService.Conn.ExecuteClCommand(commandCL); 
        messageSubmit = "Commande envoyée avec succès.";

        await Task.Delay(4000); 
        ResetForm();          
        ResetGenerateCommand(); 
    }
    catch (Exception ex)
    {
        messageSubmit = $"{ex.Message}"; 
    }
}

Prior validation with CheckCLCommand()

Before any submission, the command is validated via CheckCLCommand(), available through the NTi Toolbox extension. It queries IBM i to fully analyze the syntactic validity of the command without executing it: unknown keywords, malformed values, missing mandatory parameters. This upfront check reproduces exactly the behavior of the system in a 5250 environment.

Syntax validation with CheckCLCommand

Execution with ExecuteClCommand()

Once validated, the command is sent via ExecuteClCommand(), a method exposed by the NTiConnection class. It runs within the already open session, with its full environment: user, active job, current library, permissions. All return messages or system errors are surfaced exactly as if the command had been run directly from an IBM i terminal.

CL execution with ExecuteClCommand

What is controlled on the Blazor/.NET side

The entire process is handled within the .NET application, which orchestrates command generation, syntax validation, execution and error handling through a try/catch block.

Success message after CL submission

MYLIB library created on IBM i

Conclusion

With RetrieveCommandDefinition() and RetrieveCommandDefinitionXml(), provided by NTi Toolbox, it is possible to dynamically generate input interfaces for any CL command, relying solely on the metadata returned by the QCDRCMDD API.

RetrieveCommandDefinition() fully encapsulates this process on the .NET side: it queries the IBM i API, retrieves the XML stream in CDML format, decodes it and deserializes it into a strongly typed C# object. No manual parsing or XML manipulation is required. The entire command structure (parameters, types, values, dependencies) is immediately available and ready to use in code.

This abstraction significantly reduces development overhead, allowing full focus on UX and business logic, without ever manually coding the structure of each command.

But above all, it breaks free from the constraints of a 5250 terminal. The interface becomes portable, accessible from a browser, a business application, a client workstation or a containerized platform. In other words, this shift in approach opens the door to genuinely new use cases:

  • Simplified forms for teams with no IBM i expertise
  • Centralized commands in an administration portal
  • Integration into DevOps workflows or support tools
  • Automation of system or technical tasks, without a green screen session

You don't leave IBM i, you extend it. To the web, to .NET, to tomorrow.

Toolbox methods are not meant to replace IBM i skills, they remove technical complexity and carry them forward into a new ecosystem, more agile and accessible.


Quentin Destrade

Ready to get started?

Get your free trial license online
and connect your .NET apps to your IBM i right away.

Create your account

Log in to the Aumerial portal, generate your trial license and activate NTi on your IBM i instantly.

Start your trial

Add NTi to your project

Install NTi Data Provider from NuGet in Visual Studio and reference it in your .NET project.

View documentation

Need help?

If you have questions about our tools or licensing options, our team is here to help.

Contact us
30-day free trial instant activation no commitment nothing to install on the IBM i side