9.1 OOP Principles & QueryBuilder Class

Key Takeaways

  • X++ enforces single inheritance for classes using the extends keyword, while multiple inheritance is achieved purely through interfaces using the implements keyword.
  • X++ strictly forbids method overloading; classes cannot define multiple methods sharing the same identifier with differing parameter signatures, requiring developers to leverage optional parameters with default values instead.
  • The Query object framework (Query, QueryBuildDataSource, QueryBuildRange, QueryRun) abstracts SQL syntax into a dynamic, object-oriented runtime graph that can be serialized across tiers and modified programmatically.
  • Data sources within a Query support multiple relational join modes—including JoinMode::InnerJoin, JoinMode::OuterJoin, JoinMode::ExistsJoin, and JoinMode::NoExistsJoin—where ExistsJoin optimizes existence checks without returning child table buffer records.
  • Dynamic query filtering must utilize QueryBuildRange with SysQuery::value() to properly sanitize literals, prevent SQL injection, and format composite types such as dates and extensible enums.
Last updated: September 2026

9.1 OOP Principles & QueryBuilder Class

Quick Answer: The MB-500 exam requires developers to understand advanced Object-Oriented Programming (OOP) in X++ and the object-oriented Query framework. X++ supports single class inheritance (extends) and multiple interface implementation (implements). Crucially, X++ does not support method overloading; flexible signatures are achieved using optional parameters with default values. For dynamic runtime data access, the Query object model replaces static while select statements. A Query contains hierarchical QueryBuildDataSource (QBDS) nodes joined via JoinMode (InnerJoin, OuterJoin, ExistsJoin, NoExistsJoin), filtered through QueryBuildRange (QBR) sanitized with SysQuery::value(), sorted via addSortField(), and traversed using a QueryRun cursor loop (queryRun.next()). Remember: an ExistsJoin tests record existence but never populates the child table buffer.


1. Advanced OOP Constructs in X++

X++ is an object-oriented, strongly typed programming language compiled to Common Intermediate Language (.NET CIL). Enterprise design in Dynamics 365 Finance and Operations relies on core OOP tenets to maintain modularity, testability, and code reuse.

Abstraction and Encapsulation

Encapsulation restricts direct internal access to object state, exposing only controlled operations through access modifiers:

  • public: Accessible from any code in any model referencing the current model.
  • protected: Accessible only within the declaring class and its subclasses (essential for extensibility and Chain of Command).
  • private: Accessible strictly within the declaring class scope. Cannot be inherited, overridden, or wrapped by Chain of Command.
  • internal: Accessible only within the same compilation unit (model/assembly), isolating framework internals from external consumer models.

Inheritance Hierarchies and Construction Lifecycle

X++ enforces single inheritance for class implementations. A class may extend only one direct superclass using the extends keyword. When an instance is constructed, the constructor execution begins from the top of the inheritance tree:

public class SalesLineValidator
{
    protected SalesLine salesLine;

    public void new(SalesLine _salesLine)
    {
        salesLine = _salesLine;
    }

    public boolean validate()
    {
        return salesLine.QtyOrdered > 0;
    }
}

public class SpecialSalesLineValidator extends SalesLineValidator
{
    public void new(SalesLine _salesLine)
    {
        // Mandatory constructor chaining to base class
        super(_salesLine);
    }

    public boolean validate()
    {
        boolean isValid = super();
        return isValid && salesLine.LineAmount >= 0;
    }
}

Polymorphism and Method Dispatch

Polymorphism allows objects of different concrete subclasses to be treated through a shared superclass or interface reference. Method dispatch in X++ is virtual by default, meaning runtime execution invokes the most derived implementation of a method unless explicitly marked otherwise.

[!WARNING] Critical Exam Rule: X++ Does Not Support Method Overloading Unlike C# or Java, X++ strictly prohibits method overloading. You cannot declare two methods with the same name in the same class, even if their parameter types or counts differ. Attempting to define public void calculate(ItemId _item) and public void calculate(ItemId _item, CustAccount _cust) results in a compile-time fatal error. To provide call flexibility, X++ developers must use optional parameters with default values or distinct method names.

Abstract Base Classes vs. Interfaces

When defining reusable architectural contracts, developers must select between an abstract base class and an interface.

Architectural DimensionAbstract Base Class (abstract class)Interface (interface)
Inheritance ModelSingle inheritance (extends BaseClass)Multiple inheritance (implements Inter1, Inter2)
Instance VariablesCan declare protected/private instance variablesCannot declare instance variables (stateless contract)
Method ImplementationsCan provide concrete default method bodies alongside abstract methodsMethod signatures only (pure contracts without implementation bodies)
Constructors (new)Can define constructors called by subclasses via super()Cannot define constructors
Extension via CoCConcrete methods can be wrapped by Chain of CommandInterface declarations cannot be wrapped by CoC
Typical Use CaseBase framework classes providing shared plumbing (e.g., RunBaseBatch, SysOperationServiceController)Defining cross-cutting capabilities (e.g., SysPackable, FormRunConfigurationPublishable)

