7.1 Class Architecture & Access Modifiers

Key Takeaways

  • X++ enforces a single-inheritance object-oriented class hierarchy rooted in the common Object class, complemented by multiple interface implementation via the implements keyword.
  • The standard instantiation pattern in enterprise X++ encapsulates the new() constructor as protected or private, exposing public static construct() factory methods to control parameterization, caching, and polymorphism.
  • Modern X++ provides four access modifiers: public (accessible anywhere), protected (accessible within the class and derived classes), private (restricted to the declaring class), and internal (accessible only within the declaring compilation package/model).
  • The final modifier prevents class inheritance and method overriding; critically for extensions, methods declared as final cannot be wrapped using Chain of Command (CoC).
  • Form and table display methods compute runtime presentation values; decorating display methods with [SysClientCacheDataMethodAttribute(true)] eliminates redundant remote procedure calls between client and AOS tiers during grid rendering.
Last updated: September 2026

7.1 Class Architecture & Access Modifiers

Quick Answer: X++ is a single-inheritance, class-based object-oriented language that compiles into .NET Common Intermediate Language (CIL). Classes inherit from a single parent via extends and implement one or more contracts via implements. In enterprise X++, class instantiation is governed by the static construct() factory pattern, which hides the parameterless new() constructor to enforce controlled initialization and polymorphic subclass selection. Access visibility is governed by four modifiers: public, protected, private, and internal (which restricts visibility strictly to the declaring model/package). Method modifiers include abstract, final (which prohibits overriding and blocks Chain of Command wrapping), and static. For user interfaces, display methods compute values dynamically and must be cached using [SysClientCacheDataMethodAttribute(true)] to prevent severe grid rendering lag. Type inspection and casting are safely handled via the is and as operators.


1. Object-Oriented Class Hierarchy in X++

Dynamics 365 Finance and Operations executes on the Microsoft .NET runtime. Every X++ class is a first-class CLR type. Understanding how X++ constructs map to object-oriented principles is foundational for MB-500 exam success.

The Root Object & Single Inheritance

  • Single Inheritance (extends): An X++ class can inherit from at most one direct parent superclass. If no superclass is specified in the declaration, the class implicitly inherits from the system root class Object.
  • Multiple Interface Implementation (implements): A class can implement multiple interfaces, providing a mechanism for polymorphic contracts without diamond-inheritance ambiguity.
  • Object Lifecycle and Memory Management: Objects in X++ are instantiated on the managed heap. The .NET Common Language Runtime (CLR) Garbage Collector automatically manages memory deallocation. Destructors (finalize) are not supported in X++; cleanup of unmanaged resources must follow the System.IDisposable pattern.
// Standard class declaration illustrating inheritance and interface implementation
public class CustInvoiceProcessor extends DocumentProcessor implements IBatchable, IDisposable
{
    // Instance state variables
    private CustInvoiceTable invoiceTable;
    private boolean          isProcessed;

    // Implementation of interface and base class methods
}

2. Constructor Patterns: new() vs. Static construct() & main()

