Introduction
When it comes to running CL(CMD) commands on IBM i, most users still use the traditional green screens (5250). These interfaces, while effective for insiders, are often complex and unintuitive for users with little or no IBM i expertise. Solutions now exist to modernize, simplify and enrich the user experience.
With NTi and its Toolbox extension, available as a NuGet package, you can directly query the system API QCDRCMDD, provided by IBM, to automatically retrieve the full description of a CL command.
The RetrieveCommandDefinition()
method completely encapsulates this operation, performing it automatically:
- 1️⃣ API call.
- 2️⃣ Retrieving the XML (CDML) stream from the CL command.
- 3️⃣ The deserialization of this stream into a strongly typed C# object, directly usable in your .NET applications.
Depending on your needs, you can :
- Directly retrieve raw XML with the
RetrieveCommandDefinitionXml()
method for customized processing, notably via APIs or external analysis tools.
- Use the structured C# object returned by
RetrieveCommandDefinition()
, to automatically and dynamically generate modern user interfaces, especially with Blazor as illustrated in this tutorial.
So, from a single instruction
CommandDefinition commandDef = Conn.RetrieveCommandDefinition("CRTLIB", "QSYS", false);
You get a complete description that can be used immediately in your code, including
- An exhaustive list of parameters (required and optional)
- Precise type (text, numeric, qualified, list, etc.)
- Default, special and unique 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 way to new uses, such as the creation of interactive forms with automatic pre-filling, advanced automation of parameter validations, and the automatic generation of ready-to-use CL commands to simplify their execution and reuse.
💡 The code presented in this article is provided for information purposes only. It does not constitute a definitive or optimized solution for all contexts of use. It simply illustrates what can be achieved with the Toolbox Extensions NTi and Blazor methods.
The scenario: create a dynamic interface for any CL command
To illustrate these possibilities in concrete terms, in this tutorial we'll create a Blazor Web App (.NET 8) based on NTi and the Toolbox extension. The aim will be to build a dynamic, generic interface, enabling:
- Load the detailed structure of any CL order (standard or customized).
- Automatically generate suitable input fields (text, drop-down lists, checkboxes, etc.).
- Dynamically build complete CL syntax ready for use.
- Execute this validated command directly on the IBM i side.
All presented in a modern interface, with IBM's Carbon design, radically different from the traditional IBM i approach.
Step 1: Dynamically load an order structure
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 input form fields.
First of all, you need to establish a conventional connection to the IBM i via NTi:
- Install the NuGet
Aumerial.Data.Nti
package (NTi Data Provider). - Then add the NuGet
Aumerial.Toolbox
package to access CL methods. - Set up an
NTiConnection
pointing to your IBM i, and reference the 2 packages in your project.
Once this configuration has been made, the application can query the system to retrieve the full description of a CL command.
In concrete terms, as soon as the user enters the name of a command (e.g. CRTLIB
), the Blazor application interrogates the IBM i API via NTi and retrieves all the information required for the dynamic construction of the form.
1️⃣ Declaration of internal data structures
In the Blazor component code, we declare several variables that manage the state of the form and store the values entered by the user:
// Main object containing the complete command definition
private CommandDefinition? Command;
// The currently selected command
private Command? SelectedCommand;
// Storage of simple values associated with each keyword (Kwd) in the command parameters
private Dictionary<string, string> CommandValues = new();
// Management of qualified parameters (e.g. File(NAME/LIB))
private Dictionary<string, Dictionary<string, string>> QualifierValues = new();
// Allows the user to enter a custom value (override)
private Dictionary<string, Dictionary<string, string>> QualifierCustom = new();
// Managing the sub-elements of a complex ELEM parameter
private Dictionary<string, Dictionary<string, string>> ElementValues = new();
// Multiple selections via checkboxes
private Dictionary<string, Dictionary<string, bool>> CheckedValues = new();
These dictionaries enable each type of data associated with the various parameters to be managed independently and in isolation, ensuring clear management.
2️⃣ Dynamic data retrieval and initialization
When a user enters a command and validates, the RetrieveCommandDefinition()
method is called:
try
{
// Call the RetrieveCommandDefinition() method from NTiConnection via Toolbox
Command = await Task.Run(() => DBService.Conn.RetrieveCommandDefinition(CommandName.ToUpper(), "QSYS", true));
// Check that a valid command has been returned
if (Command != null && Command.CmdDefinition.Any())
{
// Selecting the first command returned
SelectedCommand = Command.CmdDefinition.FirstOrDefault();
if (SelectedCommand != null)
{
// Resetting dictionaries before use
CommandValues.Clear();
QualifierValues.Clear();
QualifierCustom.Clear();
CheckedValues.Clear();
ElementValues.Clear();
// Parameter browsing for individual initialization
foreach (var param in SelectedCommand.Parameters)
{
// Store default value or empty 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<string, bool>();
foreach (var spcVal in param.SpecialValues.SelectMany(s => s.SpecialValues))
{
CheckedValues[param.Kwd][spcVal.Value] = false; // no selection checked by default
}
}
// Initialization of qualified parameters (QUAL type, e.g. FILE(NAME/LIB))
QualifierValues[param.Kwd] = new Dictionary<string, string>();
QualifierCustom[param.Kwd] = new Dictionary<string, string>();
int qualIndex = 0;
foreach (var qual in param.Qualifiers)
{
var key = !string.IsNullOrEmpty(qual.Prompt) ? qual.Prompt : $"{qual.Type}_{qualIndex++}";
// If parameter type is QUAL, initialize values to empty by default
QualifierValues[param.Kwd][key] = param.Type == "QUAL" ? "" : (qual.Dft ?? "");
QualifierCustom[param.Kwd][key] = ""; // always initially empty for user input
}
// Sub-element initialization for complex parameters (ELEM type)
ElementValues[param.Kwd] = new Dictionary<string, string>();
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 of the command has been retrieved, we validate that it is correctly defined, and initialize the internal dictionaries to distinctly store each value linked to this command. We go through each parameter returned by the API to determine:
- Default values to be displayed in simple fields,
- Multiple options (checkboxes) and their initial state (unchecked by default),
- Any qualified values (name/library) to be presented to the user,
- Sub-elements of complex parameters to be displayed separately.
This approach ensures that all the necessary information, clearly structured and isolated in our dictionaries, is immediately available to dynamically build an interface adapted to each CL order, whatever its complexity or particularities, with the certainty that all fields are correctly pre-filled, that drop-down lists, checkboxes and conditional zones function perfectly, and that the order will be dynamically reconstructed without loss of information.
👉 For more details on the returned structure, see the documentation for the RetrieveCommandDefinition method.
Step 2: Dynamically generate input fields
Once the complete CL command structure has been loaded and our state dictionaries initialized (see step 1), we can dynamically generate the appropriate input fields for each parameter in our Blazor interface, faithfully reflecting the parameters returned by the IBM i via NTi.
In concrete terms, each parameter from SelectedCommand.Parameters
is browsed, and conditional logic is applied to dynamically display the right component according to the type and constraints defined on the IBM i side: simple type, special values, qualified parameters, nested elements, multiple selection, etc.
Metadata-driven logic
Each parameter is processed in an @foreach
loop, and depending on its properties (Type, SpecialValues, Elements, etc.), a specific rendering is generated. This logic is based on if/else
blocks that dynamically interpret the metadata provided by the API for each field.
@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" />
}
}
Rendering is not only dynamic in its structure, it also adapts to the expected behavior on the IBM i side, thanks to the metadata returned by the API via NTi.
- Maximum authorized length (
param.Len
) - Suggestion text (
param.Prompt
) - Default values (
param.Dft
) - Allowed values (
SpecialValues
) - Input groups (qualified, nested elements)
If a parameter of type CHAR
has a length of 10 characters with a default value, this will be automatically pre-filled and the length limited to 10 thanks to the maxlength
attribute. If the parameter expects a qualified value (e.g. file + library), several fields are displayed and linked to their sub-parts.
Each field is binded directly to the state dictionaries initialized earlier (CommandValues
, QualifierValues
, ElementValues
, etc.), allowing immediate capture of entered or selected values without further processing.
Dependency management
Some commands also incorporate conditional dependencies between parameters, known as Control Prompt
and exposed via SelectedCommand.Dependencies
. These rules can be used in the component to disable, pre-fill or make mandatory certain fields based on values entered in others. For example: if parameter TYPE
is *USR
, then OWNER
becomes mandatory.
This tutorial doesn't deal in detail with their management in the interface, but all the necessary elements are accessible via the
RetrieveCommandDefinition()
method, for easy integration within the Bazor component.
Step 3: Dynamically build the CL command to be executed
At this stage, all interface fields have been filled in, and their values stored in internal dictionaries (CommandValues
, QualifierValues
, ElementValues
, etc.).
The aim now is to dynamically reconstruct the complete syntax of the CL command, from the data collected, in the form of a string ready to be executed on the IBM i side.
This implementation logic is centralized in a dedicated method: BuildCommandCL()
:
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<string>();
// Embedded elements (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)})");
}
}
// Qualified parameters (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)
{
// Generate nothing if one of the elements is missing
}
else if (values.Any())
{
var combined = string.Join("/", values.AsEnumerable().Reverse());
segment.Add($"{param.Kwd.ToUpperInvariant()}({combined.ToUpperInvariant().Trim()})");
}
}
// Multiple selections (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 initialized by starting with the main name (DLTLIB
, CRTLIB
, etc.), followed by a sequence of parameters dynamically formatted according to their type. For each parameter, several cases are handled.
- If a single value is entered as
CommandValues
, it is added directly, with special treatment for theTEXT
parameter (surrounded by single quotes). - If no simple field is filled in, secondary structures are analyzed.
- For nested parameters of type
ELEM
, we assemble the sub-values by separating them with a space. - For
QUAL
qualified parameters, sub-values are combined (e.g. file/library), taking into account any manual entries (override). We avoid adding the parameter if a part is missing (especially for critical pairs such as FILE). - For multiple checkbox values, all selected options are concatenated with a comma, as in OPTION(*SRC, *MBR).
Each segment is converted to uppercase, cleaned and then concatenated to the main command. The end result is a complete, clean, compliant and executable CL command on the IBM i side - exactly as if it had been entered in a 5250 terminal, but here, dynamically generated from a modern interface.
Step 4: Execute the command on the IBM i side
Once the command has been built (see step 3), all that's left to do is run it on the IBM i. This is a two-step process: a preliminary syntax check, followed by the actual execution of the command.
Everything happens in a SubmitCommand()
method called when the Blazor form is submitted:
private async Task SubmitCommand()
{
messageSubmit = "";
string commandCL = BuildCommandCL();
try
{
DBService.Conn.CheckCLCommand(commandCL); // Checks syntax without executing
DBService.Conn.ExecuteClCommand(commandCL); // Execute command
messageSubmit = "Commande envoyée avec succès.";
await Task.Delay(4000); // Pause before reset
ResetForm(); // Empty form
ResetGenerateCommand(); // Clears the displayed command
}
catch (Exception ex)
{
messageSubmit = $"{ex.Message}";
}
}
1️⃣ Pre-check with CheckCLCommand()
.
Before sending, you must be able to validate the generated order. This is checked via the CheckCLCommand()
method, also available via NTi's Toolbox extension.
It interrogates the IBM i to analyze the complete syntactic validity of the command, without executing it: unknown keywords, badly formatted value, missing mandatory parameter. This advance control avoids hot errors, by reproducing exactly the same behavior as the system in a 5250 environment.
2️⃣ Execute with ExecuteClCommand()
.
Once validated, the command is transmitted via ExecuteClCommand()
, a method exposed by NTi DataProvider's NTiConnection
class.
It is executed in the session already open - with its complete environment: user, active job, current library, authorizations.
All feedback messages and system errors are rendered as if the command had been launched directly from an IBM i terminal.
What is controlled on the Blazor/.NET side
The entire process is managed within the .NET application. It orchestrates:
- Dynamic CL command generation with
BuildCommandCL()
, based on values entered in the interface. - Syntax validation with
CheckCLCommand()
, which simulates system behavior without actually running the command. - Actual execution with
ExecuteClCommand()
, in IBM i session context (job, user, current library). - Error and message handling via a try/catch block, to provide the user with feedback from the IBM i system.
The command runs successfully, and displays the success message
And the library is well created on our IBM i
Conclusion
With RetrieveCommandDefinition()
and RetrieveCommandDefinitionXml()
, offered by ToolboxExtension, you can dynamically generate input interfaces for any CL command, simply by relying 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, etc.) is immediately available for use in your code.
This abstraction considerably reduces the workload on the development side, allowing you to concentrate solely on UX and business logic, without ever having to manually code the structure of each command.
But above all, you're finally out of the confines of a 5250 terminal. Your interface becomes portable, accessible from a browser, a business application, a client workstation or a containerized platform. In other words, this change in approach opens the way to truly new uses:
- Simplified forms for non-specialized IBM i teams
- Commands centralized in an administration portal
- Integration into DevOps workflows or support tools
- Automation of system or technical tasks, without a green session
You don't leave IBM i - you extend it. To the web, to .NET, to tomorrow.
Toolbox methods are not intended to replace your IBM i skills, but to free you from technical complexity, and extend them into a new, more agile and accessible ecosystem. Saving time on the development side, and clearly increasing the value of your applications.
Quentin Destrade