Mastering X++ Containers in D365 F&O

ChallaChalla
7 min read

This blog explores how to leverage X++ containers in banking and financial scenarios with practical examples that you can implement in your own solutions. What Are X++ Containers?

What Are X++ Containers?

Containers in X++ are versatile data structures that can store multiple values of different types in a single variable. Unlike arrays or lists that typically store elements of the same type, containers can mix strings, integers, dates, and even record IDs in one collection.

Key characteristics:

  • Heterogeneous storage (multiple data types)

  • Dynamic sizing (grow and shrink as needed)

  • One-based indexing (first element is at position 1)

  • Built-in manipulation functions with the "con" prefix

Internal Implementation Details

  1. Memory Management: X++ containers use a dynamically allocated memory block that grows as needed. When a container exceeds its current capacity, the runtime allocates a larger block and copies the existing elements.

  2. Type Handling: Containers maintain type information for each element. When you retrieve a value with conPeek(), the runtime performs an implicit type conversion if necessary.

  3. Serialization Support: Containers can be easily serialized and deserialized, making them ideal for storing in database fields or passing between tiers in the D365 architecture.

  4. Value Semantics: Containers use value semantics (not reference semantics), meaning when you assign one container to another, a complete copy is made:

      container c1 = [1, 2, 3];
        container c2 = c1;  // Creates a complete copy
        conPoke(c1, 1, 99); // Only affects c1, not c2
    
    1. AOT Representation: In X++ metadata, containers are represented using the Array type with special handling for heterogeneous contents.

Example : Banking Transaction Batch Processing

//Plz Treat this as Example Code , Not Production Standard

public void processBankTransactions(BankAccountTable _bankAccount)
{
    container transactionBatch;
    container currentTransaction;
    BankTransactionTable bankTrans;

    // Build a batch of pending transactions
    while select * from bankTrans
        where bankTrans.BankAccountId == _bankAccount.RecId &&
              bankTrans.Status == BankTransactionStatus::Pending
    {
        // Store each transaction as a container within our batch container
        currentTransaction = [bankTrans.RecId,          // Transaction ID
                             bankTrans.TransDate,       // Transaction date
                             bankTrans.Amount,          // Amount
                             bankTrans.TransactionType, // Type
                             bankTrans.Reference];      // Reference

        transactionBatch += currentTransaction;
    }

    // Process each transaction in the batch
    for (int i = 1; i <= conLen(transactionBatch); i++)
    {
        currentTransaction = conPeek(transactionBatch, i);

        try
        {
            this.processTransaction(currentTransaction);
            this.updateTransactionStatus(conPeek(currentTransaction, 1), 
                                        BankTransactionStatus::Processed);
        }
        catch (Exception::Error)
        {
            this.updateTransactionStatus(conPeek(currentTransaction, 1),
                                        BankTransactionStatus::Error);
            // Log error details
        }
    }
}

Another Example : Bank Reconciliation Helper

When reconciling bank statements, you often need to temporarily store matches between system records and bank statement lines:

//Plz Treat this as Example Code , Not Production Standard
public container findPotentialMatches(BankStatementLine _statementLine)
{
    container matches;
    container matchDetails;
    LedgerJournalTrans journalTrans;
    BankAccountTrans bankTrans;

    // Look for matches in journal entries
    while select * from journalTrans
        where journalTrans.AccountType == LedgerJournalACType::Bank &&
              journalTrans.Account == _statementLine.BankAccountId &&
              journalTrans.AmountCurDebit == _statementLine.Amount &&
              journalTrans.TransDate == _statementLine.TransactionDate
    {
        matchDetails = ['JournalEntry',             // Match type
                        journalTrans.RecId,         // Record ID
                        journalTrans.Voucher,       // Voucher
                        journalTrans.AmountCurDebit,// Amount
                        journalTrans.TransDate];    // Date

        matches += matchDetails;
    }

    // Look for matches in bank transactions
    while select * from bankTrans
        where bankTrans.BankAccountId == _statementLine.BankAccountId &&
              bankTrans.Amount == _statementLine.Amount &&
              bankTrans.ValueDate == _statementLine.TransactionDate
    {
        matchDetails = ['BankTransaction',         // Match type
                        bankTrans.RecId,           // Record ID
                        bankTrans.Reference,       // Reference
                        bankTrans.Amount,          // Amount
                        bankTrans.ValueDate];      // Date

        matches += matchDetails;
    }

    return matches;
}

Advanced Container Techniques in Banking Applications

Beyond the basic examples, here are some advanced techniques that can take your container usage to the next level in banking applications:

1. Functional Container Operations

You can implement functional programming techniques with containers:

//Plz Treat this as Example Code , Not Production Standard

