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.