5 Practical X++ Tips for D365 F&O Developers (That Actually Save Time)
Whether you’re new to X++ or have been working with Dynamics 365 Finance & Operations (D365 F&O) for years, there are small patterns and habits that can make your life much easier.
In this post, I’ll share 5 practical X++ tips that I’ve seen repeatedly help in real projects – from safer queries to more maintainable and performance-friendly code.
───
Environment
• Product: Dynamics 365 Finance & Operations / Finance and Supply Chain Management
• Language: X++
• Context: Customizations, extensions, and integrations in D365 F&O
These tips are focused on real-world scenarios you’re likely to encounter in daily development.
───
Tip 1 – Prefer while select with clear field lists
It’s very common to see:
while select * from salesTable
{
// do something
}
This works, but it’s not ideal. Pulling all fields can increase IO and memory usage, especially on big tables.
Instead, try to:
• Select only the fields you need
• Make it explicit and easier to read
SalesTable salesTable;
;
while select RecId, SalesId, CustAccount from salesTable
where salesTable.SalesStatus == SalesStatus::Backorder
{
// Use only the fields you need
info(strFmt("Sales %1 / Customer %2", salesTable.SalesId, salesTable.CustAccount));
}
Why it helps:
• Reduces data transfer from SQL to AOS
• Makes it clearer to reviewers what the query is really using
• Easier to spot unnecessary joins/fields later
───
Tip 2 – Always be explicit with ttsbegin / ttscommit
Transactions in X++ are powerful, but mistakes here can be painful. A typical anti-pattern is:
ttsbegin;
// Some logic
update_recordset custTable
setting Blocked = CustVendorBlocked::All;
// More logic, more updates, maybe a call to another method
ttscommit;
Huge transaction scopes make it harder to debug and can cause long locks.
Instead:
• Keep your transaction scope as small and focused as possible.
• Avoid calling methods that you don’t fully control from inside ttsbegin / ttscommit.
ttsbegin;
// Only the updates that really need to be in this transaction
update_recordset custTable
setting Blocked = CustVendorBlocked::All
where custTable.CustGroup == 'DEFAULT';
ttscommit;
// Non-transactional work goes outside
info("Customers have been blocked for group DEFAULT.");
Quick checklist:
• Do you really need all of this inside a single transaction?
• Is there any external call (web service, file IO, long calculations) inside ttsbegin? If yes, try to move it outside.
───
Tip 3 – Use QueryBuildDataSource instead of string-based queries
String-based queries are fragile and harder to maintain. For example:
common = null;
select * from common where common.TableId == tableNum(SalesTable) &&
common.RecId == _recId;
Or worse, dynamic SQL strings built with strFmt.
Instead, use Query / QueryBuildDataSource:
Query query = new Query();
QueryBuildDataSource qbds;
QueryRun queryRun;
SalesTable salesTable;
;
qbds = query.addDataSource(tableNum(SalesTable));
qbds.addRange(fieldNum(SalesTable, RecId)).value(int642str(_recId));
queryRun = new QueryRun(query);
if (queryRun.next())
{
salesTable = queryRun.get(tableNum(SalesTable));
// do something
}
Benefits:
• Strongly typed: if you rename a field, the compiler helps you.
• Easier to debug and extend (adding joins, ranges, etc.).
• Plays better with the standard patterns used in D365 F&O.
───
Tip 4 – Use info(), warning(), and error() wisely (and clean up logging)
It’s tempting to sprinkle info() messages everywhere while debugging:
info("Step 1");
info("Step 2");
info(strFmt("Value is %1", someValue));
This works locally but becomes noisy in shared environments.
A better pattern:
• Use structured messages during development.
• Remove or downgrade them before moving to higher environments.
• For troubleshooting-heavy areas, consider designing a small “debug flag” pattern.
Example:
public static void runBatchJob(boolean _debug = false)
{
if (_debug)
{
info("Batch job started.");
}
// Logic here ...
if (_debug)
{
info("Batch job finished.");
}
}
}
}
Then you can call:
MyClass::runBatchJob(true); // debug mode
// or
MyClass::runBatchJob(false); // normal mode
Why it helps:
• Reduces noise in production
• Still gives you a quick way to get more info when needed
• Makes your intent clearer to other developers
───
Tip 5 – Make your code extension-friendly from the start
Even if you’re writing code in your own model, try to think “how would someone extend this later?”. Small changes in structure can save a lot of refactoring.
Instead of:
public static void processSalesOrder(SalesTable _salesTable)
{
// Validation
if (_salesTable.SalesStatus != SalesStatus::Backorder)
{
error("Only Backorder orders are supported.");
}
// Main logic
// ...
}
Consider breaking it into smaller methods that can be extended via Chain of Command (CoC):
public static void processSalesOrder(SalesTable _salesTable)
{
MySalesProcessor::validateSalesOrder(_salesTable);
MySalesProcessor::runProcessing(_salesTable);
}
protected static void validateSalesOrder(SalesTable _salesTable)
{
if (_salesTable.SalesStatus != SalesStatus::Backorder)
{
error("Only Backorder orders are supported.");
}
}
protected static void runProcessing(SalesTable _salesTable)
{
// Main logic
}
Now, another developer (or future you) can extend validateSalesOrder or runProcessing without rewriting everything.
Key idea:
Smaller, single-purpose methods are easier to read, test, and extend in D365 F&O.
───
Conclusion
These 5 tips are not advanced patterns or deep framework internals – they’re small, practical habits that can make your X++ code:
• Easier to maintain
• Safer to run in shared and production environments
• Friendlier for other developers (and your future self)
To recap:
1. Select only the fields you need with while select.
2. Keep ttsbegin / ttscommit scopes small and focused.
3. Prefer QueryBuildDataSource over string-based queries.
4. Use info() / warning() / error() intentionally and cleanly.
5. Design your code to be extension-friendly from the start.
In a future post, I’ll go deeper into debugging X++ batch jobs and patterns for handling exceptions cleanly in D365 F&O.
If you have your own X++ tips or patterns that save you time, feel free to share them in the comments.
Disclaimer
The content on this blog is provided for informational and educational purposes only. Dynamics 365 EZ makes no representations or warranties of any kind, express or implied, about the accuracy, completeness, reliability, suitability, or availability of the information contained herein.
Any reliance you place on such information is strictly at your own risk. Dynamics 365 EZ shall not be liable for any errors or omissions, or for any losses, injuries, or damages arising from the use of, or reliance on, any information displayed on this site.
Comments
Post a Comment
Please be patient and polite