Friday, July 24, 2026

Object Oriented Programming - OOP

In Dynamics 365 Finance and Operations (F&O), Object Oriented Programming (OOP) is used frequently in the standard application code base and is fundamental to how the business logic is applied in the system.

The basics

As the name implies, OOP makes uses of objects in programming. In X++, and also other programming languages, objects are designed with classes. These classes they represent a real life objects. Just like real life objects, they also have properties or attributes and they can do certain things.

The image below illustrates an OOP example of three different types of pets. It consists of a hierarchy with a base class and three different sub types (child classes) inheriting properties and methods from the base (parent) class.

Note
A key principle of OOP is inheritance. In fact, inheritance is a core design principle in the business logic / code base as well as the actual data of the system in F&O.

Image 1: Class design.

Base class

The base class is called Pet because it is generic and contains all the property definitions and functionality for all pets. In our example, we have three methods defined in the base class: species, legs and eyes. These are some basic attributes of all pets.

Note that the Pet base class does not implement the two abstract methods. This is by design because we want each child class to implement these methods specifically. Hence the abstract keyword in the method declarations. The Pet class declaration also contains the abstract keyword. This means that Pet class to be instantiated directly, only the child classes can be instantiated. This is also by design.

The third method called eyes, is implemented in the base class because most pets have two eyes. But, if it just so happens that there are pets with more than 2 eyes, like spiders, the child class of that pet can implement the eyes method and specify how many eyes there are.


public abstract class Pet
{
    //the species of the pet e.g. mammal, reptile, bird etc.
    public abstract str species()
    {
    }

    //the number of legs of the pet.
    public abstract int legs()
    {
    }

    //the number of eyes of the pet.
    public int eyes()
    {
        return 2;
    }

}

Base class.

Sub types

The sub types or child classes inherit properties and methods from the Pet base class using the extends keyword. They contain the pet specific properties and or methods. Each sub type class implements the species and legs methods.

Note
Because these two methods are declared abstract in the base class, the compiler enforces the implementation of the abstract methods in each child class or else will raise a compile error.


public class Pet_Bird extends Pet
{
    public str type()
    {
        return 'Bird';
    }

    public int legs()
    {
        return 2;
    }
}

public class Pet_Dog extends Pet
{
    public str type()
    {
        return 'Mammal';
    }

    public int legs()
    {
        return 4;
    }
}

public class Pet_Lizard extends Pet
{
    public str type()
    {
        return 'Reptile';
    }

    public int legs()
    {
        return 4;
    }
}

Runnable class

To demonstrate OOP with our pet example, the runnable class below creates an object of each pet class and then calls the showMessage method.

The showMessage method calls the species, legs and eyes methods of the Pet object. So, which method gets called in which class? Well, that depends on the child class implementation of any of the methods in the base class.

None of the child classes implement the eyes method but all of child classes implement the species, legs methods. The showMessage method has single argument of the type Pet which is the aprent class of all the child classes. This is one of the advantages of OOP and this is how we can use a single variable type for the different child classes.


public class PetDemoJob
{
    public static void main(Args _args)
    {
        //variable of type Pet
        Pet pet;
        
        //create object of type Pet_Dog
        pet = new Pet_Dog();
        
        //show message
        PetDemoJob::showMessage(pet);

        //create object of type Pet_Bird
        pet = new Pet_Bird();

        //show message
        PetDemoJob::showMessage(pet);

        //create object of type Pet_Lizard
        pet = new Pet_Lizard();

        //show message
        PetDemoJob::showMessage(pet);
    }

    public static void showMessage(Pet _pet)
    {
        info(strFmt('A %1 has %2 eyes and %3 legs.',
            _pet.species(),
            _pet.eyes(),
            _pet.legs()));
    }

}

Running the class

Let's run our class using SysClassRunner in F&O to see what happens.

The system calls the main method our runnable class and three infolog messages are displayed:

Image 2: The infolog messages.

  • A Reptile has 2 eyes and 4 legs
  • A Bird has 2 eyes and 2 legs
  • A Mammal has 2 eyes and has 4 legs

This demontrates that the species and legs methods of each different pet object was called, but for all pets the eyes method in the base class was called.

OOP in F&O

Okay, so that's all fine and dandy but how is does this relate to F&O? How is OOP implemented in F&O with real life objects?

The Bank class

Let's take a simple example, the Bank class. The Bank base class represents a bank account in real life. In this case a bank account is not tangible but classes can also represent intangible objects too. Like our Pet base class, it contains properties and methods. Some of these methods are overwritten in the child classes (sub types) depending on the local bank account rules in the related country. For simplicity and to keep this article as short as possible, the class desing diagram below contains just two of the Bank class methods.

The base class contains the properties and methods and all child classes that extend the Bank class, inherit those properties and methods. It is no surprise but bank account numbers are different per country. So, how is that handled with OOP? This is the power of OOP, each child class implements the base class methods differently. This provides isolation en encapsulation of the code. Let's take the checkBankAccount method declared in the base class. As the name indicates, it's purpose is to check the bank account number and returns a BOOLEAN value if the bank account number is valid or not. This is validation is different in different countries.

Image 3: Bank class design (simplified).

The Bank class

The Bank class is the base or parent class of all the Bank sub types or child classes. The class contains more methods but I have included only two methods to keep things simple. These two methods in the Bank_NL, Bank_FR and Bank_ES classes are implemented differently because of the differences in bank accounts in these respective countries.


public class Bank
{
    public boolean checkBankAccount(BankAccountMap _bankAccountMap)
    {
        boolean ret = true;

        ret = ret && this.checkBankAccountNum(_bankAccountMap.AccountNum);

        return ret;
    }

    public boolean checkBankAccountNum(BankAccount _bankAccount)
    {
        boolean ret = true;
        return ret;
    }
}

Bank_NL class

The checkBankAccountNum method is overwritten and implemented according to the NL bank account format. It checks if the format is correct and is valid according to the specifications.


public class Bank_NL extends Bank
{
    public boolean checkBankAccount(BankAccountMap _bankAccountMap)
    {
        return this.checkBankAccountNum(_bankAccountMap.AccountNum);
    }

    public boolean checkBankAccountNum(BankAccount _bankAccount)
    {
        GlobalizationInstrumentationHelper::featureRun(GlobalizationConstants::FeatureReferenceNL00016, funcName());

        boolean ok = true;

        if (strLen(_bankAccount) == 9)
        {
            if (strRem(subStr(_bankAccount,1,9), '1234567890') != '')
            {
                error(strfmt("@SYS86814", _bankAccount));
                ok = false;
            }
        }

        if (ok)
        {
            // "P" or "G" Stands for a Giro Number (Post Bank)
            if (strUpr(subStr(_bankAccount,1,1)) == 'p' || strupr(subStr(_bankAccount,1,1)) == 'g')
            {
                ok = this.giroTest(_bankAccount);
            }
            else
            {
                ok = this.bankTest(_bankAccount);
            }
        }

        return ok;
    }
}

Bank_FR class

The checkBankAccountNum method is overwritten and implemented according to the FR bank account format. It checks if the format is correct and is valid according to the specifications.


public class Bank_FR extends Bank
{
    public boolean checkBankAccount(BankAccountMap _bankAccountMap)
    {
        boolean ret = this.checkBankRegNum(_bankAccountMap.RegistrationNum);
        if (ret)
        {
            ret = this.checkBankAccountNum(_bankAccountMap.AccountNum);
        }

        if (ret)
        {
            ret = this.checkControlText(_bankAccountMap.RegistrationNum, _bankAccountMap.AccountNum);
        }

        return ret;
    }

    public boolean checkBankAccountNum(BankAccount _bankAccount)
    {
        GlobalizationInstrumentationHelper::featureRun(GlobalizationConstants::FeatureReferenceFR00013, funcName());

        if (!_bankAccount)
        {
            return true;
        }

        if (strLen(_bankAccount) != 13)
        {
            return checkFailed(strFmt("@SYS74835", fieldPName(BankAccountMap, AccountNum)));
        }

        return true;
    }

    protected boolean checkControlText(BankRegNum _bankRegNum, BankAccount _bankAccount)
    {
        const str digits = '0123456789';

        str 23 controlTxt = _bankRegNum + _bankAccount;

        int idx = strNFind(controlTxt, digits, 1, 99999);
        while (idx)
        {
            controlTxt = strPoke(controlTxt, this.accountChar2Num(subStr(controlTxt, idx, 1)), idx);

            idx = strNFind(controlTxt, digits, idx + 1, 99999);
        }

        return true;
    }
}

Bank_ES class

The checkBankAccountNum method is overwritten and implemented according to the ES bank account format. It checks if the format is correct and is valid according to the specifications.


public class Bank_ES extends Bank
{
    public boolean checkBankAccount(BankAccountMap _bankAccountMap)
    {
        return this.checkBankAccountNum(_bankAccountMap.AccountNum);
    }