2. Object-Oriented Query Architecture: The Query Framework

While inline X++ SQL (while select) provides fast compile-time syntax checking, it creates static queries whose structure cannot be altered at runtime. Enterprise scenarios require dynamic data extraction where datasources, join topologies, ranges, and sort orders are assembled dynamically based on user parameters, security contexts, or configuration keys.

The Query Object Framework abstracts relational SQL queries into an object graph managed by four interconnected core classes:

Query Object Framework Hierarchy
├── Query                      (The root container representing the entire SQL statement)
│   └── QueryBuildDataSource   (Represents table entities, join modes, and fetch modes)
│       ├── QueryBuildRange    (Represents WHERE clauses and filter criteria)
│       ├── QueryBuildLink     (Represents explicit join relations between parent/child)
│       └── QueryBuildDataSource (Nested child data sources for multi-table joins)
└── QueryRun                   (The runtime iterator cursor engine executing the query)

Core Classes of the Query Framework

  1. Query: The metadata container defining the overall query structure. Can be authored in the AOT as an object or instantiated programmatically in X++ via Query query = new Query();.
  2. QueryBuildDataSource (QBDS): Represents an individual table participating in the query. Governs joins, relations, fields selected, and groupings.
  3. QueryBuildRange (QBR): Defines filtering conditions applied to a specific field on a QBDS. Can represent single values, open ranges, wildcards, or complex expressions.
  4. QueryRun: The execution engine. Accepts a Query object, coordinates communication with the SQL database, manages database paging, and advances through records using an iterator pattern.

3. Dynamic Query Construction and Execution in Code

Building a robust query programmatically requires configuring data sources, defining relational joins, sanitizing range inputs, specifying sort fields, and iterating with a QueryRun cursor.

Configuring Data Sources and Join Modes

When multiple tables are involved, child data sources are appended to parent data sources using addDataSource(tableNum(Table)):

Query                   query = new Query();
QueryBuildDataSource    qbdsCustTable;
QueryBuildDataSource    qbdsCustTrans;

// Add root data source
qbdsCustTable = query.addDataSource(tableNum(CustTable));

// Add child data source
qbdsCustTrans = qbdsCustTable.addDataSource(tableNum(CustTrans));
qbdsCustTrans.relations(true); // Automatically uses AOT table relations
qbdsCustTrans.joinMode(JoinMode::InnerJoin);

Supported Join Modes (JoinMode Enum)

  • JoinMode::InnerJoin: Returns parent rows only when matching child rows exist, projecting fields from both tables into the cursor.
  • JoinMode::OuterJoin: Returns all parent rows regardless of whether matching child rows exist. Child fields contain null/default values if unmatched.
  • JoinMode::ExistsJoin: Evaluates existence. Returns the parent record if at least one matching child record exists. Crucially, no child buffer data is returned, reducing network overhead.
  • JoinMode::NoExistsJoin: Returns parent records only when zero matching records exist in the child table (anti-semi-join).

Applying Ranges with SysQuery::value()

Direct string concatenation when assigning ranges is a critical security vulnerability and causes data type conversion failures. Developers must use SysQuery::value() to safely sanitize inputs:

QueryBuildRange qbrStatus;
QueryBuildRange qbrAmount;

// Safe value assignment for Enums and Strings
qbrStatus = qbdsCustTable.addRange(fieldNum(CustTable, Blocked));
qbrStatus.value(SysQuery::value(CustVendorBlocked::No));

// Setting closed interval ranges
qbrAmount = qbdsCustTrans.addRange(fieldNum(CustTrans, AmountMST));
qbrAmount.value(SysQuery::range(1000.00, 50000.00)); // "1000..50000"

// Open-ended filtering (greater than or equal to)
qbrAmount.value(strFmt('>= %1', SysQuery::value(1000.00)));

Adding Sort Orders

Sorting is applied to individual data sources using addSortField() and addOrderByField():

// Sort descending by transaction date
qbdsCustTrans.addSortField(fieldNum(CustTrans, TransDate), SortOrder::Descending);

Query Execution Loop via QueryRun

To execute the query and read data, instantiate QueryRun and traverse the results using a while (queryRun.next()) loop:

QueryRun    queryRun = new QueryRun(query);
CustTable   custTable;
CustTrans   custTrans;

while (queryRun.next())
{
    // Retrieve table buffers from the current cursor position
    custTable = queryRun.get(tableNum(CustTable));
    custTrans = queryRun.get(tableNum(CustTrans));

    info(strFmt("Customer: %1, Voucher: %2, Amount: %3",
        custTable.AccountNum,
        custTrans.Voucher,
        custTrans.AmountMST));
}

4. Scenario Walk-Through: Dynamic Financial Auditor

Business Scenario

A financial compliance officer needs a batch report identifying all Customers in Customer Group "10" who have at least one overdue open transaction exceeding $10,000, but who are not currently blocked from invoicing. Because this is an existence check across millions of transaction records, performance must be optimized by avoiding unnecessary child record buffer hydration.