In standard object-oriented languages (like C# or Java), parameterized constructors are directly invoked using the new operator. In X++, constructor mechanics and legacy architectural patterns dictate specific instantiation conventions.

The new() Method

In X++, the constructor method is always named new(). While modern X++ supports parameters on new(), Microsoft architectural guidelines strongly discourage exposing public parameterized new() methods directly to external consumers.

Key reasons for this rule include:

  1. Polymorphic Factory Delegation: Calling new binds the caller to a concrete class. A static factory method can inspect parameters, configuration keys, or country-region codes and return a specialized subclass.
  2. Extensibility & Hooking: Direct instantiation via new cannot be easily intercepted by extension frameworks if subclass substitution is required.
  3. Initialization Sequencing: A static factory can coordinate pre-initialization checks, parameter caching, and state validation before returning the instance.

The construct() Factory Pattern

Every business logic and service class in Dynamics 365 F&O should declare its new() method as protected (or private) and expose a public static construct() method.

public class SalesLineValidator
{
    protected SalesLine salesLine;

    // Hide default constructor to prevent un-parameterized instantiation
    protected void new()
    {
    }

    // Public static factory method
    public static SalesLineValidator construct(SalesLine _salesLine)
    {
        SalesLineValidator validator;

        // Factory logic: return specialized subclass based on sales type
        switch (_salesLine.SalesType)
        {
            case SalesType::ReturnItem:
                validator = new SalesLineValidator_Return();
                break;
            default:
                validator = new SalesLineValidator();
                break;
        }

        validator.parmSalesLine(_salesLine);
        return validator;
    }

    public SalesLine parmSalesLine(SalesLine _salesLine = salesLine)
    {
        salesLine = _salesLine;
        return salesLine;
    }

    public boolean validate()
    {
        // Core validation logic
        return true;
    }
}

The main(Args _args) Entry Point

Classes intended to be invoked directly from Action Menu Items, forms, or batch jobs must declare a public static main() method accepting an Args parameter.

public static void main(Args _args)
{
    if (!_args || !_args.record() || _args.dataset() != tableNum(SalesTable))
    {
        throw error("@SYS25516"); // Record context required
    }

    SalesTable salesTable = _args.record() as SalesTable;
    SalesOrderPostManager manager = SalesOrderPostManager::construct(salesTable);
    manager.run();
}

3. Visibility and Access Modifiers

Access modifiers define the encapsulation boundary for classes, methods, and member variables. Dynamics 365 F&O supports four distinct access scopes.

Comparison of Access Modifiers

ModifierScope within Declaring ClassDerived Classes in Same ModelDerived Classes in External ModelExternal Callers in Same ModelExternal Callers in External Model
publicAccessibleAccessibleAccessibleAccessibleAccessible
protectedAccessibleAccessibleAccessibleBlockedBlocked
internalAccessibleAccessibleBlockedAccessibleBlocked
privateAccessibleBlockedBlockedBlockedBlocked

The internal Modifier

Introduced to support modular architecture, the internal keyword restricts the visibility of a class, interface, or method strictly to the compilation model/package in which it is defined.

  • Encapsulating Private APIs: When building reusable framework models, core utility classes that should not become public contracts for ISVs or downstream customer extensions are marked internal.
  • Impact on Extensions: A class or method marked internal cannot be extended, subclassed, or invoked by any code residing in a different package. If an external model attempts to reference an internal class, the X++ compiler generates an accessibility violation error.

[!NOTE] Default Visibility in X++ Unlike C# where class members default to private, in X++ method declarations that omit an access modifier default to public. However, good engineering practice and Microsoft compiler linter rules mandate explicit modifier declarations on all classes, methods, and variables.


4. Class & Method Modifiers: abstract, final, and static

Modifiers govern inheritance behavior, method dispatch, and runtime execution boundaries.

Class Modifiers

  1. abstract Class:
    • Cannot be instantiated directly using new or factory methods.
    • Acts as a partial template or contract for derived classes.
    • Can contain both concrete implemented methods and abstract method signatures (which declare no body and end with a semicolon).
  2. final Class:
    • Prohibits inheritance. No other class can extend a class declared as final.
    • Used to enforce security, immutability, or deterministic execution.

Method Modifiers

  1. abstract Method:
    • Can only exist inside an abstract class.
    • Has no implementation body; terminating with ;.
    • Must be overridden in any non-abstract derived concrete class.
  2. final Method:
    • Prohibits overriding in derived subclasses.
  3. static Method:
    • Operates on the type itself rather than an instance.
    • Has no this pointer; cannot access instance variables or non-static methods.

[!WARNING] Critical Exam Rule: final Methods Block Chain of Command (CoC) Chain of Command (CoC) is the primary method-wrapping extensibility mechanism in modern Dynamics 365. However, methods declared as final CANNOT be wrapped using Chain of Command — unless the method's author explicitly opts back in with [Wrappable(true)], the documented override that makes a final method wrappable again. If a base application method is marked final, the compiler will reject any extension class attempting to wrap it. This is a favorite trick question on the MB-500 exam.


5. UI Method Modifiers: display, edit & Cache Optimization

In Dynamics 365 Finance and Operations, forms frequently need to display calculated or linked values that do not map directly to a single physical table column.

The display Method

A display method calculates and returns a read-only value for presentation on a form or report control. It can be declared on a Table or directly on a Form.

// Declared on CustTable
public display CustName customerFullName()
{
    return DirPartyTable::findRec(this.Party).Name;
}

The Client-Server Performance Bottleneck

When a grid displaying 50 rows renders on a web browser client, an un-cached display method declared on a table executes row-by-row on the Application Object Server (AOS). If the grid is scrolled, filtered, or redrawn, the AOS recalculates the display method repeatedly, triggering separate database queries and severe network latency.

Caching Display Methods: [SysClientCacheDataMethodAttribute]

To optimize performance, developers must instruct the AOS to cache display method return values on the client tier. Modern X++ uses the [SysClientCacheDataMethodAttribute] decorator:

// Cache the display method result on the client tier to avoid repeated AOS RPC calls
[SysClientCacheDataMethodAttribute(true)]
public display CustName customerFullName()
{
    return DirPartyTable::findRec(this.Party).Name;
}
  • true (Default Parameter): Caches the return value on the client tier. The method executes once when the record is fetched and is not re-executed unless the record buffer is refreshed or updated.
  • Form Initialization Caching (cacheAddMethod): On form datasources, developers can also register table display methods into the form's cache during init():
[ExtensionOf(formDataSourceStr(CustTable, CustTable))]
final class CustTableForm_Extension
{
    public void init()
    {
        next init();
        this.cacheAddMethod(tableMethodStr(CustTable, customerFullName));
    }
}

The edit Method

An edit method provides read/write access to calculated or unmapped values. It takes a boolean parameter indicating whether the user is setting or getting the value:

// Declared on a Table or Form Datasource
public edit boolean editOverrideCreditLimit(boolean _set, boolean _newValue)
{
    if (_set)
    {
        this.CreditMaxOverride = _newValue;
        this.modifiedField(fieldNum(CustTable, CreditMaxOverride));
    }
    return this.CreditMaxOverride;
}

6. Polymorphism, Interfaces & Safe Casting (is and as Operators)

Polymorphism enables treating instances of different subclasses through a common superclass or interface reference.

Defining and Implementing Interfaces

Interfaces define public contractual signatures without implementation:

public interface IPaymentGateway
{
    public boolean authorizePayment(AmountCur _amount, CurrencyCode _currency);
    public TransId capturePayment(AmountCur _amount);
}

public class StripePaymentGateway implements IPaymentGateway
{
    public boolean authorizePayment(AmountCur _amount, CurrencyCode _currency)
    {
        // Stripe API integration
        return true;
    }

    public TransId capturePayment(AmountCur _amount)
    {
        return "STRIPE-" + guid2Str(newGuid());
    }
}

Safe Type Checking and Casting: is and as

Prior to modern X++, casting an object to an incompatible type resulted in an unhandled CLR InvalidCastException that halted execution. Modern X++ provides the safe is and as keywords:

  1. is Operator (Type Verification):
    • Evaluates whether an object instance is compatible with a given class or interface type.
    • Returns a boolean true or false without throwing an exception.
  2. as Operator (Safe Casting):
    • Attempts to cast the object instance to the specified type.
    • If the object is incompatible, it evaluates to null instead of raising a runtime exception.
public static void processGateway(Object _unknownObject)
{
    // 1. Safe type checking using 'is'
    if (_unknownObject is IPaymentGateway)
    {
        // 2. Safe casting using 'as'
        IPaymentGateway gateway = _unknownObject as IPaymentGateway;
        if (gateway != null)
        {
            gateway.authorizePayment(150.00, "USD");
        }
    }
    else
    {
        warning("Provided object does not implement IPaymentGateway.");
    }
}

7. Scenario Walk-Through: Building an Extensible Tax Calculation Framework

Scenario Description

Contoso Retail requires a decoupled, polymorphic tax calculation framework. The system must support standard domestic sales tax, value-added tax (VAT) for European entities, and zero-rated export tax. Direct instantiation of concrete tax engines must be blocked, and the solution must dynamically return the appropriate calculator based on the delivery country code.

Step-by-Step Implementation Flow

  1. Define the Interface (ITaxCalculator):
    • Declare method AmountMST calculateTax(AmountMST _taxableAmount, TaxCode _taxCode).
  2. Declare Abstract Base Class (TaxCalculatorBase):
    • Declare as public abstract class TaxCalculatorBase implements ITaxCalculator.
    • Define protected void new() to prohibit direct instantiation.
    • Declare protected AddressCountryRegionId countryRegionId.
    • Declare public abstract AmountMST calculateTax(AmountMST _taxableAmount, TaxCode _taxCode);.
  3. Implement Static Factory Method (construct):
    • In TaxCalculatorBase, implement public static TaxCalculatorBase construct(AddressCountryRegionId _countryId).
    • Evaluate _countryId using a switch block and instantiate TaxCalculator_Domestic, TaxCalculator_VAT, or TaxCalculator_Zero.
  4. Implement Concrete Subclasses:
    • Implement concrete calculation logic in each subclass.
  5. Consume Polymorphically via Safe Casting:
    • In the posting pipeline, obtain the calculator via TaxCalculatorBase::construct(address.CountryRegionId) and invoke calculateTax().

8. Real-World Exam Traps: Class Architecture & Access Modifiers

[!WARNING] Exam Trap 1: Attempting Chain of Command on a final Method or Class An exam scenario asks why a developer's class extension fails to compile when wrapping a method on a standard class. If the target method or class is decorated with the final keyword, Chain of Command is strictly prohibited by the compiler. The developer must use pre/post events (if hookable) or raise an extensibility request.

[!WARNING] Exam Trap 2: Omitting Display Method Caching on Form Grids A scenario describes a form grid where scrolling is sluggish and users experience notable latency. The question asks how to optimize the display method. The correct solution is adding [SysClientCacheDataMethodAttribute(true)] to the table display method or calling cacheAddMethod() in the form datasource init() method.

[!WARNING] Exam Trap 3: Direct Class Hard-Casting Causing Unhandled Exceptions An exam question shows code like CustInvoiceTable invoice = (CustInvoiceTable)_commonRecord; and asks what happens if _commonRecord is a SalesTable. Hard casting throws an unhandled CLR InvalidCastException. The robust pattern uses _commonRecord as CustInvoiceTable followed by a null check.

[!WARNING] Exam Trap 4: Calling internal Classes from an External Model A question presents two packages: ModelA (which defines an internal class InventoryInternalUtility) and ModelB (which references ModelA). If code in ModelB attempts to call InventoryInternalUtility::run(), the project fails to compile because internal restricts visibility strictly to ModelA.

Loading diagram...
X++ Object-Oriented Architecture, Visibility Matrix, and Factory Lifecycle
Test Your Knowledge

A developer in ModelB attempts to instantiate and execute a utility class defined in ModelA. ModelB has a reference to ModelA. However, the X++ compiler generates an error indicating that the class cannot be accessed. What access modifier on the class in ModelA is causing this compilation failure?

A
B
C
D
Test Your Knowledge

A table display method calculates customer credit balances and renders in a grid displaying hundreds of records. Users report that scrolling the grid causes severe client-server latency because the display method executes repeatedly. How should the developer optimize this display method?

A
B
C
D
Test Your Knowledge

A developer needs to evaluate whether a generic Object instance implements the IInvoiceFormatter interface and call a formatting method without risking an unhandled runtime InvalidCastException. Which X++ construct should the developer use?

A
B
C
D
Test Your Knowledge

A developer attempts to wrap a business logic method on a standard application class using Chain of Command (CoC) in an extension class. Visual Studio reports a compilation error preventing the method extension. What is the root cause of this error?

A
B
C
D