    public boolean checkBankAccountNum(BankAccount _bankAccount)
    {
        GlobalizationInstrumentationHelper::featureRun(GlobalizationConstants::FeatureReferenceES00013, funcName());

        int digit, multiply;
        int sum1 = 0;
        int sum2 = 0;

        int i = strLen(_bankAccount);
        if (!i)
        {
            return true;
        }

        if (i != 20)
        {
            return checkFailed(strFmt("@SYS54162", 20));
        }

        for (i = 0; i < 20; i = i)
        {
            do
            {
                i++;
                digit = char2Num(_bankAccount, i) - char2num('0', 1);
                if (digit < 0 || digit > 9)
                {
                    return checkFailed("@SYS97947");
                }
            }
            while (i == 9 || i == 10);

            switch ((i < 9 ? i + 2 : i) mod 10)
            {
                case 0: multiply = 6; break;
                case 1: multiply = 1; break;
                case 2: multiply = 2; break;
                case 3: multiply = 4; break;
                case 4: multiply = 8; break;
                case 5: multiply = 5; break;
                case 6: multiply = 10; break;
                case 7: multiply = 9; break;
                case 8: multiply = 7; break;
                case 9: multiply = 3; break;
            }

            if (i < 9)
            {
                sum1 += digit * multiply;
            }
            else
            {
                sum2 += digit * multiply;
            }
        }

        if (!this.checkDigit(subStr(_bankAccount, 9, 1), sum1))
        {
            return checkFailed(strFmt("@SYS54163",1));
        }

        if (!this.checkDigit(subStr(_bankAccount, 10, 1), sum2))
        {
            return checkFailed(strfmt("@SYS54163",2));
        }

        return true;
    }
}

Testing OOP with the Bank child classes

The code below is a runnable class and in the first part, it creates an instance of the Bank_NL class and calls the checkBankAccountNum method. Because the bank account number passed to the Bank_NL object is valid and the code in the checkBankAccountNum method of the Bank_NL class is executed, it shows a message "Bank account is valid".

In the second part, it creates an instance of the Bank_FR type and calls the checkBankAccountNum method. Because the bank account number passed to the Bank_FR object is a NL bank account number and the code in the checkBankAccountNum method of the Bank_FR class is executed, it shows a message "Bank account is invalid". This is because the first thing that is checked if bank account number length is 13. In our example it is 10, so that's why the method returns false and that's why the message is shown.

public class BankDemoJob
{
    public static void main(Args _args)
    {
        //variable of type Bank
        Bank bank;
        
        //create object of type Bank_NL
        bank = new Bank_NL();
        
        //check bank account number using a NL bank account number with the Bank_NL object
        if (bank.checkBankAccountNum('0417164300'))
        {
            info('Bank account is valid');
        }
        else
        {
            error('Bank account is invalid');
        }


        //create object of type Bank_FR
        bank = new Bank_FR();
        
        //check bank account number using a NL bank account number with the Bank_FR object
        if (bank.checkBankAccountNum('0417164300'))
        {
            info('Bank account is valid');
        }
        else
        {
            error('Bank account is invalid');
        }
        
    }

}

Runnable class.

Running the class

Let's run our class above using SysClassRunner in F&O to see what happens. Three different types of infolog messages are displayed. One info, one warning and one error is displayed. This is expected because we checked a NL bank account with the FR class.

Image 4: The infolog messages.

  • Bank account is valid
  • The length of Bank account number should be 13 characters including control key.
  • Bank account is invalid

The constructor method

For the sake of simplicity, in our two examples, I create an intance of each class expliticly like this bank = new Bank_NL();. However, in F&O this is usually done properly using a construct method in the base class. This method "constructs" an instance of the related sub type (child) class depending on the argument passed to the method. This is best practice and I definitely recommend that you follow this pattern in your own designs.

Below is the construct method of the Bank class. It has a single argument: ISO country code. The data type used is specific for the ISO country codes with a length of 2 characters. The country code passed to the construct method could be "NL", "FR" or "ES" or any of the supported country codes in the construct method. The method checks which ISO country code is passed in and then creates the related instance of the sub type (child) class for that country. So passing "NL" will return an instance of the Bank_NL class.


public static Bank construct(LogisticsAddressCountryRegionISOCode _bankLocationISOCode)
{
	#ISOCountryRegionCodes

	switch (_bankLocationISOCode)
	{
		case #isoDK :
			return new Bank_DK();

		case #isoCA :
			return new Bank_CA();

		case #isoFI :
			return new Bank_FI();

		case #isoFR :
			return new Bank_FR();

		case #isoNO :
			return new Bank_NO();

		case #isoES :
			return new Bank_ES();

		case #isoCH :
			return new Bank_CH();

		case #isoBE :
			return new Bank_BE();

		case #isoNL :
			return new Bank_NL();

		case #isoMX :
			return new Bank_MX();

		case #isoUS :
			return new Bank_US();

		case #isoIT :
			return new Bank_IT();

		// 
		case #isoJP :
			return new Bank_JP();
		// 

		// 
		case #isoEE :
			return new Bank_EE();
		// 

		case #isoBR :
			return new Bank_BR();

		default :
	}
	return new Bank();
}

Below is an example in the standard application of how the system calls the construct method of the Bank class. The code passes an ISO country code and then class the checkBankAccountNum method. The ISO country code from the bank account of the worker bank account table record is used.

This is a simple but good example of OOP in F&O and there are endless examples of this kind of object construction in the application. This is a fundamental code design principle in F&O that drives the business logic in many different ways. If you browse the application source, you will find many examples.

From method HcmWorkerBankAccount_onValidatedWrite in class HcmWorkerBankAccountEventHandler_AppSuite


if (workerBankAccount.AccountNum)
{
    Bank bank = Bank::construct(SysCountryRegionCode::locationCountryInfo(workerBankAccount.Location));
    isValid = isValid && bank.checkBankAccountNum(workerBankAccount.AccountNum);
}

Saturday, July 4, 2026

Set based operations for MUCH better performance

In Dynamics 365 Finance and Operations (F&O), updating or querying data is usually handled in X++ using a loop construct - for example, a while select statement or the QueryRun class (which also uses the same loop construct). Looping through rows of data and performing an operation on each row is very common in F&O and is also known as "row-by-row" operations.

Row-by-row operations are very flexible because, for each row, you can perform custom logic on the data. For example, you can loop through all customers and check their current balance, or loop through all customers and check their invoice aging to see if they have any outstanding amounts older than 60 days.

However, row-by-row operations handle each record separately and apply business logic to every single row. This adds significant overhead, which can make processing large datasets painfully slow. Performance also depends heavily on the complexity of the business logic. Complex logic often involves multiple calls to other methods or business logic layers, which can quickly create deep nested calls.

Set based operations

So, how can we handle large datasets and get good performance? With set based operations like insert_recordset, update_recordset and delete_recordset.

Set-based operations are executed with a single SQL statement and a single round trip to the SQL Server database. This is why they are so fast. Even if millions of records are affected, only one SQL statement is sent to the backend SQL Server database.

Falling back to row-by-row operations

The set based operation will fall back to a row-by-row operation in the following cases if:

  • One of the data methods has been overriden on the target table:
    • insert()
    • update()
    • delete()
  • The database log in enabled on the target table
  • There is a delete action on the target table and you are using delete_from
  • There is an alert setup on the target table
  • The ValidTimeStateFieldType table property not equal to None

For example, let's say you are inserting data into a table using the insert_recordset statement, but the insert() method of the target table contains business logic. In this case, the system will automatically fall back to row-by-row operations. This means each inserted row will result in a separate SQL statement and round trip to the database server. As a result, there is significant performance overhead, and you gain little to no performance benefit compared to a regular while select loop.

Forcing set based operations

If for one or more of the reaons above, the set based operation is falling back to a row-by-row operation and you still want to force a set-based operation. You can use the .skip... methods on the target table buffer.

For example:.skipDataMethods(true) can be used if one of the insert(), update() or delete() methods have been overridden.

See the table below on how to handle each case. Although it was originally written for AX 2012, it remains fully relevant for Dynamics 365 Finance and Operations (F&O).

  DELETE_FROM UPDATE_RECORDSET INSERT_RECORDSET ARRAY_INSERT Use ... to override
Non-SQL tables Yes Yes Yes Yes Not applicable
Delete actions Yes No No No skipDeleteActions
Database log enabled Yes Yes Yes No skipDatabaseLog
Overridden method Yes Yes Yes Yes skipDataMethods
Alerts set up for table Yes Yes Yes No skipEvents
ValidTimeStateFieldType
table property not equal to None
Yes Yes Yes Yes Not applicable

Only the table above sourced from: Microsoft Learn - Maintain Fast SQL Operations

Conclusion

Set based operations are excellent for performance on large datasets but fall back to row-by-row operations if the target table insert, update and delete methods have been overridden. Or for one of the other reasons as mentioned above.

Important: carefully consider if you can skip any data methods on the target table because you risk breaking the data integrity of the target table.

