bc-code-intelligence-mcp
Version:
BC Code Intelligence MCP Server - Complete Specialist Bundle with AI-driven expert consultation, seamless handoffs, and context-preserving workflows
665 lines (603 loc) • 20.8 kB
Markdown
# API Page Implementation - AL Code Examples
## Basic API Page Structure
```al
page 50100 "Customer API"
{
APIPublisher = 'contoso';
APIGroup = 'business';
APIVersion = 'v1.0';
EntityName = 'customer';
EntitySetName = 'customers';
PageType = API;
Caption = 'Customer API';
SourceTable = Customer;
DelayedInsert = true;
ODataKeyFields = SystemId;
layout
{
area(Content)
{
repeater(GroupName)
{
field(id; Rec.SystemId)
{
Caption = 'Id';
Editable = false;
}
field(number; Rec."No.")
{
Caption = 'Number';
}
field(displayName; Rec.Name)
{
Caption = 'Display Name';
}
field(type; Rec."Customer Posting Group")
{
Caption = 'Type';
}
field(blocked; Rec.Blocked)
{
Caption = 'Blocked';
}
field(lastModifiedDateTime; Rec.SystemModifiedAt)
{
Caption = 'Last Modified Date Time';
Editable = false;
}
}
}
}
}
```
## Business Entity Relationships
```al
page 50101 "Sales Order API"
{
APIPublisher = 'contoso';
APIGroup = 'sales';
APIVersion = 'v2.0';
EntityName = 'salesOrder';
EntitySetName = 'salesOrders';
PageType = API;
Caption = 'Sales Order API';
SourceTable = "Sales Header";
DelayedInsert = true;
ODataKeyFields = SystemId;
layout
{
area(Content)
{
repeater(GroupName)
{
field(id; Rec.SystemId)
{
Caption = 'Id';
Editable = false;
}
field(number; Rec."No.")
{
Caption = 'Number';
}
field(orderDate; Rec."Order Date")
{
Caption = 'Order Date';
}
// Customer relationship - expose related entity
field(customerId; Rec."Sell-to Customer No.")
{
Caption = 'Customer Id';
}
field(customerName; Rec."Sell-to Customer Name")
{
Caption = 'Customer Name';
Editable = false;
}
// Currency handling with proper OData exposure
field(currencyCode; CurrencyCodeText)
{
Caption = 'Currency Code';
trigger OnValidate()
begin
if CurrencyCodeText = '' then
Rec."Currency Code" := ''
else
Rec."Currency Code" := CopyStr(CurrencyCodeText, 1, MaxStrLen(Rec."Currency Code"));
end;
trigger OnAfterValidate()
begin
CurrencyCodeText := Rec."Currency Code";
end;
}
// Calculated fields for API consumption
field(totalAmountExcludingTax; TotalAmountExclTax)
{
Caption = 'Total Amount Excluding Tax';
Editable = false;
}
field(totalAmountIncludingTax; TotalAmountInclTax)
{
Caption = 'Total Amount Including Tax';
Editable = false;
}
// Status enum exposure
field(status; Rec.Status)
{
Caption = 'Status';
}
// Navigation properties for related entities
part(salesOrderLines; "Sales Order Line API")
{
Caption = 'Lines';
EntityName = 'salesOrderLine';
EntitySetName = 'salesOrderLines';
SubPageLink = "Document No." = field("No."), "Document Type" = field("Document Type");
}
}
}
}
var
CurrencyCodeText: Text[10];
TotalAmountExclTax: Decimal;
TotalAmountInclTax: Decimal;
trigger OnAfterGetRecord()
begin
CurrencyCodeText := Rec."Currency Code";
CalculateTotals();
end;
local procedure CalculateTotals()
var
SalesLine: Record "Sales Line";
begin
SalesLine.SetRange("Document Type", Rec."Document Type");
SalesLine.SetRange("Document No.", Rec."No.");
SalesLine.CalcSums("Line Amount", "Amount Including VAT");
TotalAmountExclTax := SalesLine."Line Amount";
TotalAmountInclTax := SalesLine."Amount Including VAT";
end;
}
```
## Security and Permission Integration
```al
page 50102 "Secure Customer API"
{
APIPublisher = 'contoso';
APIGroup = 'business';
APIVersion = 'v1.0';
EntityName = 'secureCustomer';
EntitySetName = 'secureCustomers';
PageType = API;
Caption = 'Secure Customer API';
SourceTable = Customer;
DelayedInsert = true;
ODataKeyFields = SystemId;
// API-specific permissions
Permissions = tabledata Customer = RIMD;
layout
{
area(Content)
{
repeater(GroupName)
{
field(id; Rec.SystemId)
{
Caption = 'Id';
Editable = false;
}
field(number; Rec."No.")
{
Caption = 'Number';
}
field(displayName; Rec.Name)
{
Caption = 'Display Name';
}
// Conditional field exposure based on permissions
field(creditLimit; CreditLimitValue)
{
Caption = 'Credit Limit';
trigger OnValidate()
begin
CheckCreditLimitPermission();
Rec."Credit Limit (LCY)" := CreditLimitValue;
end;
}
// Sensitive data with read-only access control
field(balance; BalanceValue)
{
Caption = 'Balance';
Editable = false;
}
}
}
}
var
CreditLimitValue: Decimal;
BalanceValue: Decimal;
trigger OnAfterGetRecord()
begin
// Apply security filters and data masking
ApplySecurityFilters();
LoadConditionalData();
end;
trigger OnInsertRecord(BelowxRec: Boolean): Boolean
begin
CheckInsertPermission();
exit(true);
end;
trigger OnModifyRecord(): Boolean
begin
CheckModifyPermission();
exit(true);
end;
trigger OnDeleteRecord(): Boolean
begin
CheckDeletePermission();
exit(true);
end;
local procedure ApplySecurityFilters()
var
UserSetup: Record "User Setup";
begin
// Apply user-specific filtering
if UserSetup.Get(UserId) then begin
if UserSetup."Salespers./Purch. Code" <> '' then
Rec.SetRange("Salesperson Code", UserSetup."Salespers./Purch. Code");
end;
end;
local procedure LoadConditionalData()
var
UserPermissions: Codeunit "User Permissions";
begin
// Load sensitive data based on permissions
if UserPermissions.IsSuper(UserSecurityId()) then begin
CreditLimitValue := Rec."Credit Limit (LCY)";
Rec.CalcFields(Balance);
BalanceValue := Rec.Balance;
end else begin
CreditLimitValue := 0;
BalanceValue := 0;
end;
end;
local procedure CheckCreditLimitPermission()
var
UserPermissions: Codeunit "User Permissions";
begin
if not UserPermissions.IsSuper(UserSecurityId()) then
Error('Insufficient permissions to modify credit limit');
end;
local procedure CheckInsertPermission()
begin
if not CheckTablePermission(Database::Customer, TablePermission::Insert) then
Error('Insufficient permissions to create customers');
end;
local procedure CheckModifyPermission()
begin
if not CheckTablePermission(Database::Customer, TablePermission::Modify) then
Error('Insufficient permissions to modify customers');
end;
local procedure CheckDeletePermission()
begin
if not CheckTablePermission(Database::Customer, TablePermission::Delete) then
Error('Insufficient permissions to delete customers');
end;
local procedure CheckTablePermission(TableNo: Integer; Permission: TablePermission): Boolean
var
RecordRef: RecordRef;
begin
RecordRef.Open(TableNo);
exit(RecordRef.ReadPermission() and
((Permission = TablePermission::Insert) and RecordRef.WritePermission()) or
((Permission = TablePermission::Modify) and RecordRef.WritePermission()) or
((Permission = TablePermission::Delete) and RecordRef.WritePermission()));
end;
}
enum 50100 TablePermission
{
Extensible = true;
value(0; Read) { Caption = 'Read'; }
value(1; Insert) { Caption = 'Insert'; }
value(2; Modify) { Caption = 'Modify'; }
value(3; Delete) { Caption = 'Delete'; }
}
```
## Performance Optimization Patterns
```al
page 50103 "Optimized Item API"
{
APIPublisher = 'contoso';
APIGroup = 'inventory';
APIVersion = 'v1.0';
EntityName = 'item';
EntitySetName = 'items';
PageType = API;
Caption = 'Optimized Item API';
SourceTable = Item;
DelayedInsert = true;
ODataKeyFields = SystemId;
// Performance optimization: Define source table view
SourceTableView = where(Blocked = const(false), Type = filter(<> "Non-Inventory"));
layout
{
area(Content)
{
repeater(GroupName)
{
field(id; Rec.SystemId)
{
Caption = 'Id';
Editable = false;
}
field(number; Rec."No.")
{
Caption = 'Number';
}
field(displayName; Rec.Description)
{
Caption = 'Display Name';
}
field(unitPrice; Rec."Unit Price")
{
Caption = 'Unit Price';
}
// Efficient inventory calculation
field(inventory; InventoryValue)
{
Caption = 'Inventory';
Editable = false;
}
// Pre-calculated unit of measure relationship
field(baseUnitOfMeasureCode; Rec."Base Unit of Measure")
{
Caption = 'Base Unit Of Measure Code';
Editable = false;
}
field(lastModifiedDateTime; Rec.SystemModifiedAt)
{
Caption = 'Last Modified Date Time';
Editable = false;
}
}
}
}
var
InventoryValue: Decimal;
trigger OnAfterGetRecord()
begin
LoadOptimizedData();
end;
trigger OnOpenPage()
begin
SetOptimalFilters();
end;
local procedure LoadOptimizedData()
begin
// Efficient inventory calculation - avoid FlowField if possible
if ShouldCalculateInventory() then begin
Rec.CalcFields(Inventory);
InventoryValue := Rec.Inventory;
end else
InventoryValue := 0;
end;
local procedure ShouldCalculateInventory(): Boolean
begin
// Conditional expensive calculations based on context
exit(Rec.Type = Rec.Type::Inventory);
end;
local procedure SetOptimalFilters()
begin
// Performance optimization: Apply efficient filters
Rec.SetFilter(Type, '<>%1', Rec.Type::"Non-Inventory");
Rec.SetRange(Blocked, false);
end;
}
```
## Anti-Pattern Examples (What NOT to Do)
```al
// ❌ ANTI-PATTERN: Poor field exposure and mapping
page 50199 "Bad Field Mapping API"
{
APIPublisher = 'contoso';
APIGroup = 'business';
APIVersion = 'v1.0';
EntityName = 'badCustomer';
EntitySetName = 'badCustomers';
PageType = API;
SourceTable = Customer;
// ❌ Missing: DelayedInsert = true for better performance
// ❌ Missing: ODataKeyFields specification
layout
{
area(Content)
{
repeater(Group)
{
// ❌ Wrong: Using database field names as API field names
field("No."; Rec."No.") { }
field("Customer Posting Group"; Rec."Customer Posting Group") { }
// ❌ Wrong: No relationship handling for lookups
field("Currency Code"; Rec."Currency Code")
{
// ❌ Missing: Proper relationship mapping to Currency entity
}
// ❌ Wrong: Exposing sensitive data without security checks
field("Credit Limit (LCY)"; Rec."Credit Limit (LCY)") { }
// ❌ Wrong: Exposing FlowFields without consideration for performance
field(Balance; Rec.Balance) { }
// ❌ Wrong: No proper validation or error handling
field("Payment Terms Code"; Rec."Payment Terms Code")
{
trigger OnValidate()
begin
// ❌ No validation - could cause data integrity issues
end;
}
}
}
}
// ❌ Missing: Proper trigger implementations
// ❌ Missing: Security and permission checks
// ❌ Missing: Error handling and validation
}
// ❌ ANTI-PATTERN: Performance-killing implementation
page 50198 "Performance Bad API"
{
APIPublisher = 'contoso';
APIGroup = 'business';
APIVersion = 'v1.0';
EntityName = 'slowCustomer';
EntitySetName = 'slowCustomers';
PageType = API;
SourceTable = Customer;
ODataKeyFields = SystemId;
layout
{
area(Content)
{
repeater(Group)
{
field(id; Rec.SystemId) { }
field(number; Rec."No.") { }
field(name; Rec.Name) { }
// ❌ Performance killer: Calculating complex data on every record
field(totalSales; GetTotalSales())
{
Caption = 'Total Sales';
}
// ❌ Performance killer: Multiple FlowFields
field(balance; Rec.Balance) { }
field(balanceLCY; Rec."Balance (LCY)") { }
field(salesLCY; Rec."Sales (LCY)") { }
// ❌ Performance killer: Related table lookups in field triggers
field(salesPersonName; GetSalesPersonName())
{
Caption = 'Sales Person Name';
}
}
}
}
// ❌ Performance killers: Heavy calculations in field methods
local procedure GetTotalSales(): Decimal
var
CustLedgerEntry: Record "Cust. Ledger Entry";
TotalAmount: Decimal;
begin
// ❌ Expensive calculation on every record load
CustLedgerEntry.SetRange("Customer No.", Rec."No.");
CustLedgerEntry.CalcSums("Sales (LCY)");
exit(CustLedgerEntry."Sales (LCY)");
end;
local procedure GetSalesPersonName(): Text
var
SalespersonPurchaser: Record "Salesperson/Purchaser";
begin
// ❌ Database lookup on every record display
if SalespersonPurchaser.Get(Rec."Salesperson Code") then
exit(SalespersonPurchaser.Name)
else
exit('');
end;
}
```
## Correct Implementation Patterns
```al
// ✅ CORRECT: Optimized API with proper structure
page 50197 "Optimized Customer API"
{
APIPublisher = 'contoso';
APIGroup = 'business';
APIVersion = 'v1.0';
EntityName = 'customer';
EntitySetName = 'customers';
PageType = API;
Caption = 'Customer API';
SourceTable = Customer;
DelayedInsert = true;
ODataKeyFields = SystemId;
layout
{
area(Content)
{
repeater(GroupName)
{
field(id; Rec.SystemId)
{
Caption = 'Id';
Editable = false;
}
field(number; Rec."No.")
{
Caption = 'Number';
}
field(displayName; Rec.Name)
{
Caption = 'Display Name';
}
field(email; Rec."E-Mail")
{
Caption = 'Email';
}
field(phoneNumber; Rec."Phone No.")
{
Caption = 'Phone Number';
}
field(blocked; Rec.Blocked)
{
Caption = 'Blocked';
}
// ✅ Calculated field with efficient loading
field(salesPersonName; SalesPersonNameVar)
{
Caption = 'Sales Person Name';
Editable = false;
}
field(lastModifiedDateTime; Rec.SystemModifiedAt)
{
Caption = 'Last Modified Date Time';
Editable = false;
}
}
}
}
var
SalesPersonNameVar: Text[50];
trigger OnAfterGetRecord()
begin
LoadRelatedData();
end;
local procedure LoadRelatedData()
var
SalespersonPurchaser: Record "Salesperson/Purchaser";
begin
// ✅ Efficient: Single lookup per record
if Rec."Salesperson Code" <> '' then begin
if SalespersonPurchaser.Get(Rec."Salesperson Code") then
SalesPersonNameVar := SalespersonPurchaser.Name
else
SalesPersonNameVar := '';
end else
SalesPersonNameVar := '';
end;
}
```
## API Implementation Guidelines
### Essential API Page Configuration
1. **Required Properties**: APIPublisher, APIGroup, APIVersion, EntityName, EntitySetName, PageType = API
2. **Performance Settings**: DelayedInsert = true, ODataKeyFields = SystemId
3. **Field Naming**: Use camelCase for API field names, provide meaningful captions
4. **Relationship Handling**: Use SystemId for entity relationships, implement proper validation
### Security Best Practices
1. **Permission Integration**: Use Permissions property and implement trigger-based permission checks
2. **Data Filtering**: Apply user-specific filters in OnAfterGetRecord trigger
3. **Sensitive Data**: Implement conditional loading based on user permissions
4. **Validation**: Add proper validation in field triggers and record triggers
### Performance Optimization
1. **Avoid FlowFields**: Calculate values in triggers instead of using FlowFields directly
2. **Efficient Queries**: Use SourceTableView to apply optimal filters
3. **Batch Loading**: Load related data efficiently in OnAfterGetRecord
4. **Conditional Processing**: Only perform expensive operations when necessary
### Error Handling
1. **Validation Errors**: Provide clear, actionable error messages
2. **Permission Errors**: Return appropriate HTTP status codes for authorization issues
3. **Business Logic**: Respect Business Central validation rules and constraints
4. **Data Integrity**: Ensure API operations maintain data consistency