bc-code-intelligence-mcp
Version:
BC Code Intelligence MCP Server - Complete Specialist Bundle with AI-driven expert consultation, seamless handoffs, and context-preserving workflows
677 lines (556 loc) • 20.1 kB
Markdown
# Command Queue Pattern in AL - Code Examples
This sample demonstrates implementing command queue patterns for asynchronous processing in Business Central.
## Basic Command Interface
```al
// Core command interface
interface ICommand
{
procedure Execute(): Boolean;
procedure GetCommandName(): Text[50];
procedure GetDescription(): Text[250];
procedure CanUndo(): Boolean;
procedure Undo(): Boolean;
procedure GetExecutionTime(): DateTime;
procedure GetCommandData(): Dictionary of [Text, Variant];
}
```
## Command Queue Implementation
```al
// Main command queue system
codeunit 50700 "Command Queue Manager"
{
var
CommandQueue: List of [Interface ICommand];
ExecutedCommands: List of [Interface ICommand];
IsProcessing: Boolean;
ProcessingStats: Dictionary of [Text, Variant];
procedure EnqueueCommand(Command: Interface ICommand)
begin
CommandQueue.Add(Command);
LogCommandEnqueued(Command);
// Auto-process if not already processing
if not IsProcessing then
ProcessNextCommand();
end;
procedure EnqueuePriorityCommand(Command: Interface ICommand)
begin
CommandQueue.Insert(1, Command);
LogCommandEnqueued(Command);
if not IsProcessing then
ProcessNextCommand();
end;
procedure ProcessAllCommands(): Integer
var
ProcessedCount: Integer;
begin
ProcessedCount := 0;
while CommandQueue.Count > 0 do begin
if ProcessNextCommand() then
ProcessedCount += 1
else
break; // Stop processing on error
end;
exit(ProcessedCount);
end;
procedure ProcessNextCommand(): Boolean
var
Command: Interface ICommand;
ExecutionResult: Boolean;
StartTime: DateTime;
EndTime: DateTime;
begin
if CommandQueue.Count = 0 then
exit(false);
if IsProcessing then
exit(false); // Prevent concurrent processing
IsProcessing := true;
Command := CommandQueue.Get(1);
CommandQueue.RemoveAt(1);
try
StartTime := CurrentDateTime;
ExecutionResult := ExecuteCommandSafely(Command);
EndTime := CurrentDateTime;
if ExecutionResult then begin
ExecutedCommands.Add(Command);
LogCommandExecuted(Command, StartTime, EndTime, true);
UpdateProcessingStats(Command, true, EndTime - StartTime);
end else begin
LogCommandExecuted(Command, StartTime, EndTime, false);
HandleCommandFailure(Command);
UpdateProcessingStats(Command, false, EndTime - StartTime);
end;
finally
IsProcessing := false;
end;
exit(ExecutionResult);
end;
procedure UndoLastCommand(): Boolean
var
LastCommand: Interface ICommand;
begin
if ExecutedCommands.Count = 0 then
exit(false);
LastCommand := ExecutedCommands.Get(ExecutedCommands.Count);
if not LastCommand.CanUndo() then
exit(false);
if LastCommand.Undo() then begin
ExecutedCommands.RemoveAt(ExecutedCommands.Count);
LogCommandUndone(LastCommand);
exit(true);
end;
exit(false);
end;
procedure GetQueueStatus(): Dictionary of [Text, Variant]
var
Status: Dictionary of [Text, Variant];
begin
Status.Set('QueueLength', CommandQueue.Count);
Status.Set('ExecutedCount', ExecutedCommands.Count);
Status.Set('IsProcessing', IsProcessing);
Status.Set('ProcessingStats', ProcessingStats);
exit(Status);
end;
procedure ClearQueue()
begin
CommandQueue.Clear();
LogQueueCleared();
end;
local procedure ExecuteCommandSafely(Command: Interface ICommand): Boolean
begin
try
exit(Command.Execute());
except
LogCommandError(Command, GetLastErrorText);
exit(false);
end;
end;
local procedure HandleCommandFailure(Command: Interface ICommand)
begin
// Add to dead letter queue or retry logic could go here
LogCommandFailure(Command);
end;
local procedure UpdateProcessingStats(Command: Interface ICommand; Success: Boolean; Duration: BigInteger)
var
TotalCommands: Integer;
SuccessfulCommands: Integer;
FailedCommands: Integer;
TotalDuration: BigInteger;
AverageDuration: BigInteger;
begin
if ProcessingStats.Get('TotalCommands', TotalCommands) then
TotalCommands += 1
else
TotalCommands := 1;
if Success then begin
if ProcessingStats.Get('SuccessfulCommands', SuccessfulCommands) then
SuccessfulCommands += 1
else
SuccessfulCommands := 1;
end else begin
if ProcessingStats.Get('FailedCommands', FailedCommands) then
FailedCommands += 1
else
FailedCommands := 1;
end;
if ProcessingStats.Get('TotalDuration', TotalDuration) then
TotalDuration += Duration
else
TotalDuration := Duration;
AverageDuration := TotalDuration div TotalCommands;
ProcessingStats.Set('TotalCommands', TotalCommands);
ProcessingStats.Set('SuccessfulCommands', SuccessfulCommands);
ProcessingStats.Set('FailedCommands', FailedCommands);
ProcessingStats.Set('TotalDuration', TotalDuration);
ProcessingStats.Set('AverageDuration', AverageDuration);
end;
// Logging procedures
local procedure LogCommandEnqueued(Command: Interface ICommand)
begin
// Log command enqueue event
end;
local procedure LogCommandExecuted(Command: Interface ICommand; StartTime: DateTime; EndTime: DateTime; Success: Boolean)
begin
// Log command execution details
end;
local procedure LogCommandUndone(Command: Interface ICommand)
begin
// Log command undo event
end;
local procedure LogCommandError(Command: Interface ICommand; ErrorText: Text)
begin
// Log command execution error
end;
local procedure LogCommandFailure(Command: Interface ICommand)
begin
// Log command failure for analysis
end;
local procedure LogQueueCleared()
begin
// Log queue clear event
end;
}
```
## Sales Document Creation Command
```al
// Example command: Create sales document
codeunit 50701 "Create Sales Document Cmd" implements ICommand
{
var
CustomerNo: Code[20];
Items: List of [Dictionary of [Text, Variant]];
OrderDate: Date;
CommandData: Dictionary of [Text, Variant];
CreatedDocumentNo: Code[20];
ExecutionTime: DateTime;
procedure Initialize(NewCustomerNo: Code[20]; OrderItems: List of [Dictionary of [Text, Variant]]; NewOrderDate: Date)
begin
CustomerNo := NewCustomerNo;
Items := OrderItems;
OrderDate := NewOrderDate;
// Store command data for logging/undo
CommandData.Set('CustomerNo', CustomerNo);
CommandData.Set('OrderDate', OrderDate);
CommandData.Set('ItemCount', Items.Count);
end;
procedure Execute(): Boolean
var
SalesHeader: Record "Sales Header";
SalesLine: Record "Sales Line";
ItemData: Dictionary of [Text, Variant];
LineNo: Integer;
begin
ExecutionTime := CurrentDateTime;
if CustomerNo = '' then
exit(false);
if not CustomerExists(CustomerNo) then
exit(false);
try
// Create sales header
SalesHeader.Init();
SalesHeader."Document Type" := SalesHeader."Document Type"::Order;
SalesHeader."No." := '';
SalesHeader.Insert(true);
CreatedDocumentNo := SalesHeader."No.";
SalesHeader.Validate("Sell-to Customer No.", CustomerNo);
if OrderDate <> 0D then
SalesHeader.Validate("Order Date", OrderDate);
SalesHeader.Modify(true);
// Add items
LineNo := 10000;
foreach ItemData in Items do begin
if not CreateSalesLine(SalesHeader, ItemData, LineNo) then
exit(false);
LineNo += 10000;
end;
// Update command data with result
CommandData.Set('CreatedDocumentNo', CreatedDocumentNo);
exit(true);
except
exit(false);
end;
end;
procedure GetCommandName(): Text[50]
begin
exit('CREATE_SALES_DOCUMENT');
end;
procedure GetDescription(): Text[250]
begin
exit(StrSubstNo('Create sales document for customer %1 with %2 items', CustomerNo, Items.Count));
end;
procedure CanUndo(): Boolean
begin
exit(CreatedDocumentNo <> '');
end;
procedure Undo(): Boolean
var
SalesHeader: Record "Sales Header";
begin
if CreatedDocumentNo = '' then
exit(false);
if SalesHeader.Get(SalesHeader."Document Type"::Order, CreatedDocumentNo) then begin
// Only allow undo if not posted
if SalesHeader.Status = SalesHeader.Status::Open then begin
SalesHeader.Delete(true);
exit(true);
end;
end;
exit(false);
end;
procedure GetExecutionTime(): DateTime
begin
exit(ExecutionTime);
end;
procedure GetCommandData(): Dictionary of [Text, Variant]
begin
exit(CommandData);
end;
local procedure CustomerExists(CustomerNo: Code[20]): Boolean
var
Customer: Record Customer;
begin
exit(Customer.Get(CustomerNo));
end;
local procedure CreateSalesLine(SalesHeader: Record "Sales Header"; ItemData: Dictionary of [Text, Variant]; LineNo: Integer): Boolean
var
SalesLine: Record "Sales Line";
ItemNo: Code[20];
Quantity: Decimal;
UnitPrice: Decimal;
begin
try
SalesLine.Init();
SalesLine."Document Type" := SalesHeader."Document Type";
SalesLine."Document No." := SalesHeader."No.";
SalesLine."Line No." := LineNo;
SalesLine.Insert(true);
if ItemData.Get('ItemNo', ItemNo) then
SalesLine.Validate("No.", ItemNo);
if ItemData.Get('Quantity', Quantity) then
SalesLine.Validate(Quantity, Quantity);
if ItemData.Get('UnitPrice', UnitPrice) then
SalesLine.Validate("Unit Price", UnitPrice);
SalesLine.Modify(true);
exit(true);
except
exit(false);
end;
end;
}
```
## Batch Processing Command
```al
// Command for batch operations
codeunit 50702 "Batch Update Prices Cmd" implements ICommand
{
var
ItemCategoryCode: Code[20];
PriceAdjustmentPercent: Decimal;
EffectiveDate: Date;
ProcessedItems: List of [Code[20]];
OriginalPrices: Dictionary of [Code[20], Decimal];
ExecutionTime: DateTime;
procedure Initialize(CategoryCode: Code[20]; AdjustmentPercent: Decimal; NewEffectiveDate: Date)
begin
ItemCategoryCode := CategoryCode;
PriceAdjustmentPercent := AdjustmentPercent;
EffectiveDate := NewEffectiveDate;
Clear(ProcessedItems);
Clear(OriginalPrices);
end;
procedure Execute(): Boolean
var
Item: Record Item;
NewPrice: Decimal;
ProcessedCount: Integer;
begin
ExecutionTime := CurrentDateTime;
if ItemCategoryCode = '' then
exit(false);
if PriceAdjustmentPercent = 0 then
exit(false);
try
Item.SetRange("Item Category Code", ItemCategoryCode);
Item.SetRange(Blocked, false);
if Item.FindSet() then
repeat
// Store original price for undo capability
OriginalPrices.Set(Item."No.", Item."Unit Price");
// Calculate new price
NewPrice := Item."Unit Price" * (1 + PriceAdjustmentPercent / 100);
NewPrice := Round(NewPrice, 0.01);
// Update price
Item.Validate("Unit Price", NewPrice);
Item.Modify(true);
ProcessedItems.Add(Item."No.");
ProcessedCount += 1;
// Commit periodically for large batches
if ProcessedCount mod 100 = 0 then
Commit();
until Item.Next() = 0;
exit(true);
except
exit(false);
end;
end;
procedure GetCommandName(): Text[50]
begin
exit('BATCH_UPDATE_PRICES');
end;
procedure GetDescription(): Text[250]
begin
exit(StrSubstNo('Update prices for category %1 by %2%', ItemCategoryCode, PriceAdjustmentPercent));
end;
procedure CanUndo(): Boolean
begin
exit(ProcessedItems.Count > 0);
end;
procedure Undo(): Boolean
var
Item: Record Item;
ItemNo: Code[20];
OriginalPrice: Decimal;
UndoCount: Integer;
begin
try
foreach ItemNo in ProcessedItems do begin
if Item.Get(ItemNo) and OriginalPrices.Get(ItemNo, OriginalPrice) then begin
Item.Validate("Unit Price", OriginalPrice);
Item.Modify(true);
UndoCount += 1;
if UndoCount mod 100 = 0 then
Commit();
end;
end;
exit(true);
except
exit(false);
end;
end;
procedure GetExecutionTime(): DateTime
begin
exit(ExecutionTime);
end;
procedure GetCommandData(): Dictionary of [Text, Variant]
var
CommandData: Dictionary of [Text, Variant];
begin
CommandData.Set('ItemCategory', ItemCategoryCode);
CommandData.Set('AdjustmentPercent', PriceAdjustmentPercent);
CommandData.Set('EffectiveDate', EffectiveDate);
CommandData.Set('ProcessedCount', ProcessedItems.Count);
exit(CommandData);
end;
}
```
## Priority Queue Implementation
```al
// Priority queue for command processing
codeunit 50703 "Priority Command Queue"
{
var
HighPriorityQueue: List of [Interface ICommand];
NormalPriorityQueue: List of [Interface ICommand];
LowPriorityQueue: List of [Interface ICommand];
procedure EnqueueCommand(Command: Interface ICommand; Priority: Option High,Normal,Low)
begin
case Priority of
Priority::High:
HighPriorityQueue.Add(Command);
Priority::Normal:
NormalPriorityQueue.Add(Command);
Priority::Low:
LowPriorityQueue.Add(Command);
end;
LogCommandEnqueued(Command, Priority);
end;
procedure DequeueNextCommand(): Interface ICommand
var
EmptyCommand: Interface ICommand;
begin
// Process high priority first
if HighPriorityQueue.Count > 0 then begin
EmptyCommand := HighPriorityQueue.Get(1);
HighPriorityQueue.RemoveAt(1);
exit(EmptyCommand);
end;
// Then normal priority
if NormalPriorityQueue.Count > 0 then begin
EmptyCommand := NormalPriorityQueue.Get(1);
NormalPriorityQueue.RemoveAt(1);
exit(EmptyCommand);
end;
// Finally low priority
if LowPriorityQueue.Count > 0 then begin
EmptyCommand := LowPriorityQueue.Get(1);
LowPriorityQueue.RemoveAt(1);
exit(EmptyCommand);
end;
exit(EmptyCommand);
end;
procedure HasCommands(): Boolean
begin
exit((HighPriorityQueue.Count > 0) or (NormalPriorityQueue.Count > 0) or (LowPriorityQueue.Count > 0));
end;
procedure GetQueueCounts(): Dictionary of [Text, Integer]
var
Counts: Dictionary of [Text, Integer];
begin
Counts.Set('High', HighPriorityQueue.Count);
Counts.Set('Normal', NormalPriorityQueue.Count);
Counts.Set('Low', LowPriorityQueue.Count);
exit(Counts);
end;
procedure ProcessAllCommandsByPriority(): Integer
var
Command: Interface ICommand;
ProcessedCount: Integer;
begin
ProcessedCount := 0;
while HasCommands() do begin
Command := DequeueNextCommand();
if ExecuteCommand(Command) then
ProcessedCount += 1;
end;
exit(ProcessedCount);
end;
local procedure ExecuteCommand(Command: Interface ICommand): Boolean
begin
try
exit(Command.Execute());
except
LogCommandError(Command, GetLastErrorText);
exit(false);
end;
end;
local procedure LogCommandEnqueued(Command: Interface ICommand; Priority: Option)
begin
// Log command enqueue with priority
end;
local procedure LogCommandError(Command: Interface ICommand; ErrorText: Text)
begin
// Log command execution error
end;
}
```
## Usage Example
```al
// Example of using the command queue system
codeunit 50704 "Command Queue Usage Example"
{
procedure DemonstrateCommandQueue()
var
CommandQueue: Codeunit "Command Queue Manager";
SalesDocCommand: Codeunit "Create Sales Document Cmd";
PriceUpdateCommand: Codeunit "Batch Update Prices Cmd";
Items: List of [Dictionary of [Text, Variant]];
ItemData: Dictionary of [Text, Variant];
QueueStatus: Dictionary of [Text, Variant];
ProcessedCount: Integer;
begin
// Create sales document command
ItemData.Set('ItemNo', 'ITEM001');
ItemData.Set('Quantity', 5);
ItemData.Set('UnitPrice', 25.00);
Items.Add(ItemData);
Clear(ItemData);
ItemData.Set('ItemNo', 'ITEM002');
ItemData.Set('Quantity', 10);
Items.Add(ItemData);
SalesDocCommand.Initialize('CUST001', Items, Today);
// Create price update command
PriceUpdateCommand.Initialize('ELECTRONICS', 5.0, Today);
// Enqueue commands
CommandQueue.EnqueueCommand(SalesDocCommand);
CommandQueue.EnqueueCommand(PriceUpdateCommand);
// Process all commands
ProcessedCount := CommandQueue.ProcessAllCommands();
// Check status
QueueStatus := CommandQueue.GetQueueStatus();
Message('Processed %1 commands. Queue status: %2', ProcessedCount, Format(QueueStatus));
// Demonstrate undo
if CommandQueue.UndoLastCommand() then
Message('Last command undone successfully');
end;
}
```
This comprehensive implementation provides a flexible command queue system with support for priority processing, undo operations, batch processing, and comprehensive logging for reliable asynchronous processing in Business Central.