Sunday, May 24, 2026

Strings in X++ when to use " and when to use '

I have seen a lot of code of a lot of developers during the past two decades. There is however one thing that even more seasoned developers keep getting wrong when it comes to using strings in code.

I see mixed uses of "" and '' as demonstrated in the two examples below. Both are programatically correct and are accepted by the compiler and both work fine.


public static str callerBufferIsMandatory()
{
    return "The form has been called incorrectly. A caller buffer is required.";
}

Example 1 of a string with "".


public static str callerBufferIsMandatory()
{
    return 'The form has been called incorrectly. A caller buffer is required.';
}

Example 2 of a string with ''.

Conclusion

Basically, if your string is used in the use interface towards the end user, you use "" for the string. Best practice is to use a label but this is another topic all together and not covered here.

If your string is not used in the user interface and is a basically a constant that won't change and will never be converted to a label, you use '' for the string. Like the example below. The name is a constant and will never change.


public static void writeLog(str _text)
{
    new SysExceptionLog().writeEntry(Exception::Info, _text, 'MyCustomizationName');
}

Saturday, April 11, 2026

Generating deep links

When a form is opened in Dynamics 365 Finance and Operations F&O the form displays all records or sometimes applies a filter if a default view is applied. It doesn't drill down to a single record. Sometimes you need a sharable URL link to drill down to a single record. E.g. a customer or vendor or it could be anything really. With deep links, you can drill down to a specific record in a form.

When the system generates a deep link of a specific record, it adds detailed query parameters to the URL. The extra parameters are readable in the unencrypted version above. The datasource name, table field name and filter value are specfied for the form to filter on. If the form menu item has the "Allow root navigation" and "Copy caller query" properties set to Yes, then the form will recognize the extra parameters and apply the filter automatically.

Unencrypted deeplink:

https://usnconeboxax1aos.cloud.onebox.dynamics.com/?cmp=USMF&prt=initial&mi=display:CustTable&q={"Parameters":[{"DataSource":"CustTable","FieldValues":[{"Field":"AccountNum","Value":"US-002"}]}]}

Encrypted deeplink:

https://usnconeboxax1aos.cloud.onebox.dynamics.com/?cmp=usmf&prt=initial&mi=display:CustTable&q=BgAAAKtjn9amNj9Ibe1SID5uUafqfbbsq2Of1qY2P0ht7VIgPm5Rp%2bp9tuymMl72yjSV2bDEqxFBm%2blUyTB3uTX5155lA1AAn%2fzUohRn52fhmJ7y%2fy6kGLVzYTxMde0xqPliZnUU28hblRZ%2b4CxkpeT1576UN2n89XUANdpPxJ4H2mcc0O9WCOdpgtWGEr5nC8hHV%2bvV4u4Pj5DVhdrcWyg7ipSdPfVtNtPO2EmMf2nQQMTSePLlPvKJq8AlHqakLjdeN74lqUCxqroB8l0VgiXfYVvUe9NXs8hwnX6MrP2EAaUdd4dzE7DaNJNuxQnRXbu2BbO0QaUyBERPZ0IofUO3M8G5M9JpePw7nBMvBrCnixIGg4tZMAoPK0cjEAycIvYzG1A4be%2bhTWe1odR6WtnF4B3J8syydPbHUkIqWlnltkJCUToiQ4FiOfhScK%2bzHsyv2%2b8WLpLG2fxrIJGR5DrBusC6PLy%2bWHfZvvHQnUfMZnbqknj6%2b%2fG2qOcE%2fK%2fP%2baAkbfxDeaLTjrgHx2Sbe6KB4eKIi%2fA8bb8VidM29KkXkjXfAWRe%2b6D%2fognEL0kcy1nnYHZKs%2bOEl32ofeMT7REdfmSVLPP7tonH1NgKo7r%2fA9qIR%2b11%2bzbAXGfc0U76ZphIlylscjXMkZGceDK5%2b8mXgHFjF8vtamNaH1uH%2byONKCYsDCq4kI4W%2bfM7XvTulFVWq7hWJb2hnPAsCkdJuWKdgWbgOs4ACSQvH3f63ZQtysryLH0yAsa8TQdmXYJ610xmskt0a6K4hC7ZHFHdfGPYyICwSza7wDH6mZJuWlnkdSKQVixmCV1HRkyjkVTg5fYTXZyfeWGHVpwf%2bBC3%2fp7NyD7wFsvP3im6S1Cdgu5eLIwVKRlZyaSj37SoylD5XwPQmmQXURi54I2f

The encrypted version hides the query information so it's safe to be used externally. The system encrypts the extra query parameters and this results in a unreadable and safe version of the URL.

Notes:

  • The menu item used must have the following properties enabled:
    • Allow root navigation = Yes
    • Copy caller query = Yes
  • The UrlUtility::getUrl() function does not work in batch. Do a search on an alternative way to get the system URL in batch.
  • Use the SysEntityNavigation data entity to create deep links from external applications.
  • The length of deep links URL are more than 1000 characters long and can vary. The length of the above deep link is 1063 characters.
  • Only use unencrypted deeplinks for testing as they will result in the error message below.
The specified record query failed to apply and the can't be opened.

Error message: The specified record query failed to apply and the can't be opened.



Full code example


public class MyDeepLinkTest
{
    public static str createDeepLinkUrl(MenuItemName _menuItemName, DataSourceName _keyFilterDataSourceName, FieldName _keyFilterFieldName, str _keyFilterFieldValue)
    {
        var generator = new Microsoft.Dynamics.AX.Framework.Utilities.UrlHelper.UrlGenerator();
        var currentHost = new System.Uri(UrlUtility::getUrl());

        generator.HostUrl = currentHost.GetLeftPart(System.UriPartial::Authority);
        generator.Company = curext();
        generator.MenuItemName = _menuItemName;
        generator.MenuItemType = MenuItemType::Display; //avent: display only.
        generator.Partition = getCurrentPartition();
        generator.EncryptRequestQuery = true; //avent: set to false for testing only.

        //repeat for each datasource to filter
        var requestQueryParameterCollection = generator.RequestQueryParameterCollection;
        requestQueryParameterCollection.AddRequestQueryParameter(_keyFilterDataSourceName, _keyFilterFieldName, _keyFilterFieldValue);

        System.Uri fullURI = generator.GenerateFullUrl();
        
        return fullURI.AbsoluteUri; //to get the encoded URI
    }

    public static void main(Args _args)
    {
        str link = MyDeepLinkTest::createDeepLinkUrl(menuItemDisplayStr(CustTable),
            formDataSourceStr(CustTable, CustTable),
            fieldstr(CustTable, AccountNum),
            "US-002");

        Dialog dlg = new Dialog();
        dlg.addField(extendedTypeStr(Notes)).value(link);
        dlg.run();
    }

}


Result of running above test code to create a deep link

Simple dialog with a single field containing the deep link.

Using the deep link

Dynamics 365 Finance and Operations customer form filtered to the customer specified in the deep link.

Edit: (untested) alternative method for getting the host URL. Source community.dynamics.com.

IApplicationEnvironment env = EnvironmentFactory::GetApplicationEnvironment();
str currentUrl = env.Infrastructure.HostUrl;
System.Uri currentHostUrl = new System.Uri(currentUrl);
 
UrlGenerator urlGenerator = new UrlGenerator();
urlGenerator.HostUrl = currentHostUrl.GetLeftPart(System.UriPartial::Authority);

Tuesday, March 10, 2026

Getting stack trace information using xSession::xppCallStack()

From the battle hardened trenches of the F&O development front lines, I present to you a small code nugget that will help you save time and troubleshoot your custom code.

I am actually talking about the standard system method xSession::xppCallStack(). It retieves the current execution call stack. If combined with an error message, it provides useful information for troubleshooting custom code running in production.

The code example below, is just an example of using the standard system method xSession::xppCallStack() to retrieve the current execution call stack of the code being executed. Having the execution call stack information saves a lot of time because you can see where error message originates from.


public static void throwError(str _error)
{
	container callStack = xSession::xppCallStack();
	conDel(callStack, conLen(callStack), 1); //remove last one.
	throw error(_error + '\n' + con2Str(callStack, '\n'));
}

Thursday, February 5, 2026

How to effectively detect if data has changed using RecVersion

Sometimes there is a requirement to effectivle detect if data has changed in a table. The first thing that might come to mind is, why not just use the "ModifiedDateTime" field of the data? Well, that's perfectly fine if don't have a lot of data in your table.

But, what if there is a lot of data in our table? Like millions of records? Filtering on an unindexed field like "ModifiedDateTime" and with a lot of data, will force the database server to scan all the records in the table. The database server has to do this to determine which records are modified after the specified date and time. Table scans on large tables is not effective and painfully slow because it's not using any of the table indexes.

RecVersion

