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

Example using construct

For 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.

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. See line 39 and 40 below. 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.

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.