X++ Data Structures for C# Developers: Bridging the Gap

From Web APIs to ERP : A Developers Journey
As someone who just recently started in the Dynamics 365 F&O space after spending many years in the world of .NET APIs and Web development, my first encounter with X++ felt like landing on an alien planet. The syntax looked almost familiar -- yes, there were curly braces and semicolons -- but the way data structures worked? That was a whole different story.
“Where is the List equivalent in this language?" I remember asking during my first week. "How is it possible to mix different data types in the same collection without type constraints?
If you're a C# developer who's been thrown into the X++ world (or just starting like me), I understand your confusion. This post is everything I wish someone had told me when I first made the transition, written while the struggles and discoveries are still fresh in my mind.
Understanding the X++ Data Access Architecture
When working with Dynamics 365 F&O, you're dealing with a layered architecture designed specifically for enterprise resource planning (ERP) systems. Unlike the web APIs I was used to building, X++ code operates within the Application Object Server (AOS), which mediates between the user interface and the database.
Here's the full picture of how it all fits together:
Try This Now: Explore Your Environment
If you're new to D365 F&O, take a moment to orient yourself:
Open Visual Studio with your D365 F&O development environment
Navigate to AOT > Jobs
Create a new job called "ExploreDataStructures"
Add this simple code to print out the type of architecture you're working with
static void ExploreDataStructures(Args _args) { info("I'm running in: " + Microsoft.Dynamics.AX.Metadata.Core.xppObject.getApplicationContext().getEnvironment()); info("Available to me are various X++ data structures!"); }
This simple exercise helps establish where your code will execute and reinforces that you're in a different world than typical C# applications.
X++ Data Structures: The Building Blocks
Containers: The Swiss Army Knife
Try This Now: Your First X++ Container
X++ container with mixed types container myContainer = ['String value', 123, true];
static void ContainerExperiment(Args _args)
{
container myFirstContainer = ['Customer', 1001, true];
// Access elements (notice the 1-based indexing!)
info(conPeek(myFirstContainer, 1)); // Prints "Customer"
// Add a new element
myFirstContainer += "New Value";
(conLen(myFirstContainer)); // Prints 4
// What happens if we try to mix operations?
// Uncomment to see the error:
// int result = conPeek(myFirstContainer, 1) + conPeek(myFirstContainer, 2);
}
What happened when you ran this? Did you notice how X++ handles the mixed types?
A Bit More Concrete Scenario
//Simplified Example Treat them as Mental Models,In Prod there are various Other considerations to be made
// Prepares reconciliation parameters for a bank statement import
static void SetupBankReconciliationParameters(Args _args)
{
// Container holding mixed types for reconciliation settings
// [AccountID, ToleranceAmount, AutoMatchEnabled, MatchingPeriodDays]
container reconcParams;
str bankAccountId = 'MAIN001';
// Create container with different parameter types
reconcParams = [
bankAccountId, // String: Bank account ID
10.00, // Real: Tolerance amount for matching
true, // Boolean: Enable automatic matching
30 // Integer: Days to look back for matching
];
// Display the parameters
info("Bank Reconciliation Parameters:");
info(strFmt("Account: %1", conPeek(reconcParams, 1)));
info(strFmt("Tolerance Amount: $%1", conPeek(reconcParams, 2)));
info(strFmt("Auto-Matching: %1", conPeek(reconcParams, 3) ? "Enabled" : "Disabled"));
info(strFmt("Matching Period: %1 days", conPeek(reconcParams, 4)));
// Pass parameters to the reconciliation process
if (conPeek(reconcParams, 3)) // If auto-matching is enabled
{
info("Starting automatic reconciliation process...");
runBankReconciliation(reconcParams);
}
}
/// Simulates running the bank reconciliation process
static void runBankReconciliation(container _params)
{
// In a real system, this would use the parameters to
// perform reconciliation between bank statements and ledger
info(strFmt("Reconciling account %1 with $%2 tolerance for the past %3 days",
conPeek(_params, 1),
conPeek(_params, 2),
conPeek(_params, 4)));
}
Lists: The Ordered Collection
List<T>
. In X++, there's the List class, which maintains items in the order you add them./X++ List List myList = new List(Types::String); myList.addEnd("First item"); myList.addEnd("Second item");
Try This Now: Working with Lists
static void ListExperiment(Args _args)
{
List nameList = new List(Types::String);
nameList.addEnd("Alice");
nameList.addEnd("Dyamics");
nameList.addEnd("FnO");
// Lists have iterators
ListIterator iterator = new ListIterator(nameList);
while (iterator.more())
{
info(iterator.value());
iterator.next();
}
}
A Bit More Concrete Scenario
//Simplified Example Treat them as Mental Models,In Prod there are various Other considerations to be made
//Generates a list of bank accounts requiring review based on last activity date
static void FindInactiveBankAccounts(Args _args)
{
BankAccountTable bankAccount;
List accountsToReview = new List(Types::Record);
ListIterator iterator;
date reviewThreshold = systemDateGet() - 90; // Accounts inactive for 90+ days
// Find accounts with no recent activity
while select bankAccount
where bankAccount.LastActivity < reviewThreshold &&
bankAccount.AccountStatus == BankAccountStatus::Active
{
// Add accounts requiring review to our list
accountsToReview.addEnd(bankAccount);
}
// Process the list of accounts needing review
info(strFmt("Found %1 bank accounts requiring review", accountsToReview.elements()));
iterator = new ListIterator(accountsToReview);
while (iterator.more())
{
bankAccount = iterator.value();
info(strFmt("Account %1 (%2) - Last activity: %3",
bankAccount.BankAccountId,
bankAccount.Name,
bankAccount.LastActivity));
iterator.next();
}
}
Maps: The Key-Value Store
Dictionary<K,V>
in C#, you'll work with Map in X++X++ Map Map customerMap = new Map(Types::String, Types::Integer); customerMap.insert("CUST001", 1001); customerMap.insert("CUST002", 1002);
Building a Customer Map
static void MapExperiment(Args _args)
{
// Create a map of customer IDs to credit limits
Map creditLimitMap = new Map(Types::String, Types::Real);
// Add some customers
creditLimitMap.insert("CUST001", 5000.00);
creditLimitMap.insert("CUST002", 10000.00);
// Look up a credit limit
print("Credit limit for CUST001: " +
any2str(creditLimitMap.lookup("CUST001")));
// Update a value
creditLimitMap.insert("CUST001", 7500.00);
print("New credit limit for CUST001: " +
any2str(creditLimitMap.lookup("CUST001")));
}
A Bit More Concrete Scenario
Imagine a Scenario where you want to build a quick in-memory lookup of BankAccountId → CurrentBalance, so you can:
Quickly retrieve balances for processing,
Compare multiple accounts,
Avoid repeated queries to the database.
//Simplified Example Treat them as Mental Models,In Prod there are various Other considerations to be made static void Map_BankAccountsBalances(Args _args) { Map bankBalanceMap = new Map(Types::String, Types::Real); // BankAccountId → Balance BankAccountTable bankAccount; BankAccountTrans bankTrans; AmountCur balance; // 1. Loop through bank accounts while select bankAccount { // 2. Calculate balance (simplified logic: sum of transactions) balance = 0; while select sum(AmountCur) from bankTrans where bankTrans.BankAccountID == bankAccount.BankAccountId { balance = bankTrans.AmountCur; } // 3. Add to Map bankBalanceMap.insert(bankAccount.BankAccountId, balance); } // 4. Iterate and display MapEnumerator mapEnum = bankBalanceMap.getEnumerator(); while (mapEnum.moveNext()) { str bankId = mapEnum.currentKey(); real bal = mapEnum.currentValue(); info(strFmt("Bank Account: %1 | Balance: %2", bankId, bal)); } }
Database-Related Structures
This is where X++ really differs from C# and web development. Instead of ORM frameworks like Entity Framework, X++ provides specialized structures for database operations:
RecordSortedList: For Sorted In-Memory Records
// Sorting customers by name
RecordSortedList sortedCustomers = new RecordSortedList(tableNum(CustTable));
sortedCustomers.addSortField(fieldNum(CustTable, Name), SortOrder::Ascending);
A Bit More Concrete Scenario
//// Inserts multiple bank accounts in bulk for better performance
// A Mental Model for the data structure -
//In Production code you would want to Optimize for
//1. batch processing to handle larger datasets for timeout handing
//2. Err Handling
//etc
public static void insertCustomersBulk()
{
RecordInsertList recordInsertList;
CustTable custTable;
int i;
// 1. Create a new RecordInsertList for CustTable
recordInsertList = new RecordInsertList(tableNum(CustTable));
// 2. Loop and create multiple new records
for (i = 1; i <= 5; i++)
{
custTable.clear();
custTable.AccountNum = strFmt("NEWACC%1", i);
custTable.Name = strFmt("New Customer %1", i);
// ... assign other fields as necessary
// 3. Add the newly populated record to the RecordInsertList
recordInsertList.add(custTable);
}
// 4. Commit (bulk insert) all records to the database
ttsbegin;
recordInsertList.insertDatabase();
ttscommit;
}
RecordSortedList: For Sorted In-Memory Records
Sorting customers by nameRecordSortedList sortedCustomers = new RecordSortedList(tableNum(CustTable)); sortedCustomers.addSortField(fieldNum(CustTable, Name), SortOrder::Ascending);
A Bit More Concrete Scenario : In-Memory Record Sorting
//Simplified Example Treat them as Mental Models,In Prod there are various Other considerations to be made
//Using RecordSortedList with BankAccountTable
static void InMemorySortingExample(Args _args)
{
// Create a sorted list of bank accounts by name
RecordSortedList sortedBankAccounts = new RecordSortedList(tableNum(BankAccountTable));
sortedBankAccounts.addSortField(fieldNum(BankAccountTable, Name), SortOrder::Ascending);
BankAccountTable bankAccount;
int count = 0;
while select bankAccount
{
if (count++ >= 10)
break;
sortedBankAccounts.add(bankAccount);
}
// Now iterate through the sorted bank accounts
BankAccountTable sortedAcct;
info("Bank accounts sorted by name:");
for (int i = 1; i <= sortedBankAccounts.elements(); i++)
{
sortedAcct = sortedBankAccounts.getRecord(i);
info(strFmt("%1. %2", i, sortedAcct.Name));
}
}
Points to Ponder
X++ vs. C# Side-by-Side: Visual Comparison
To help you visualize the differences, here's a side-by-side comparison of common operations:
Common Pitfalls When Transitioning from C# to X++
Final Thoughts
As I continue learning my way through the X++ landscape, I'm discovering new patterns and practices every day. This post captures my understanding at this point in my journey, and I'll be sharing new insights as I grow more comfortable with Dynamics 365 F&O development.
If you're also new to this world, I'd love to hear about your experiences and discoveries in the comments. And if you're a seasoned X++ developer who spotted something I've misunderstood, I welcome your gentle corrections -- we're all learning together!
Join me next week when I dive deeper into containers and how they compare to the C# collections I used to rely on. Until then, happy coding in whatever syntax life throws your way!
✨Happy Coding!
Subscribe to my newsletter
Read articles from Challa directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