So, what's the solution? How do we effectively detect which rows have changed in our data? By using the system field RecVersion. Every table in Dynamics 365 Finance and Operations (F&O) has a field called RecVersion.

Every time a record is changed in a table, the RecVersion of the record is updated by the system to a new value, or version. The RecVersion field contains the record version of the record.

But, there is a small problem, how do know what the previous version of RecVersion was before the data was changed? Well, we don't. But luckily there is a simple way so solve this. By creating a custom table with the RecId and RecVersion of our data.

Steps

  • Create a custom table with a reference RecId and RecVersion field to our data. Field names: RefRecId and RefRecVersion
  • In code, create a method to create a "snapshot" of the RecId and RecVersion values of the data. Use insert_recordset to copy the data.
  • Query your table with a join to the custom table. Join on YourTable.RecId and YourTable.RecVersion and SnapShotTable.RefRecId and SnapShotTable.RefRecVersion

To get all the data from the target table that has changed, join the two tables on RecId and RecVersion. The records where RecVersion is different, has changed.

Sunday, January 25, 2026

Base64 encoding and decoding

What is Base64 encoding and decoding? On a very high level: it is a method of converting binary data to ASCII text and the ASCII text back again to binary. There are many articles online on how this works technically but this article will explain the basics and how Base64 encoding and decoding is used in Dynamics 365 Finance and Operations (F&O).

For example, if you attach a spreadsheet or other binary file like a PDF document to an email. The binary file is converted to ASCII text using Base64 encoding and is embedded in the message body of the email as text. When the email message is received by the recipient, the email application used to view the email message, detects that there is a file attached embedded as text and converts the text back to binary so that the attachment can be downloaded as a file. Most email applications support previewing attachments, but will have to convert the attachments from text to binary on the fly before being able to display the contents of the file.

So, how does Base64 encoding and decoding relate to F&O? It is frequently used in the standard application and also by developers when using the SysOperation framework just to mention just a few examples.

Code example

In a SysOperation contract class, the SysOperation framework allows a Query object to be used to query data for the operation. Because the related operation can also be run in "batch", in other words in "unattended" mode without an user interface, the Query object of the class needs to be saved to the database so that the operation can be executed by the batch framework and use the exact same query object that was specified by the user when adding the operation to batch. In other words, the system saves all query filters, joins and other changes to the query and will then use your exact query when executing in batch.

An instance of an class object cannot be saved to the database. So, how does the system save the object to the database? By using Base64 encoding and decoding. The Query object of the contract class is serialized to a binary object using the "pack" method of the Query class. This method returns a binary representation of the query in a container type. All class variables of the Query object are serialized and converted to a binary object (container). The resulting binary object of the Query object is then Base64 encoded to text. This text can be saved to the database as a regular text field.

When the Query object instance is required by the SysOperation framework, the process is reversed. The text is Base64 decoded to binary, the original Query object with all the specified filters and other changes can then be instantiated by using the binary data to construct a Query object.

1. Class variable for the Base64 encoded text.

2. Code that converts the text back to binary object.

3. Code that converts the binary object to text.

Sunday, January 18, 2026

Using Boolean OR to make where clause fields optional

Sometimes you have a custom "while select" statement and you need to have one or more of the where clause fields in the statement to be optional. In this example, I will use a simple method to display the customer account and group for customers.

The method below displays the customer accounts for customers and uses the _custGroup argument to filter customers that match the customer group. The method works as expected but there is one drawback: it can only show customers for the specified customer group.

So, what if we want to show info for all customers with this method? Can't we just call the method and pass an empty string? The answer is yes, that's possible but will not work as one would expect. The problem with this is, that if you call the method and pass an empty string for the customer group, the while select statement will return all customers where the customer group is empty. Since the customer group field is mandatory, and all customers have a customer group, the query will return nothing.

Show customer info method


//show customer info with mandatory argument
public static void showCustomerInfo(CustGroupId _custGroup)
{
    CustTable custTable;

    while select custTable
        where custTable.CustGroup == _custGroup
    {
        info(strFmt("%1 - %2", custTable.AccountNum, custTable.CustGroup));
    }

}

How do we solve this problem? Your first thought might be to simply duplicate the while select statement in the method for each argument used in the where clause. In this example two statements, one statement with, and another statement whithout the customer group where clause. And simply add an "if" statement to check if the customer group argument has a value or not. Yes, this will work but this is definitely not the way to go. The reason is because if you have a large while select statement with many lines and a method with multiple arguments. The code will be hard to read and error prone when modifications are done to it.

Optional argument

So, what is the solution? By using the Boolean OR operator in the while select statement. The where clause for customer group is extended with an Boolean OR to contain two parts. The first part of the Boolean OR evaluates the method customer group argument variable, if this part of the Boolean OR evaluates to TRUE, the second part of the OR is simply ignored, effectively ignoring the where clause part for customer group in the second part. However, if the first part of the Boolean OR statement evaluates to FALSE, the second part of the boolean OR is then evaluated which effectively then applies the where clause of the customer group to the statement. Making the where clause optional and having compact and readible code.


//show customer info with optional argument
public static void showCustomerInfo(CustGroupId _custGroupId = '')
{
	CustTable custTable;

	while select custTable
		where (_custGroupId == '' || custTable.CustGroup == _custGroupId)
	{
		info(strFmt("%1 - %2", custTable.AccountNum, custTable.CustGroup));
	}

}

Complete example

You can run the class using the SysClassRunner class.
e.g. https://[your-environment].operations.eu.dynamics.com/?mi=SysClassRunner&cls=MyTestJob


public final class MyTestJob
{
    public static void main(Args _args)
    {
        CustGroupId custGroup;
        Dialog dlg = new Dialog('Enter customer group');
        DialogField fldCustGroup = dlg.addField(extendedTypeStr(CustGroupId));

        if (dlg.run())
        {
            custGroup = fldCustGroup.value();
            MyTestJob::showCustomerInfo(custGroup);
        }
    }

    //show customer info with optional argument
    public static void showCustomerInfo(CustGroupId _custGroup = '')
    {
        CustTable custTable;

        while select custTable
            where (_custGroup == '' || custTable.CustGroup == _custGroup)
        {
            info(strFmt("%1 - %2", custTable.AccountNum, custTable.CustGroup));
        }
    }

}

Tuesday, December 30, 2025

The SysOperation framework

The SysOperation framework is used to implement business logic and is widely used in the standard application. It is just one of the many frameworks in the application and it simplifies the creation of tasks that can run in the background.

Like all frameworks in F&O, the SysOperation framework provides rich functionality out of the box, and lets developers focus on coding business logic. Developers do not have to worry about how the business logic is executed, that's all taken care of by the SysOperation framework!

When implementing business logic by using the SysOperation framework, I recommend creating a minimum of three classes. Let's break that down a bit, I'll explain the function of each class.

Data contract class

The data contract class contains the query and parameters required by the operation. When an operation is started interactively, the user is presented with a dialog that allows them to set some parameter values before the operation is executed. After setting the values, the user can either confirm and run the operation or cancel the operation. If the operation is started programmatically, the data contract is populated in code and the dialog is not shown.

Controller class

The controller class manages the execution of the operation. It does not contain any business logic itself and typically provides an entry point (a static main method) that allows the operation to be started from a menu item. The controller also tells the SysOperation framework which method to execute.

Business logic class

The business logic class contains the business logic of the operation. My custom business logic classes always have a single purpose. For example, update the status of data from one value to another.

Basic example

The following basic example simply displays the account number of all customers in the current legal entity. It demonstrates a minimal implementation of the SysOperation framework.

Data contract class


[DataContractAttribute]
public final class MySysOpDemoContract
{
    private boolean showName;
    private str packedQuery;

    [DataMemberAttribute, SysOperationLabel("Show customer name")]
    public boolean parmShowName(boolean _showName = showName)
    {
        showName = _showName;
        return showName;
    }

    public Query getQuery()
    {
        return new Query(SysOperationHelper::base64Decode(packedQuery));
    }

    public void setQuery(Query _query)
    {
        packedQuery = SysOperationHelper::base64Encode(_query.pack());
    }

    [DataMemberAttribute, AifQueryTypeAttribute('_packedQuery', queryStr(CustTable))]
    public str parmQuery(str _packedQuery = packedQuery)
    {
        packedQuery = _packedQuery;
        return packedQuery;
    }
}

As mentioned before, the data contract class contains the query and parameters for the operation. It also includes other variables and methods required by the system to function properly. These methods can be used by the controller class during runtime. Don’t worry too much about the details for now.

[DataContractAttribute] - This attribute is required on the data contract class so that the system knows it should be serialized and deserialized.

private boolean showName - Custom class variable that determines whether the business logic should display the customer name.

private str packedQuery - The system serializes the operation query to a string and stores it in a class variable (required).

[DataMemberAttribute] - Required attribute on the parm method to ensure the showName field is serialized and displayed in the runtime dialog. Without it, the field is excluded from serialization and will not appear in the dialog.