Architectural Implementation Solution

internal final class CustOverdueAuditorService
{
    public void runAudit()
    {
        Query                   query = new Query();
        QueryBuildDataSource    qbdsCust;
        QueryBuildDataSource    qbdsTrans;
        QueryBuildRange         qbrGroup;
        QueryBuildRange         qbrBlocked;
        QueryBuildRange         qbrAmount;
        QueryBuildRange         qbrDueDate;
        QueryRun                queryRun;
        CustTable               custTable;

        // 1. Root Datasource: CustTable
        qbdsCust = query.addDataSource(tableNum(CustTable));
        
        qbrGroup = qbdsCust.addRange(fieldNum(CustTable, CustGroup));
        qbrGroup.value(SysQuery::value('10'));

        qbrBlocked = qbdsCust.addRange(fieldNum(CustTable, Blocked));
        qbrBlocked.value(SysQuery::value(CustVendorBlocked::No));

        // 2. Child Datasource: CustTrans (ExistsJoin for maximum SQL throughput)
        qbdsTrans = qbdsCust.addDataSource(tableNum(CustTrans));
        qbdsTrans.relations(true);
        qbdsTrans.joinMode(JoinMode::ExistsJoin); // Does not pull CustTrans into AOS memory

        qbrAmount = qbdsTrans.addRange(fieldNum(CustTrans, AmountMST));
        qbrAmount.value(strFmt('> %1', SysQuery::value(10000.00)));

        qbrDueDate = qbdsTrans.addRange(fieldNum(CustTrans, DueDate));
        qbrDueDate.value(strFmt('< %1', SysQuery::value(DateTimeUtil::getSystemDate(DateTimeUtil::getUserPreferredTimeZone()))));

        // 3. Order results
        qbdsCust.addSortField(fieldNum(CustTable, AccountNum), SortOrder::Ascending);

        // 4. Execution
        queryRun = new QueryRun(query);
        while (queryRun.next())
        {
            custTable = queryRun.get(tableNum(CustTable));
            info(strFmt("Audited Customer: %1 - %2", custTable.AccountNum, custTable.name()));
        }
    }
}

5. Real-World Exam Traps: OOP & QueryBuilder

[!WARNING] Exam Trap 1: Attempting to Read Child Buffers in an ExistsJoin On the MB-500 exam, questions frequently test what happens when a developer executes custTrans = queryRun.get(tableNum(CustTrans)) when qbdsTrans.joinMode(JoinMode::ExistsJoin) is used. In an ExistsJoin, the SQL query executes an EXISTS (SELECT ...) clause. The child table columns are never projected into the SQL result set. The buffer custTrans remains completely empty (RecId == 0). If child fields are required in application logic, JoinMode::InnerJoin must be used instead.

[!WARNING] Exam Trap 2: Direct String Formatting in Query Ranges Writing qbr.value("Blocked == 0") or hardcoding enum integers directly into ranges without SysQuery::value() causes severe bugs. For extensible enums, string conversion produces integer hashes that fail matching. Always use SysQuery::value(CustVendorBlocked::No).

[!WARNING] Exam Trap 3: Expecting Method Overloading to Compile in X++ A scenario question may present a refactoring proposal adding a second constructor or helper method with identical name but different parameters. In X++, this produces compile-time error The method has already been defined. The correct answer always involves default parameter values or distinct method names.

[!WARNING] Exam Trap 4: Forgetting queryRun.next() Advances the Database Cursor Candidates often assume queryRun.get(tableNum(CustTable)) executes the query. get() only retrieves the current buffer from memory; queryRun.next() is what actually communicates with the database and fetches the next record row.

Loading diagram...
Query Object Model Architecture and Execution Flow
Test Your Knowledge

A developer needs to write a dynamic X++ query that returns all CustTable records that have at least one corresponding open invoice in CustTrans. To maximize database performance, no fields from the CustTrans table should be retrieved over the network to the AOS tier. Which join mode must be configured on the CustTrans QueryBuildDataSource?

A
B
C
D
Test Your Knowledge

An X++ developer attempts to implement two methods in a custom pricing calculation engine class: public Amount calculatePrice(ItemId _itemId) and public Amount calculatePrice(ItemId _itemId, CustAccount _custAccount). What will happen when the project is compiled in Visual Studio?

A
B
C
D
Test Your Knowledge

A developer writes a custom query in X++ to filter records on CustTable. When setting a range on an extensible Base Enum field using QueryBuildRange, what is the Microsoft-recommended best practice to guarantee type safety and prevent SQL conversion errors?

A
B
C
D
Test Your Knowledge

Consider the following X++ code snippet utilizing the Query object framework: QueryRun queryRun = new QueryRun(query); CustTable custTable = queryRun.get(tableNum(CustTable)); while (queryRun.next()) { info(custTable.AccountNum); } What is the functional defect in this code?

A
B
C
D