bc-code-intelligence-mcp
Version:
BC Code Intelligence MCP Server - Complete Specialist Bundle with AI-driven expert consultation, seamless handoffs, and context-preserving workflows
508 lines (451 loc) • 16.1 kB
Markdown
# ETag Implementation for Optimistic Concurrency - AL Code Sample
## Basic ETag Implementation
```al
page 50300 "Customer ETag API"
{
PageType = API;
APIPublisher = 'contoso';
APIGroup = 'customers';
APIVersion = 'v1.0';
EntityName = 'customer';
EntitySetName = 'customers';
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(lastModifiedDateTime; Rec.SystemModifiedAt)
{
Caption = 'Last Modified Date Time';
Editable = false;
// Used for ETag generation
}
// ETag field for optimistic concurrency control
field(etag; ETagValue)
{
Caption = 'ETag';
Editable = false;
}
}
}
}
var
ETagValue: Text;
OriginalETagValue: Text;
trigger OnAfterGetRecord()
begin
// Generate ETag based on SystemModifiedAt
ETagValue := GenerateETag(Rec.SystemModifiedAt, Rec.SystemId);
end;
trigger OnModifyRecord(): Boolean
var
CurrentRecord: Record Customer;
CurrentETag: Text;
begin
// Validate ETag for optimistic concurrency
if CurrentRecord.GetBySystemId(Rec.SystemId) then begin
CurrentETag := GenerateETag(CurrentRecord.SystemModifiedAt, CurrentRecord.SystemId);
// Check if record was modified by another user
if CurrentETag <> OriginalETagValue then
Error('The record has been modified by another user. Please refresh and try again.');
end;
exit(true);
end;
local procedure GenerateETag(ModifiedDateTime: DateTime; RecordSystemId: Guid): Text
var
TypeHelper: Codeunit "Type Helper";
ETagSource: Text;
begin
// Combine timestamp and SystemId for unique ETag
ETagSource := Format(ModifiedDateTime, 0, 9) + Format(RecordSystemId);
exit(TypeHelper.GetMD5Hash(ETagSource));
end;
}
```
## Advanced ETag with Custom Versioning
```al
// Custom versioning table for ETag management
table 50300 "Record Version Control"
{
Caption = 'Record Version Control';
DataClassification = SystemMetadata;
fields
{
field(1; "Table ID"; Integer)
{
Caption = 'Table ID';
}
field(2; "Record SystemId"; Guid)
{
Caption = 'Record SystemId';
}
field(3; "Version Number"; Integer)
{
Caption = 'Version Number';
InitValue = 1;
}
field(4; "Last Modified DateTime"; DateTime)
{
Caption = 'Last Modified DateTime';
}
field(5; "Modified By User"; Text[100])
{
Caption = 'Modified By User';
}
field(6; "ETag Value"; Text[100])
{
Caption = 'ETag Value';
}
}
keys
{
key(PK; "Table ID", "Record SystemId")
{
Clustered = true;
}
key(ETag; "ETag Value")
{
// Index for fast ETag lookups
}
}
}
// ETag management codeunit
codeunit 50300 "ETag Manager"
{
// Generate and store ETag for record
procedure GenerateETag(TableID: Integer; RecordSystemId: Guid): Text
var
VersionControl: Record "Record Version Control";
ETagValue: Text;
VersionNumber: Integer;
begin
// Get or create version record
if VersionControl.Get(TableID, RecordSystemId) then begin
VersionNumber := VersionControl."Version Number" + 1;
end else begin
VersionControl.Init();
VersionControl."Table ID" := TableID;
VersionControl."Record SystemId" := RecordSystemId;
VersionNumber := 1;
end;
// Generate ETag from version and timestamp
ETagValue := StrSubstNo('%1-%2-%3',
TableID,
VersionNumber,
Format(CurrentDateTime, 0, 9));
// Update version control record
VersionControl."Version Number" := VersionNumber;
VersionControl."Last Modified DateTime" := CurrentDateTime;
VersionControl."Modified By User" := UserId;
VersionControl."ETag Value" := ETagValue;
if VersionControl."Version Number" = 1 then
VersionControl.Insert()
else
VersionControl.Modify();
exit(ETagValue);
end;
// Validate ETag for concurrency control
procedure ValidateETag(TableID: Integer; RecordSystemId: Guid; ProvidedETag: Text): Boolean
var
VersionControl: Record "Record Version Control";
begin
if not VersionControl.Get(TableID, RecordSystemId) then
exit(false); // No version record exists
exit(VersionControl."ETag Value" = ProvidedETag);
end;
// Get current ETag for record
procedure GetCurrentETag(TableID: Integer; RecordSystemId: Guid): Text
var
VersionControl: Record "Record Version Control";
begin
if VersionControl.Get(TableID, RecordSystemId) then
exit(VersionControl."ETag Value");
exit('');
end;
}
// API page with advanced ETag implementation
page 50301 "Item Advanced ETag API"
{
PageType = API;
APIPublisher = 'contoso';
APIGroup = 'inventory';
APIVersion = 'v1.0';
EntityName = 'item';
EntitySetName = 'items';
SourceTable = Item;
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.Description)
{
Caption = 'Display Name';
}
field(unitPrice; Rec."Unit Price")
{
Caption = 'Unit Price';
}
field(inventory; Rec.Inventory)
{
Caption = 'Inventory';
Editable = false;
}
field(etag; ETagValue)
{
Caption = 'ETag';
Editable = false;
}
field(version; VersionNumber)
{
Caption = 'Version';
Editable = false;
}
}
}
}
var
ETagValue: Text;
VersionNumber: Integer;
ETagManager: Codeunit "ETag Manager";
ProvidedETag: Text;
trigger OnAfterGetRecord()
var
VersionControl: Record "Record Version Control";
begin
// Get current ETag and version
ETagValue := ETagManager.GetCurrentETag(Database::Item, Rec.SystemId);
if VersionControl.Get(Database::Item, Rec.SystemId) then
VersionNumber := VersionControl."Version Number"
else
VersionNumber := 0;
// Generate ETag if not exists
if ETagValue = '' then
ETagValue := ETagManager.GenerateETag(Database::Item, Rec.SystemId);
end;
trigger OnInsertRecord(BelowxRec: Boolean): Boolean
begin
// Generate initial ETag for new record
ETagValue := ETagManager.GenerateETag(Database::Item, Rec.SystemId);
exit(true);
end;
trigger OnModifyRecord(): Boolean
begin
// Validate provided ETag before modification
if ProvidedETag <> '' then begin
if not ETagManager.ValidateETag(Database::Item, Rec.SystemId, ProvidedETag) then
Error('Concurrency conflict detected. The record has been modified by another user. ETag: %1', ProvidedETag);
end;
// Generate new ETag after successful modification
ETagValue := ETagManager.GenerateETag(Database::Item, Rec.SystemId);
exit(true);
end;
trigger OnDeleteRecord(): Boolean
var
VersionControl: Record "Record Version Control";
begin
// Validate ETag before deletion
if ProvidedETag <> '' then begin
if not ETagManager.ValidateETag(Database::Item, Rec.SystemId, ProvidedETag) then
Error('Concurrency conflict detected. Cannot delete record that has been modified.');
end;
// Clean up version control record
if VersionControl.Get(Database::Item, Rec.SystemId) then
VersionControl.Delete();
exit(true);
end;
}
```
## HTTP Header ETag Processing
```al
// Codeunit for processing ETag HTTP headers
codeunit 50301 "HTTP ETag Processor"
{
// Extract ETag from If-Match header
procedure ExtractETagFromHeader(HttpHeaders: Dictionary of [Text, Text]): Text
var
ETagHeader: Text;
ETagValue: Text;
begin
// Check If-Match header for ETag value
if HttpHeaders.Get('If-Match', ETagHeader) then begin
// Remove quotes from ETag value
ETagValue := ETagHeader.Replace('"', '');
exit(ETagValue);
end;
// Check If-None-Match header
if HttpHeaders.Get('If-None-Match', ETagHeader) then begin
ETagValue := ETagHeader.Replace('"', '');
exit(ETagValue);
end;
exit('');
end;
// Set ETag in response headers
procedure SetETagResponseHeader(var HttpHeaders: Dictionary of [Text, Text]; ETagValue: Text)
begin
if ETagValue <> '' then
HttpHeaders.Set('ETag', '"' + ETagValue + '"');
end;
// Process conditional request based on ETag
procedure ProcessConditionalRequest(ProvidedETag: Text; CurrentETag: Text; RequestMethod: Text): Boolean
begin
case RequestMethod of
'GET':
// If-None-Match: return 304 if ETags match
exit(ProvidedETag <> CurrentETag);
'PUT', 'PATCH', 'DELETE':
// If-Match: proceed only if ETags match
exit(ProvidedETag = CurrentETag);
else
exit(true);
end;
end;
}
// API page with HTTP ETag header processing
page 50302 "Sales Order ETag API"
{
PageType = API;
APIPublisher = 'contoso';
APIGroup = 'sales';
APIVersion = 'v1.0';
EntityName = 'salesOrder';
EntitySetName = 'salesOrders';
SourceTable = "Sales Header";
DelayedInsert = true;
ODataKeyFields = SystemId;
SourceTableView = WHERE("Document Type" = CONST(Order));
layout
{
area(Content)
{
repeater(GroupName)
{
field(id; Rec.SystemId)
{
Caption = 'Id';
Editable = false;
}
field(number; Rec."No.")
{
Caption = 'Number';
}
field(customerNumber; Rec."Sell-to Customer No.")
{
Caption = 'Customer Number';
}
field(orderDate; Rec."Order Date")
{
Caption = 'Order Date';
}
field(totalAmount; TotalAmount)
{
Caption = 'Total Amount';
Editable = false;
}
field(etag; ETagValue)
{
Caption = 'ETag';
Editable = false;
}
}
}
}
var
ETagValue: Text;
TotalAmount: Decimal;
ETagManager: Codeunit "ETag Manager";
HttpETagProcessor: Codeunit "HTTP ETag Processor";
RequestETag: Text;
trigger OnAfterGetRecord()
begin
// Calculate totals
Rec.CalcFields("Amount Including VAT");
TotalAmount := Rec."Amount Including VAT";
// Generate ETag based on header and line modifications
ETagValue := GenerateOrderETag();
end;
trigger OnModifyRecord(): Boolean
var
CurrentETag: Text;
begin
// Get current ETag for comparison
CurrentETag := GenerateOrderETag();
// Validate provided ETag matches current state
if (RequestETag <> '') and (RequestETag <> CurrentETag) then
Error('Concurrency conflict: Order has been modified by another user. Current ETag: %1, Provided ETag: %2', CurrentETag, RequestETag);
exit(true);
end;
local procedure GenerateOrderETag(): Text
var
SalesLine: Record "Sales Line";
ETagSource: Text;
TypeHelper: Codeunit "Type Helper";
LineModificationTime: DateTime;
begin
// Build ETag from header and all related lines
ETagSource := Format(Rec.SystemModifiedAt, 0, 9);
// Include line modifications in ETag
SalesLine.SetRange("Document Type", Rec."Document Type");
SalesLine.SetRange("Document No.", Rec."No.");
if SalesLine.FindLast() then
LineModificationTime := SalesLine.SystemModifiedAt
else
LineModificationTime := Rec.SystemModifiedAt;
ETagSource += Format(LineModificationTime, 0, 9);
ETagSource += Format(Rec.SystemId);
// Generate hash-based ETag
exit(TypeHelper.GetMD5Hash(ETagSource));
end;
}
```
## Implementation Notes
**ETag Implementation Best Practices:**
1. **ETag Generation Strategies:**
- Use SystemModifiedAt for simple timestamp-based ETags
- Combine multiple factors (timestamp + SystemId + version) for robust ETags
- Include related record modifications for composite entities
- Use consistent hashing algorithms (MD5, SHA-256) for ETag values
2. **Concurrency Control:**
- Validate ETags on all modification operations (PUT, PATCH, DELETE)
- Return appropriate HTTP status codes (409 Conflict, 412 Precondition Failed)
- Implement version tracking for detailed conflict resolution
- Provide clear error messages for concurrency conflicts
3. **Performance Optimization:**
- Index ETag fields for fast lookups
- Cache ETag values to avoid recalculation
- Use efficient ETag generation algorithms
- Minimize database queries during ETag validation
4. **HTTP Integration:**
- Process If-Match and If-None-Match headers correctly
- Set ETag response headers for all API responses
- Support conditional requests (304 Not Modified responses)
- Handle weak vs strong ETag semantics appropriately