[SysOperationLabel("Show customer name")] - Specifies the label for showName in the runtime dialog. If omitted, the label from the parm method’s extended data type is used.

parmShowName - Runtime parm method for accessing/setting the contract class show name class variable.

getQuery - Returns the query for the contract class at runtime. Required by the system.

setQuery - Sets the query for the contract class at runtime. Required by the system.

parmQuery - Runtime parm property for accessing/setting the contract class query. Required.

Controller class


public class MySysOpDemoController extends SysOperationServiceController
{
    public static void main(Args _args)
    {
        MySysOpDemoController operation = new MySysOpDemoController();
        operation.startOperation();
    }

    public void new(IdentifierName _className = '', IdentifierName _methodName = '', SysOperationExecutionMode _executionMode = SysOperationExecutionMode::Asynchronous)
    {
        super(_className, _methodName, _executionMode);
        
        this.parmClassName(classStr(MySysOpDemoService));
        this.parmMethodName(methodStr(MySysOpDemoService, runQuery));
        this.parmExecutionMode(SysOperationExecutionMode::Synchronous);
        this.parmDialogCaption("My SysOperation demo");
    }
}

The controller class is used to launch the operation interactively from the F&O user interface or from code. Importantly, it provides information to the system about which business logic method to execute.

public static void main(Args _args) - The entry point of the controller class, this method is called by F&O when the operation is launched interactively using a menu item. The method instantiates an instance of our controller class and calls a single method startOperation.

public void new - The new method sets the business logic class name and method name after the call to super. This is how the SysOperation framework knows that our method must be called in the operation. The dialog caption is also set in the new method.

Note: The ExecutionMode in the example is set to Synchronous. That means the operation is executed as soon as the startOperation method is called. More about this later when I update this article.

Business logic class


public class MySysOpDemoService
{
    public void runQuery(MySysOpDemoContract _data)
    {
        Query query = _data.getQuery();
        QueryRun queryRun = new QueryRun(query);

        while (queryRun.next())
        {
            CustTable custTable = queryRun.get(tableNum(CustTable));
            
            if (_data.parmShowName())
            {
                info(strFmt('%1 - %2', custTable.AccountNum, custTable.name()));
            }
            else
            {
                info(custTable.AccountNum);
            }
        }
    }
}

Contains the business logic of the operation. The method must be public and can take only one argument – the data contract. The data contract holds the query and parameters for the operation.

The business logic in the example above does the following:

  • The query is retrieved from the data contract object
  • A QueryRun object is created from the query
  • A while loop enumerates all customers in the query
  • An if statement checks the value of the showName parameter and displays the information accordingly

Note: The data contract class passed as the parameter to the operation method is automatically instantiated by the SysOperation framework and provided to the controller.

Menu item

To launch the example operation from a menu or form in F&O, create a new Action Menu Item, then set its key properties:

  • Label → My SysOperation demo
  • Object → MySysOpDemoController
  • ObjectTypeClass

Once these properties are set, the menu item will open the SysOperation dialog and execute the operation exactly like any standard F&O process.

Basic properties of the menu item.


When the operation is launched via the menu item, the SysOperation framework automatically generates and displays a dialog. Our "Show customer name" parameter appears as a checkbox (slider) control because it is of type boolean. The framework automatically chooses the appropriate dialog control based on the parameter’s data type – for example:

  • str → text box
  • int or real → numeric field
  • boolean → checkbox/slider
  • enum → drop-down list
  • etc.

Output of the operation after clicking OK (with Show customer name disabled).


Launching our operation again and enabling the show customer name parameter.


Output of the operation after clicking OK (with Show customer name enabled).

Note:The output contains the same customer account multiple times, whereas one would expect to see the customer account only once per customer. The reason for this is that we are using the standard CustTable query, which also includes data sources for the CustTrans and CustTransOpen tables. As a result, the query returns open transactions as well. The output therefore displays the customer account once for each open transaction the customer has.

Thursday, November 27, 2025

X++ native types vs objects - the fundamental difference you need to understand

In X++ programming, there are simple variable types and more complex types. Native types are primitive variable types like int, str, real, etc. Simple types can be used to store specific variable values of types such as string, number, integer, and so on.

There are also more advanced types in X++. One of them is classes. A class is a representation of a real-life object, like a customer, vendor, product, bank, person, and so on. It can be anything that requires more than a single value, unlike a simple type. In fact, a class uses simple types to store the data in the object. A class can be instantiated to create an instance or object of that class. The object reference is stored in a variable just like simple types, but there is a fundamental difference between simple types and objects.

The biggest difference between these two types that I want to make clear is that object variables are references to objects. So, two different variables can point to the same object. If you change something in the object using the first variable, those changes will reflect in the second variable, and vice versa. It is key to understand this concept because in X++, this is something that is common in the application.

Code examples

Example 1. Native types

Two native type variables assigned the same value. One is changed but different values are printed to the output.


public static void main(Args _args)
{
    real len1, len2;

    len1 = 1.23;
    len2 = len1;
    len2 = 2;
    
    info(strFmt('len1: %1', len1));
    info(strFmt('len2: %1', len2));
}

// Generated output:
//    len2: 2
//    len1: 1.23

Example 2. Object types

Two variables pointing to the same object, or referencing the same object. I am using tables in this example to make it a bit simpler. Tables are actually classes and you don't have to instantiate them like classes.


public static void main(Args _args)
{
    CustTable custTable1, custTable2;

    custTable1.AccountNum = 'C1001';
    custTable1.CustGroup = 'ABC';
    
    custTable2 = custTable1; // assign object 1 to 2. object 2 is now referencing 1, both are pointing to the object 1.
    
    custTable2.CustGroup = 'XYZ'; // change customer group of object 2, object 1 will also reflect this change because it is pointing to the same object.
    
    info(strFmt('custTable1.AccountNum: %1', custTable1.AccountNum));
    info(strFmt('custTable1.CustGroup: %1', custTable1.CustGroup));
    info(strFmt('custTable2.AccountNum: %1', custTable2.AccountNum));
    info(strFmt('custTable2.CustGroup: %1', custTable2.CustGroup));
}

// Generated output:
//    custTable1.AccountNum: C1001
//    custTable1.CustGroup: XYZ
//    custTable2.AccountNum: C1001
//    custTable2.CustGroup: XYZ

Friday, October 31, 2025

Using a temporary table as a datasource in a form

Dynamics 365 Finance and Operations (F&O) has the ability to utilize temporary tables in code. There are many examples of temporary tables in the standard application. However, sometimes there is a requirement to display data in a temporary table in a form, so that the user can make selections or modify the data in the form before the data is processed.

One example in the standard application is when you post sub ledger documents, e.g. purchase or sales orders. The system presents the user with temporary data before the system posts the data and it becomes final.

Below is a simple example of how to populate a temporary table and display it in a simple form. It simply demonstrates the above-mentioned technique of using a temporary table as a dataSource and does not have any real-world value.

Create a temporary table or use an existing one. Note: The Table Type property of the table must be TempDB. So, if you are using an existing temporary table, make sure the Table Type property is TempDB.

Populate your temporary table with data. Note: The data in your table will be lost when the table buffer goes out of scope.


public class AVTmpTableDataProvider
{
    private AVUpdateDataTmpIdRef tmpIdRef; //added here to keep temp table instance alive while there is an object of this class.

    public AVUpdateDataTmpIdRef populateTmpTableDemo()
    {
        for (int j = 0; j < 100; j++)
        {
            tmpIdRef.Id = j;
            tmpIdRef.Name = guid2Str(newGuid());
            tmpIdRef.insert();
        }

        return tmpIdRef;
    }

}

Create a form and add your temporary table as the dataSource. Set the dataSource property of the grid and add some fields to the grid.

In your form, in the dataSource init method, populate your temporary table and then call the linkPhysicalTableInstance method of the dataSource. Note: You have to call this method after the dataSource has been initialized.


[Form]
public class AVTmpTableDialog extends FormRun
{
    //form class with an instance of our temp table. added here to keep temp table instance alive while the form is open.
    private AVTmpTableDataProvider dataProvider = new AVTmpTableDataProvider();

    [DataSource]
    class AVUpdateDataTmpIdRef
    {
        public void init()
        {
            super();

            //populate temp table in form class and link it to this datasource (after datasource has initialized).
            AVUpdateDataTmpIdRef.linkPhysicalTableInstance(dataProvider.populateTmpTableDemo());
        }

    }
}

Custom service using set based operations

Sometimes you just need a fast way to get specific data from Dynamics 365 Finance (F&O) for real-time processing of F&O data in another application. The data analyst and I were debating the architecture, and exporting the F&O data to a data warehouse was not meeting two of the most important requirements: performance and timeliness (i.e., data that must be up to date and not stale).

The client needs the data from F&O to be up to date and in real time without delays. Since the data output requirement did not involve large datasets, my solution was to use set-based operations to fill a temporary table and to expose the data in a custom service. I am very happy with the performance of this service and definitely recommend using this pattern to everyone out there.

