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.
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
extendsand implement one or more contracts viaimplements. In enterprise X++, class instantiation is governed by the staticconstruct()factory pattern, which hides the parameterlessnew()constructor to enforce controlled initialization and polymorphic subclass selection. Access visibility is governed by four modifiers:public,protected,private, andinternal(which restricts visibility strictly to the declaring model/package). Method modifiers includeabstract,final(which prohibits overriding and blocks Chain of Command wrapping), andstatic. For user interfaces,displaymethods compute values dynamically and must be cached using[SysClientCacheDataMethodAttribute(true)]to prevent severe grid rendering lag. Type inspection and casting are safely handled via theisandasoperators.
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 classObject. - 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 theSystem.IDisposablepattern.
// 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:
- Polymorphic Factory Delegation: Calling
newbinds the caller to a concrete class. A static factory method can inspect parameters, configuration keys, or country-region codes and return a specialized subclass. - Extensibility & Hooking: Direct instantiation via
newcannot be easily intercepted by extension frameworks if subclass substitution is required. - 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
| Modifier | Scope within Declaring Class | Derived Classes in Same Model | Derived Classes in External Model | External Callers in Same Model | External Callers in External Model |
|---|---|---|---|---|---|
public | Accessible | Accessible | Accessible | Accessible | Accessible |
protected | Accessible | Accessible | Accessible | Blocked | Blocked |
internal | Accessible | Accessible | Blocked | Accessible | Blocked |
private | Accessible | Blocked | Blocked | Blocked | Blocked |
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
internalcannot 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 topublic. 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
abstractClass:- Cannot be instantiated directly using
newor 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).
- Cannot be instantiated directly using
finalClass:- Prohibits inheritance. No other class can extend a class declared as
final. - Used to enforce security, immutability, or deterministic execution.
- Prohibits inheritance. No other class can extend a class declared as
Method Modifiers
abstractMethod:- Can only exist inside an
abstractclass. - Has no implementation body; terminating with
;. - Must be overridden in any non-abstract derived concrete class.
- Can only exist inside an
finalMethod:- Prohibits overriding in derived subclasses.
staticMethod:- Operates on the type itself rather than an instance.
- Has no
thispointer; cannot access instance variables or non-static methods.
[!WARNING] Critical Exam Rule:
finalMethods Block Chain of Command (CoC) Chain of Command (CoC) is the primary method-wrapping extensibility mechanism in modern Dynamics 365. However, methods declared asfinalCANNOT be wrapped using Chain of Command — unless the method's author explicitly opts back in with[Wrappable(true)], the documented override that makes afinalmethod wrappable again. If a base application method is markedfinal, 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 duringinit():
[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:
isOperator (Type Verification):- Evaluates whether an object instance is compatible with a given class or interface type.
- Returns a boolean
trueorfalsewithout throwing an exception.
asOperator (Safe Casting):- Attempts to cast the object instance to the specified type.
- If the object is incompatible, it evaluates to
nullinstead 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
- Define the Interface (
ITaxCalculator):- Declare method
AmountMST calculateTax(AmountMST _taxableAmount, TaxCode _taxCode).
- Declare method
- 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);.
- Declare as
- Implement Static Factory Method (
construct):- In
TaxCalculatorBase, implementpublic static TaxCalculatorBase construct(AddressCountryRegionId _countryId). - Evaluate
_countryIdusing a switch block and instantiateTaxCalculator_Domestic,TaxCalculator_VAT, orTaxCalculator_Zero.
- In
- Implement Concrete Subclasses:
- Implement concrete calculation logic in each subclass.
- Consume Polymorphically via Safe Casting:
- In the posting pipeline, obtain the calculator via
TaxCalculatorBase::construct(address.CountryRegionId)and invokecalculateTax().
- In the posting pipeline, obtain the calculator via
8. Real-World Exam Traps: Class Architecture & Access Modifiers
[!WARNING] Exam Trap 1: Attempting Chain of Command on a
finalMethod 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 thefinalkeyword, 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 callingcacheAddMethod()in the form datasourceinit()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_commonRecordis aSalesTable. Hard casting throws an unhandled CLRInvalidCastException. The robust pattern uses_commonRecord as CustInvoiceTablefollowed by anullcheck.
[!WARNING] Exam Trap 4: Calling
internalClasses from an External Model A question presents two packages:ModelA(which defines aninternal class InventoryInternalUtility) andModelB(which referencesModelA). If code inModelBattempts to callInventoryInternalUtility::run(), the project fails to compile becauseinternalrestricts visibility strictly toModelA.
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 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 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 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?