public container filterContainer(container _source, IdentifierName _predicateFunctionName)
{
    container result;
    DictClass dictClass = new DictClass(0); // Properly initialize with an iterator ID

    for (int i = 1; i <= conLen(_source); i++)
    {
        // Call the predicate function to determine if element should be kept
        if (dictClass.callStatic(_predicateFunctionName, _source, i))
        {
            result += conPeek(_source, i);
        }
    }

    return result;
}

// Example usage for transaction filtering
public static boolean isHighValueTransaction(container _transactions, int _index)
{
    container transaction = conPeek(_transactions, _index);
    real amount = conPeek(transaction, 3);

    return amount > 10000; // Consider transactions over 10,000 as high-value
}

// Usage in code
container highValueTransactions = filterContainer(transactionBatch, 
                                                 identifierStr(isHighValueTransaction));

Performance Analysis: Big O for X++ Containers

Understanding the time complexity of container operations is crucial when working with large datasets in banking applications. Here's a comprehensive analysis of X++ container operations:

OperationFunctionTime ComplexityDescription
AccessconPeek(container, index)O(1)Direct access by index is constant time
UpdateconPoke(container, index, value)O(1)Updating an existing element is constant time
LengthconLen(container)O(1)Getting container size is constant time
Addcontainer += valueO(1)*Adding to the end of a container is amortized constant time

\Note: While appending is O(1) amortized, individual operations may occasionally trigger a reallocation, which is O(n).*

Search Operations

OperationFunctionTime ComplexityDescription
FindconFind(container, value)O(n)Linear search through all elements
Find with PredicateCustom loop with conditionO(n)Must check each element until match found

Modification Operations

OperationFunctionTime ComplexityDescription
InsertconIns(container, index, value)O(n)Requires shifting elements after insertion point
DeleteconDel(container, index)O(n)Requires shifting elements after deletion point
ConcatenateconCat(container1, container2)O(n)Where n is the size of the second container

Memory Complexity

Memory usage is another critical aspect when dealing with financial data:

OperationMemory OverheadNotes
Container CreationO(1)Initial allocation
Element AdditionO(n) in worst caseWhen capacity is exceeded, reallocation occurs
Container CopyO(n)Full copy is made
Nested ContainersO(sum of all elements)Each container has its own allocation

Large Dataset Considerations

For very large datasets (e.g., processing thousands of bank transactions), consider these optimizations:

  • Batch size control: Limit container sizes to avoid memory issues

  • Database filtering: Filter data at the database level before adding to containers

  • Selective storage: Store only necessary fields in containers

  • Memory management: Clear containers when no longer needed:

    container myContainer; // ... operations ... myContainer = []; // Clear container

    Comparison with Other X++ Collections

Best Practices for X++ Containers

  1. Document container structure: Always document what each position in your container represents.

  2. Consider type safety: For complex scenarios, weigh containers against structured types like classes.

  3. Use constants for indexes: Define constants for container positions to avoid "magic numbers":
    #define.CUST_PROFILE_ACCOUNT(1)
    #define.CUST_PROFILE_NAME(2)
    #define.CUST_PROFILE_BANK_ACCOUNT(3)
    // Usage:
    str accountNum = conPeek(profile, #CUST_PROFILE_ACCOUNT);

  4. Validate containers: Check container length before accessing elements.

  5. Nested containers: For complex data, consider containers within containers as seen in the transaction example.

  6. Performance monitoring: For large banking operations, monitor memory consumption and processing time when using large containers.

  7. Boundary checking: Always check container boundaries before accessing elements:
    if (conLen(container) >= index) { value = conPeek(container, index); }

  8. Container reuse: Consider clearing and reusing containers for repetitive operations to reduce memory allocations:
    container reusableContainer;

    // In a loop reusableContainer = []; // Clear before reuse // Add new data...

  9. Encapsulate container operations: Create utility methods for common container operations to improve code readability and maintainability.

  10. Balance with other data structures: Use the right tool for the job—containers for heterogeneous collections, Lists for homogeneous collections with frequent insertions, Maps for key-value lookups.

Conclusion

X++ containers provide a flexible and powerful way to handle diverse data types in Dynamics 365 F&O banking modules. They shine in scenarios involving temporary storage, parameter passing, and working with heterogeneous data. While they shouldn't replace proper class design for complex business objects, they offer an elegant solution for many everyday development challenges.

Remember that containers are a tool in your arsenal—not a silver bullet. When used appropriately, they can significantly improve code readability and maintainability while maintaining good performance. But they should be complemented with proper object-oriented design for complex domain models and data structures.

0
Subscribe to my newsletter

Read articles from Challa directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Challa
Challa