On the highest level, there are basically two methods to insert data into the database: row-based and set-based. Let's briefly discuss both methods so that it's clear why I chose the set-based method.

Set-based operations

With set-based operations, the system creates a single SQL statement and sends it to the SQL Server, even if millions of records are processed in the select part of the statement. This statement selects data from tables and inserts it directly into the target table. It does the selection and insertion at the same time. This is extremely effective, and that makes it so fast.

Microsoft documentation: insert_recordset statement – Copy multiple records directly from one or more tables into another table in one database trip.

Row-based operations

Row-based operations are definitely the most used method in the standard application. They are straightforward and flexible to use, but the drawback is that they're slow. Row-based operations insert each row in a separate database round trip. The system first has to fetch the data, create X++ objects for each row, and then create a separate SQL statement for each X++ object / data row that should be inserted. This whole process adds a lot of overhead and therefore is inherently slow because of its design.

Disclaimer

  • The code below is for demonstration purposes only. Use at your own risk.
  • Don't be a silly Willy and use custom services for large datasets. Use other methods for that.

Okay, enough of all that, let's dive into the code part.

Temporary table

Create a temporary table with the fields that you will need for collecting the data and that will be exposed by the custom service. Set the TableType property to TempDB.


Service class

The code of the custom service class orchestrates data retrieval and prepares the data to be exposed by the custom service framework. When the data is retrieved, it uses a set-based operation, i.e., insert_recordset, which is very fast.

The different steps in the service class are:

  • Retrieve data from tables.
  • Populate the temp table.
  • Enumerate the temp table (for each row in the temp table).
    • Transfer the data from the table to an object.
    • Add the populated object to a list.

The code below is the actual code that I created for the client. It is a real-world working example and has not been simplified, because it would have taken too much time and might contain bugs. I did change the prefixes everywhere in the code below for obvious reasons, and I anonymized some of the dimension field names.

The service has two methods, one to retrieve the balances of ledger accounts for a specified period. The second to retrieve all the transactions for a specified main account. The code below is just the first part because I don't want to make the post longer than needed. The two other helper methods demontrate how to use the set-based operations in X++.


public class AVGenJourAccEntrySvc
{
    [AifCollectionTypeAttribute('return', Types::Class, classStr(AVGenJourAccEntryBalances))]
    public List getBalances(DataAreaId _dataAreaId, AccountingDate _accountingDateFrom, AccountingDate _accountingDateTo)
    {
        AVGenJourAccEntrySvcContract    contract;               //data class for retrieving data.
        AVGenJourAccEntryAggTmp         genJourAccEntryAggTmp;  //temp table where our data is stored.
        AVGenJourAccEntrySvcDP          genJourAccEntrySvcDP;   //logic to retrieve data and populate temp table.
        List                            list = new List(Types::Class); //list of data contracts.

        if (_dataAreaId && _accountingDateFrom && _accountingDateTo)
        {
            contract = new AVGenJourAccEntrySvcContract();

            contract.parmAccountingDateFrom(_accountingDateFrom);
            contract.parmAccountingDateTo(_accountingDateTo);
            contract.parmDataAreaId(_dataAreaId);

            changecompany(contract.parmDataAreaId())
            {
                genJourAccEntrySvcDP = AVGenJourAccEntrySvcDP::construct();
                genJourAccEntryAggTmp = genJourAccEntrySvcDP.populateDataBalances(contract); //retrieve data and populate temp table.

                //get data from temp table
                while select genJourAccEntryAggTmp
                    order by genJourAccEntryAggTmp.MainAccountId,
                             genJourAccEntryAggTmp.MainAccountName,
                             genJourAccEntryAggTmp.Dim1CostCenter,
                             genJourAccEntryAggTmp.Dim2Dept,
                             genJourAccEntryAggTmp.Dim3,
                             genJourAccEntryAggTmp.TransactionCurrencyCode
                {
                    
                    AVGenJourAccEntryBalances balances = AVGenJourAccEntryBalancesenJourAccEntryBalances::construct();
                    balances.initFromAVGenJourAccEntryAggTmp(genJourAccEntryAggTmp); //transfer data from temp table to data contract.

                    list.addEnd(balances); //add data object to list.
                }
            }
        }

        return list;
    }
}

The populateDataBalances method uses two other methods that also use set-based operations. All data collected with this class is done purely with set-based operations, making it extremely fast.


public AVGenJourAccEntryAggTmp populateDataBalances(AVGenJourAccEntrySvcContract _contract)
{
    //1. insert general journal account entry data joining with flattened dimension data.
    this.populateBalanceSheetIncomeStatementMovementsTmp(_contract);

    //2. insert general journal account entry data joining with flattened dimension data filtered by main account and dim3.
    this.populateBalanceSheetClosingsTmp(_contract);

    //3. combine and aggregate result sets.
    insert_recordset genJourAccEntryAggTmp
        (
        Company,
        AccountingCurrencyAmount,
        TransactionCurrencyAmount,
        TransactionCurrencyCode,
        MainAccountId,
        MainAccountName,
        Dim1CostCenter,
        Dim2Dept,
        Dim3
        )
        select
            Company,
            sum(AccountingCurrencyAmount),
            sum(TransactionCurrencyAmount),
            TransactionCurrencyCode,
            MainAccountId,
            MainAccountName,
            Dim1CostCenter,
            Dim2Dept,
            Dim3
        from genJourAccEntryAggTmpOpenBal
        group by
            Company,
            TransactionCurrencyCode,
            MainAccountId,
            MainAccountName,
            Dim1CostCenter,
            Dim2Dept,
            Dim3;

    insert_recordset genJourAccEntryAggTmp
        (
        Company,
        AccountingCurrencyAmount,
        TransactionCurrencyAmount,
        TransactionCurrencyCode,
        MainAccountId,
        MainAccountName,
        Dim1CostCenter,
        Dim2Dept,
        Dim3
        )
        select
            Company,
            sum(AccountingCurrencyAmount),
            sum(TransactionCurrencyAmount),
            TransactionCurrencyCode,
            MainAccountId,
            MainAccountName,
            Dim1CostCenter,
            Dim2Dept,
            Dim3
        from genJourAccEntryAggTmpAccDim
        group by
            Company,
            TransactionCurrencyCode,
            MainAccountId,
            MainAccountName,
            Dim1CostCenter,
            Dim2Dept,
            Dim3;

    //4. delete zero amount transactions.
    delete_from genJourAccEntryAggTmp
        where genJourAccEntryAggTmp.AccountingCurrencyAmount == 0;

    return genJourAccEntryAggTmp;
}


//1. insert general journal account entry data joining with flattened dimension data.
private void populateBalanceSheetIncomeStatementMovementsTmp(AVGenJourAccEntrySvcContract _contract)
{
    FiscalCalendarPeriod                        fiscalCalendarPeriod;
    MainAccount                                 mainAccount;
    GeneralJournalEntry                         generalJournalEntry;
    GeneralJournalAccountEntry                  generalJournalAccountEntry;
    DimensionAttributeValueGroup                dimensionAttributeValueGroup;
    DimensionAttributeLevelValue                dimensionAttributeLevelValue;
    DimensionCombinationEntity                  dimensionCombinationEntity;

    insert_recordset genJourAccEntryAggTmpOpenBal
        (
        Company,
        AccountingCurrencyAmount,
        TransactionCurrencyAmount,
        TransactionCurrencyCode,
        MainAccountId,
        MainAccountName,
        Dim1CostCenter,
        Dim2Dept,
        Dim3
        )

        select SubledgerVoucherDataAreaId from generalJournalEntry

        join AccountingCurrencyAmount, TransactionCurrencyAmount, TransactionCurrencyCode from generalJournalAccountEntry
            where generalJournalAccountEntry.GeneralJournalEntry == generalJournalEntry.RecId

        join MainAccountId, Name from mainAccount
            where generalJournalAccountEntry.MainAccount == mainAccount.RecId
                    
        join Dim1CostCenter, Dim2Dept, Dim3
            from dimensionCombinationEntity
                where generalJournalAccountEntry.LedgerDimension == dimensionCombinationEntity.RecordId
            
        exists join fiscalCalendarPeriod
            where generalJournalEntry.FiscalCalendarPeriod == fiscalCalendarPeriod.RecId &&
            
            generalJournalEntry.Ledger         == Ledger::current()                    &&
            generalJournalEntry.AccountingDate >= _contract.parmAccountingDateFrom()   &&
            generalJournalEntry.AccountingDate <= _contract.parmAccountingDateTo()     &&
            fiscalCalendarPeriod.Type          == FiscalPeriodType::Operating          &&
          ((mainAccount.MainAccountId < #FirstIncomeStatmentMainAccount && dimensionCombinationEntity.Dim3 != '') || mainAccount.MainAccountId >= #FirstIncomeStatmentMainAccount); //balance sheet transactions with movements or income statement transactions.
}


//2. insert general journal account entry data joining with flattened dimension data filtered by main account and dim3.
private void populateBalanceSheetClosingsTmp(AVGenJourAccEntrySvcContract _contract)
{
    DimensionValue                              closingMovementCode = '777';
    MainAccount                                 mainAccount;
    GeneralJournalEntry                         generalJournalEntry;
    GeneralJournalAccountEntry                  generalJournalAccountEntry;

    DimensionAttributeValueGroup                dimensionAttributeValueGroup;
    DimensionAttributeLevelValue                dimensionAttributeLevelValue;
    DimensionCombinationEntity                  dimensionCombinationEntity;

    insert_recordset genJourAccEntryAggTmpAccDim
        (
        Company,
        AccountingCurrencyAmount,
        TransactionCurrencyAmount,
        TransactionCurrencyCode,
        MainAccountId,
        MainAccountName,
        Dim1CostCenter,
        Dim2Dept,
        Dim3
        )

        select SubledgerVoucherDataAreaId from generalJournalEntry

        join AccountingCurrencyAmount, TransactionCurrencyAmount, TransactionCurrencyCode from generalJournalAccountEntry
            where generalJournalAccountEntry.GeneralJournalEntry == generalJournalEntry.RecId

        join MainAccountId, Name from mainAccount
            where generalJournalAccountEntry.MainAccount == mainAccount.RecId

        join Dim1CostCenter, Dim2Dept, closingMovementCode
            from dimensionCombinationEntity
                where generalJournalAccountEntry.LedgerDimension == dimensionCombinationEntity.RecordId &&

            generalJournalEntry.Ledger          == Ledger::current()                    &&
            generalJournalEntry.AccountingDate	>= _contract.parmAccountingDateFrom()   &&
            generalJournalEntry.AccountingDate  <= _contract.parmAccountingDateTo()     &&
            mainAccount.MainAccountId           < #FirstIncomeStatmentMainAccount; //only balance sheet transactions.
}

Service

In order to expose your custom service, you need to create a service and specify your service class and methods. Give the service an appropriate name.




Service group

Create a custom service group and add your service class to the group.




Monday, September 29, 2025

How to encrypt parameter data with Global::editEncryptedField()

Sometimes there is a requirement to encrypt parameter data like passwords, API tokens or other types of secrets. The question is, how is that done in Dynamics 365 Finance and Operations (F&O) without using third party tools?

Well, it's actually straight forward in F&O. You can perform encryption on a table field by using the standard Global::editEncryptedField() method. The base data type of your table field must be of type container. Any other type won't work.

Important

Each F&O environment has it's own encryption key. So, when moving data from the production environment to UAT, Test or Dev, the enrypted data will be obsolete in the new environment. You will have to manually enter the data again in F&O, or export the encrypted data before moving the database, and import the encrypted data after the database movement. The reason for this is because the data was encrypted using the production environments encryption key. The encrypted data cannot be decrypted with the encrpytion key of another environment, only with the encryption key of the environment where it was encrypted.

Steps

1. Add a field of type container to your table. Set the extended data type to EncryptedField.

2. On your table, add an edit method, see code example below.

3. Add a password field to your form and set the data source to your table and data method to your edit method.

Code example

If you want to have a look at an example in the standard application. Have a look at the SysEmailSMTPPassword table and form.


public edit Password editUserPassword(boolean _set, Password _password)
{
    return Global::editEncryptedField(this, _password, fieldNum(YourTableName, Password), _set);
}

Saturday, September 27, 2025

Auto submit workflow using X++

Workflows in Dynamics 365 Finance and Operations (F&O) are used to add approval steps to a business process flow. For example, in the purchase-to-pay flow, if approval is enabled and the workflow is configured, assigning a new or different bank account to a vendor account is subject to approval. This means that in F&O, an approver must approve the new bank account before payments can be made to the vendor's new bank account. I highly recommend this vendor bank approval workflow to all my clients, by the way. So, if a user manually assigns a new or different bank account to a vendor in F&O, the system requires approval by an approver.

In the field, one of my clients has an integration (built by a partner, not me) from an external application where the vendors and bank accounts are created and then sent to F&O. The vendors and vendor bank accounts are imported into F&O using a custom framework. The custom framework in F&O creates the vendors and vendor bank accounts and assigns the vendor bank account to the vendor. After this assignment of the bank account to the vendor account, the vendor account is blocked for payment, and the system requires approval via workflow.

Because of the sheer volume that is processed, it's not feasible for someone to manually submit all those new vendors and vendor bank accounts to the workflow. It's not possible to submit them in bulk; they have to be submitted individually.

So, how do we solve this without increased risk? By creating a small customization using X++ and the SysOperation framework. The customization uses the X++ Workflow::activateFromWorkflowType method and automatically submits the vendor and vendor bank accounts to the approval workflow.

Note: The records are only submitted to the workflow, not approved. The approval part of the flow remains and has to be done by an authorized workflow approver in F&O. In this instance, we can identify the source of the data as "external," so we filter the data so that only the externally created data is handled by this customization. The approval is still manual not automatic due to the risk.

To conclude, data can be submitted to the workflow using the Workflow::activateFromWorkflowType method and approvals should not be automated because of the risk.

Code

The code below is the core of my solution. With the SysOperation framework, I always use a minimum of 3 classes. The code below extends the vendor and vend bank account tables and the methods are in the business logic class. If you need more detailed information about what I mean with "3 classes", have a look at Multi selection on forms with the SysOperation framework.

VendTable extension method


public boolean avCanSubmitToWorkflow()
{
    boolean ret = true;

    if (!this.RecId)
    {
        ret = false;
    }

    if (this.WorkflowState == VendTableChangeProposalWorkflowState::NotSubmitted)
    {
        VendTableChangeProposal changeProposal;
        
        changeProposal.disableCache(true);

        select firstOnly RecId from changeProposal
            where changeProposal.VendTable == this.RecId;

        if (!changeProposal)
        {
            ret = false;
        }
    }
    else
    {
        ret = false;
    }

    return ret;
}

VendBankAccount extension method


public boolean avCanSubmitToWorkflow()
{
    boolean ret = false;

    if (this.WorkflowState == VendBankAccountChangeProposalWorkflowState::NotSubmitted && this.requiresApproval())
    {
        ret = true;
    }

    return ret;
}

Vendor code


private Counter runVendors()
{
    Counter cntr;
    QueryRun queryRun;
    VendTable vendTable, vendTableUpdate;

    queryRun = new QueryRun(this.buildQueryVendTable());

    while (queryRun.next())
    {
        vendTable = queryRun.get(tableNum(VendTable));

        if (vendTable.avCanSubmitToWorkflow())
        {
            if (Workflow::activateFromWorkflowType(workFlowTypeStr(VendTableChangeProposalWorkflow), vendTable.RecId, "Auto submit approve manually", NoYes::No))
            {
                update_recordset vendTableUpdate
                    setting WorkflowState = VendTableChangeProposalWorkflowState::Submitted
                        where vendTableUpdate.RecId == vendTable.RecId  &&
                              vendTableUpdate.WorkflowState == VendTableChangeProposalWorkflowState::NotSubmitted;
                cntr++;
            }
        }
    }

    return cntr;
}

Vendor bank account code


private Counter runVendBankAccounts()
{
    Counter cntr;
    QueryRun queryRun;
    VendBankAccount vendBankAccount, vendBankAccountUpdate;

    queryRun = new QueryRun(this.buildQueryVendBankAcc());

    while (queryRun.next())
    {
        vendBankAccount = queryRun.get(tableNum(VendBankAccount));

        if (vendBankAccount.avCanSubmitToWorkflow())
        {
            if (Workflow::activateFromWorkflowType(workFlowTypeStr(VendBankAccountChangeProposalTemplate), vendBankAccount.RecId, "Auto submit approve manually", NoYes::No))
            {
                update_recordset vendBankAccountUpdate
                    setting WorkflowState = VendBankAccountChangeProposalWorkflowState::Submitted
                        where vendBankAccountUpdate.RecId == vendBankAccount.RecId &&
                              vendBankAccountUpdate.WorkflowState == VendBankAccountChangeProposalWorkflowState::NotSubmitted;
                cntr++;
            }
        }
    }

    return cntr;
}

Saturday, August 23, 2025

How to view data in a temporary table using SQL Server Management Studio

Temporary tables are used quite often in Dynamics 365 Finance and Operations (F&O) and you can find them everywhere in the standard code and forms. There are different types of temporary tables but in this article we are using the TempDB type. The main advantage of the TempDB type is that you can join them with regular tables because they physically exist in the database.

Every time a temporary table is instantiated in F&O, an actual table is created in the SQL Server tempdb database. The name of the table is unique each time. When the temporary table buffer in F&O goes out of scope, the data in the temporary table is discarded and the table is automatically deleted at some point.

Sometimes when you are troubleshooting code that's using a temporary table, you need to view the contents of a temporary. This cannot be done easily like a regular table using the F&O table browser or SQL Server Management Studio (SSMS). So this begs the question, how do you actually view the data of a temporary table?

The answer is by using the X++ getPhysicalTableName method of the intantiated temporary table. This retrieves the physical SQL Server table name of the temporary table. The most basic method is to temporarily add a line of code using the info method to display the table name. You can then use the table name to query the database with SSMS and view the data as long as the temporary table buffer is still in scope in F&O.

Warning: don't do this in production! The above is only applicable for dev and sandbox environments. This article is for education purposes and does not explain the best possible way to get the table name for your situation. To make thing a bit more elegant, you could write the table name to a log table (e.g. SysExceptionTable) instead of displaying a message in the infolog and you could do execute the line of code for your user account only. This way users won't notice it's there.

Get the physical SQL table name in code


public class MyTmpTableTest
{
    public static void main(Args _args)
    {
        TmpRecIdFilter tmpRecId; //note: TableType must be TempDB
    
        //add some dummy records to the temp table.
        for (Counter cntr = 1; cntr < 50; cntr++)
        {
            tmpRecId.RefRecId = cntr;
            tmpRecId.insert();
        }

        //show the temp table name. this is the only line of code you need,
        //the rest of the code here is just for this test class.
        info(tmpRecId.getPhysicalTableName());

        //for this test class we use a work around to keep temp table bufffer
        //in scope so that the data can be viewed using SSMS.
        //as soon the main method exists and the temp table buffer is out of
        //scope, the data will be lost in the table.
        Box::infoOnceModal('Table name', 'Table name', tmpRecId.getPhysicalTableName(), curUserId());
    }
}

Viewing the data in SQL Server Management Studio

  • Get the table name.
  • Connect to the F&O database using SSMS.
  • Open a new query with the table name form the first step e.g. "select * from [your_temp_table_name_here]"

Saturday, August 9, 2025

How to get the last workflow comment

The code snippet below retrieves the last workflow comment of a workflow using the workflow correlation ID.

One of my customers requested a custom workflow with a separate approval e-mail message in Outlook using a custom template in F&O. In the email message, they wanted the full details from F&O including the comment of the person who submitted the workflow for approval.

To answer a question about this on the Microsoft F&O forum, I found this code in my code archive and thought it might be worth sharing.


public static str getLastTrackingComment(WorkflowCorrelationId _workflowCorrelationId)
{
    WorkflowTrackingTable           trackingTable;
    WorkflowTrackingCommentTable    trackingCommentTable;

    if (_workflowCorrelationId)
    {
        trackingTable = Workflow::findLastWorkflowTrackingRecord(_workflowCorrelationId);

        if (trackingTable)
        {
            trackingCommentTable = WorkflowTrackingCommentTable::findTrackingRecId(trackingTable.RecId);
        }
    }

    return trackingCommentTable.Comment;
}

Sunday, July 13, 2025

How to use a custom form as a dialog in a custom runnable class

Sometimes there is a requirement to perform a simple operation in Dynamics 365 Finance and Operations using some limited input from the user before the operation is executed.

A real world example of custom records and a custom form to view, edit and process "records" Due to customer privacy, I can't use the real names of the records and will just refer to the data a "record". The requirement is to update a single field of a read only record. The record is not allowed to be edited in the form because of its status. In the form datasource active method, there is code to check the status of the record and sets the datasource allowEdit property according to the status of the record. However, due to certain factors, sometimes the record is not processed in time and the date of the record is not current anymore, it's now in the past. This creates a catch 22. This record cannot be processed because the date is in the past and the date cannot be updated because it's read only due to the status. The solution is, the clicks on a menu item button on the form to change the date of the record. When the user click the menu item button, a dialog is displayed showing a date field. The use can only choose a date in the future. The validation on the form makes sure a valid date is selected when the user clicks in the OK button.

This article recommends some basic level of code knowledge and desing patterns and doesn't go into detail and inner workings of the code. However, it demonstrates a simple pattern for a runnable class using a custom form dialog.

Recommended code knowlege:

  • You know what a runnable class is
  • You have basic knowledge of how the runnable class pattern works
  • You know how to create a form of type dialog and know ho to add input fields on the form
  • You know how to add validation on the form to validate the user input data
  • You know how to ge variable values in code from a form object of the input fields of the form

What you need to do to implement the pattern (in a nutshell):

  • Create your own runnable class with a static main method, run and prompt methods
  • Create your own form (type dialog) with required input fields on the form.
  • Add an OK button to the form that calls the forms closeOK method.
  • Add an Cancel button to the form that just closes the form whithout doing anything
  • Copy the code below to your own runnable class.
  • Change the class name on the main method to your own class
  • Change the form name in the prompt method to your own own form name
  • Retrieve the input values from the form object in the prompt method if user has clicked the OK button
  • Finally, implement the business logic in the run method

Runnable class code


public final class MyCustomRunnableClassWithCustomFormAsDialog
{
    private FormRun mFormRun;
    //add input form class variables
    
    public void run()
    {
        //add logic here.
    }
    
    public boolean prompt()
    {
        Object obj;
        
        mFormRun = classFactory.formRunClass(new Args(formStr(MyCustomFormDialog))); // Change form name.
        mFormRun.init();
        mFormRun.run();
        mFormRun.wait();
        
        if (mFormRun.closedOk()) //did the user click OK?
        {
            obj = mFormRun;
            //retrieve form variables values from form obj and set class variable here.
        }
        
        return mFormRun.closedOk();
    }
    
    public static void main(Args _args)
    {
        MyCustomRunnableClassWithCustomFormAsDialog operation = new MyCustomRunnableClassWithCustomFormAsDialog();
        
        if (operation.prompt())
        {
            operation.run();
        }
    }    
}

Saturday, July 12, 2025

Extended Data Types

This is another article I wrote ages ago that was originally published on Axaptapedia, which no longer exists. I found a copy online thought it was worth sharing as a blog as most of it remains relevant for Dynamics 365 Finance and Operations.

I have not edited the content but I left out a part about creating EDTs in code by an another author who added that later.

Side note: An extended data type is also referred to a an EDT.

Okay, enough of that. Let's start!

Introduction

Extended data types are very important and if used correctly, very powerful. An extended data type is a user-defined definition of a primitive data type. The following primitive data types can be extended: boolean, integer, real, string, date and container.

Inheritance

Name is a standard string EDT that represents the name of a person, company or object. If a property on Name is changed, all EDT's that inherit from it will automatically reflect the changed value. For example if the standard length is not long enough, it can be increased and all child EDT's will automatically be increased along with it. All database fields, forms and reports where the EDT is used, will also reflect the changed property.

Properties

Some of the properties that can be modified are StringSize, Alignment, DisplayLength, Label and HelpText.

Number sequences

When creating a number sequence, an extended data type is required to associate the number sequence with.

Advantages

1. Consistency in data model, forms and reports.

2. Automatic lookups (if table relation property is set).

3. Improve readability in code.

Sunday, June 29, 2025

Use PowerShell to search for labels fast

Here is a quick and simple method I use a lot to search for labels using PowerShell. It's seriously fast!

Let's start.

1. Open PowerShell and change the current directory to your LocalPackages folder. Change the path according to your system. Make sure you have enough permission on the local packages directory. This is why I always open PowerShell as Administrator.

cd C:\Users\mrbean\AppData\Local\Microsoft\Dynamics365\10.0.1935.92\PackagesLocalDirectory

2. Let's search for the label "Invoice" using the PowerShell "findstr" command. The command below will search in all the en-US label files.

findstr /s /l /i /n /E /c:"=Invoice" *en-US.label.txt

Explanation of the command arguments:

  • /s: Searches subdirectories.
  • /l: Performs a literal search (case-sensitive by default).
  • /i: Makes the search case-insensitive.
  • /n: Displays line numbers in results.
  • /E: Matches the pattern at the end of a line.
  • /c:"=Invoice": Searches for the exact string =Invoice.
  • *en-US.label.txt: Targets all files with the en-US.label.txt extension.

Note: The "/E" argument in the PowerShell command returns all lines of text that end with "Invoice". Because we are dealing with Dynamics 365 Finance label files that have and ID and label text separated with an "=" I've included "=" in the search. The causes the command to return the exact labels we search for. Copy and paste the label ID from the results.

Edit 10-01-2026

I created a Windows command shell script (.cmd) to make searching easier. You can just double click the .cmd file and do multiple searches. Adjust the script accordingly. Could work as PowerShell script but I have not tried yet because it works a .cmd script.


REM Author: Anton Venter 2026
@echo off
c:
cd C:\Users\antonv\AppData\Local\Microsoft\Dynamics365\10.0.2345.71\PackagesLocalDirectory
:lbl_start
set /p labelSearch="Enter label to search for: "
findstr /s /l /i /n /E /c:"=%labelSearch%" *sys*en-us.label.txt
goto :lbl_start
pause