Domain 3 β€” Module 5 of 5 100%
10 of 28 overall
Domain 3: Design and Develop AOT Elements Free ⏱ ~14 min read

Classes, Events, and Extensions

Create X++ classes in the AOT, extend standard application behaviour using the extension model and event handlers, and apply attributes like SysEntryPointAttribute and DataMemberAttribute. This module covers AOT class design β€” Domain 4 covers X++ coding patterns in depth.

Classes in D365 F&O

Simple explanation

Think of a recipe card.

A recipe card (class) has two parts: the ingredients list (variables/properties) and the cooking steps (methods). You can have a recipe for β€œChocolate Cake” and another for β€œVanilla Cake” β€” they share the same structure (both are cakes) but have different ingredients and steps.

In D365, a class is a recipe card for a business process. The β€œPost Invoice” class has the data it needs (which invoice, which customer) and the steps to follow (validate, calculate tax, create voucher, update balances). Other developers can extend the recipe β€” adding a step like β€œsend email notification” β€” without rewriting the whole thing.

The AOT (Application Object Tree) is the filing cabinet where all these recipe cards are stored and organised.

Creating a class

In Visual Studio: right-click your project β†’ Add New Item β†’ Code β†’ Class.

Class structure

/// <summary>
/// Handles inspection validation logic for Axion Dynamics.
/// </summary>
class AxionInspectionValidator
{
    // Member variables (state)
    AxionInspectionTable inspection;
    boolean isValid;

    // Constructor β€” protected so callers use construct() or newFrom()
    protected void new()
    {
    }

    // Static construct method β€” standard factory pattern
    public static AxionInspectionValidator construct()
    {
        return new AxionInspectionValidator();
    }

    // Initialise from an inspection record
    public AxionInspectionValidator initFromInspection(AxionInspectionTable _inspection)
    {
        inspection = _inspection;
        return this;   // fluent pattern β€” allows method chaining
    }

    // Business logic method
    public boolean validate()
    {
        isValid = true;

        if (!inspection.InspectionId)
        {
            isValid = checkFailed("@AxionLabels:InspectionIdRequired");
        }

        if (inspection.InspectionDate > today())
        {
            isValid = checkFailed("@AxionLabels:FutureDateNotAllowed");
        }

        if (!HcmWorker::exist(inspection.InspectorWorker))
        {
            isValid = checkFailed("@AxionLabels:InspectorNotFound");
        }

        return isValid;
    }
}

Calling the class

// Usage pattern
AxionInspectionValidator validator = AxionInspectionValidator::construct()
    .initFromInspection(inspectionRecord);

if (validator.validate())
{
    // proceed with posting
}
Exam tip: construct() vs new()

The standard pattern in D365 F&O is:

  • new() β€” protected, never called directly from outside the class
  • construct() β€” public static, the factory method callers use

Why? This pattern allows Microsoft (or you) to introduce subclasses later without breaking callers. The construct method can return a subclass based on parameters.

The exam may ask: β€œA developer calls new AxionInspectionValidator() directly. What is wrong?” Answer: the new() method is protected β€” use the static construct() method instead.

Access modifiers

ModifierVisibilityUse Case
publicAccessible from anywhereAPI methods, static helpers
protectedAccessible from this class and subclassesInternal state management, overridable logic
privateAccessible from this class onlyImplementation details, helper methods
internalAccessible from the same model onlyCross-class helpers within your model, not exposed to other models
Question

What does the 'internal' access modifier do in D365 F&O X++?

Click or press Enter to reveal answer

Answer

Internal restricts visibility to the same model. Other models cannot see or call internal methods. Use it for helper classes and methods that should be available across your model but not exposed as a public API to other ISV solutions or Microsoft updates.

Click to flip back

Extending standard classes

Since Microsoft’s classes are sealed (no inheritance), you use two patterns to add or modify behaviour:

Pattern 1: Class extensions (augmentation)

A class extension adds new methods to an existing class without modifying the original:

// Adds a new method to InventTable (Microsoft's class)
[ExtensionOf(tableStr(InventTable))]
final class AxionInventTable_Extension
{
    // New method β€” callable as inventTable.axionGetInspectionRequired()
    public boolean axionGetInspectionRequired()
    {
        // Access the current record via 'this'
        return this.AxionInspectionRequired == NoYes::Yes;
    }
}

Pattern 2: Event handlers (subscribe to events)

Event handlers run before, after, or around a standard method:

// Subscribes to the validateWrite event on CustTable
class AxionCustTable_EventHandler
{
    // Runs after CustTable.validateWrite() β€” adds extra validation
    [DataEventHandler(tableStr(CustTable), DataEventType::ValidatingWrite)]
    public static void CustTable_onValidatingWrite(
        Common _sender,
        DataEventArgs _eventArgs)
    {
        ValidateEventArgs validateArgs = _eventArgs as ValidateEventArgs;
        CustTable custTable = _sender as CustTable;

        if (!custTable.AxionPreferredInspectorWorker)
        {
            validateArgs.parmValidateResult(false);
            checkFailed("@AxionLabels:InspectorRequired");
        }
    }
}

Class extension vs event handler

Extensions add new methods; event handlers modify existing behaviour
FeatureClass ExtensionEvent Handler
What it doesAdds NEW methods to an existing class/tableSubscribes to EXISTING events on a class/table
Can modify existing logic?No β€” only adds new methodsYes β€” can run before/after existing methods and modify results
Access to private members?No β€” only public/protected membersNo β€” receives event args and sender object
Syntax[ExtensionOf(classStr(...))] final class[PostHandlerFor(...)] or [DataEventHandler(...)] static void
Use caseAdding helper methods, computed valuesValidation, logging, notifications, modifying standard behaviour
Naming conventionAxionClassName_ExtensionAxionClassName_EventHandler
Question

What is the key difference between a class extension and an event handler in D365 F&O?

Click or press Enter to reveal answer

Answer

A class extension adds NEW methods to an existing class β€” it cannot modify existing methods. An event handler subscribes to events on existing methods (pre/post) and can modify their behaviour β€” validate results, add logging, change parameters. Use extensions for new functionality, event handlers for modifying existing behaviour.

Click to flip back

Event types

Pre and post event handlers on methods

class AxionSalesConfirm_EventHandler
{
    // Runs BEFORE SalesConfirmJournalPost.validate()
    [PreHandlerFor(classStr(SalesConfirmJournalPost), methodStr(SalesConfirmJournalPost, validate))]
    public static void preValidate(XppPrePostArgs _args)
    {
        // Access the instance that called validate()
        SalesConfirmJournalPost instance = _args.getThis() as SalesConfirmJournalPost;

        info("Pre-validation: checking Axion quality requirements...");
    }

    // Runs AFTER SalesConfirmJournalPost.validate()
    [PostHandlerFor(classStr(SalesConfirmJournalPost), methodStr(SalesConfirmJournalPost, validate))]
    public static void postValidate(XppPrePostArgs _args)
    {
        boolean originalResult = _args.getReturnValue();

        if (originalResult)
        {
            // Additional validation β€” can override the return value
            // _args.setReturnValue(false); // to fail validation
        }
    }
}

Data events on tables

EventWhen It Fires
onInserting / onInsertedBefore / after a record is inserted
onUpdating / onUpdatedBefore / after a record is updated
onDeleting / onDeletedBefore / after a record is deleted
onValidatingWriteDuring validateWrite() β€” can accept or reject
onValidatingDeleteDuring validateDelete() β€” can accept or reject
onInitializedRecordWhen a new record buffer is initialised (default values)
onModifiedFieldWhen a field value changes on the form
Scenario: Vik adds quality checks to sales confirmation

Axion needs a quality gate before sales order confirmation: if the order contains items flagged for inspection, confirmation should be blocked until inspection passes.

Vik cannot modify SalesConfirmJournalPost directly β€” it’s Microsoft’s class. Instead, he creates a PostHandlerFor on the validate() method. His handler checks whether any order lines reference inspection-required items. If an item hasn’t passed inspection, he overrides the return value to false and shows an error.

Zero changes to Microsoft’s code. When Microsoft updates the SalesConfirmJournalPost class, Vik’s handler continues to work because it subscribes to the event, not the implementation.

Key attributes

Attributes are metadata annotations that tell the runtime how to treat a class or method.

SysEntryPointAttribute

Required on every service method exposed to external callers (AIF, OData custom actions):

[SysEntryPointAttribute(true)]  // true = authorisation check enforced
public AxionInspectionResult performInspection(AxionInspectionId _inspectionId)
{
    // This method can be called from outside via service endpoint
    // The attribute tells the security framework to check the caller's privileges
}

DataMemberAttribute

Marks properties on a data contract class for serialisation (used in service contracts, batch jobs, SysOperation framework):

[DataContractAttribute]
class AxionInspectionContract
{
    AxionInspectionId inspectionId;
    TransDate fromDate;
    TransDate toDate;

    [DataMemberAttribute('InspectionId')]
    public AxionInspectionId parmInspectionId(
        AxionInspectionId _inspectionId = inspectionId)
    {
        inspectionId = _inspectionId;
        return inspectionId;
    }

    [DataMemberAttribute('FromDate')]
    public TransDate parmFromDate(TransDate _fromDate = fromDate)
    {
        fromDate = _fromDate;
        return fromDate;
    }

    [DataMemberAttribute('ToDate')]
    public TransDate parmToDate(TransDate _toDate = toDate)
    {
        toDate = _toDate;
        return toDate;
    }
}

Other important attributes

AttributePurpose
SysOperationContractProcessingAttributeLinks a service operation to its UI builder
SysEntryPointAttributeSecurity entry point for service methods
DataContractAttributeMarks a class as a serialisable data contract
DataMemberAttributeMarks a property for serialisation
ExtensionOfDeclares a class extension (augmentation)
PreHandlerFor / PostHandlerForSubscribes to pre/post events on methods
ExportAttributeMarks a class for dependency injection
Question

What happens if you expose a service method without the SysEntryPointAttribute?

Click or press Enter to reveal answer

Answer

The security framework cannot enforce authorisation checks on the method. At runtime, calls to the method may fail with a security error because the framework doesn't know this is a valid entry point. Always add [SysEntryPointAttribute(true)] to service methods.

Click to flip back

Question

What is a data contract class and when do you use one?

Click or press Enter to reveal answer

Answer

A data contract class is marked with [DataContractAttribute] and has properties decorated with [DataMemberAttribute]. It's used for serialisation β€” passing structured data to service operations, batch jobs (SysOperation framework), and external integrations. The parm* naming convention (parmFromDate, parmToDate) is standard.

Click to flip back

The parm* naming convention

D365 F&O uses a distinctive pattern for getter/setter methods:

// The parm method acts as both getter and setter
[DataMemberAttribute('InspectionId')]
public AxionInspectionId parmInspectionId(
    AxionInspectionId _inspectionId = inspectionId)
{
    inspectionId = _inspectionId;
    return inspectionId;
}

// Calling as getter:
AxionInspectionId id = contract.parmInspectionId();

// Calling as setter:
contract.parmInspectionId('INS-001');

The default parameter (= inspectionId) makes this work: when called without arguments, it returns the current value. When called with an argument, it sets and returns the new value.

Exam tip: parm methods and data contracts

The exam frequently tests the parm pattern:

  • Naming: parm + PropertyName (e.g., parmFromDate)
  • Default parameter: Must default to the member variable
  • DataMemberAttribute: Required for serialisation
  • Return type: Must match the member variable type

If a question shows a data contract with [DataMemberAttribute] on a method that doesn’t follow the parm pattern, that’s likely the error they’re testing.

Extension vs overlayering: the fundamental rule

D365 F&O enforces extension-based development β€” overlayering is not supported
ApproachOverlayering (BANNED)Extension (REQUIRED)
How it worksDirectly modifying Microsoft's source codeAdding new elements or subscribing to events in a separate model
Allowed in D365 F&O?No β€” blocked for most models since Platform Update 9+Yes β€” the only supported customisation approach
Upgrade impactEvery Microsoft update could break your changesExtensions survive updates β€” they're separate from the base code
TechniquesCopy and edit standard class/methodClass extension, event handler, table extension, form extension
Code conflictsYes β€” merge conflicts on every updateNo β€” your code is in a separate model
Scenario: Sophie migrates from AX 2012 overlayering

Sophie’s company Ferris Industries is migrating from AX 2012 where they overlayered dozens of standard classes. Mentor Carl explains the migration path:

β€œIn AX 2012, we copied SalesFormLetter and added 50 lines of custom code. In D365, we can’t touch that class. Instead, we:

  1. Create a class extension for any new methods we need
  2. Create event handlers for the existing methods we modified
  3. Move our custom logic into these extension points

The result? When Microsoft updates SalesFormLetter, our code continues to work because it’s in a separate layer.”

Putting it together: when to use what

NeedApproach
Add a new helper method to CustTableClass extension with [ExtensionOf(tableStr(CustTable))]
Run extra validation before a standard post[PreHandlerFor] event handler on the post method
Log every time an invoice is created[PostHandlerFor] on the insert event or onInserted data event
Expose a new service endpointNew class with [SysEntryPointAttribute] on the service method
Pass parameters to a batch jobData contract class with [DataContractAttribute] and [DataMemberAttribute]
Create business logic for a new moduleNew class with construct() pattern, organised by responsibility
Knowledge Check

Vik needs to add custom validation that runs whenever a CustTable record is saved. He cannot modify the standard CustTable class. Which approach should he use?

Knowledge Check

A developer creates a data contract class for a batch job. The class has [DataContractAttribute] on the class and three parm methods for FromDate, ToDate, and CustomerId. But when the batch dialog opens, the CustomerId field doesn't appear. What is the most likely cause?

Knowledge Check

Elena needs to add a method to the standard InventTable that calculates a custom inspection score. She creates: [ExtensionOf(tableStr(InventTable))] final class AxionInventTable_Extension. The method compiles but when she calls inventTable.axionCalculateScore(), she gets a compile error 'method not found'. What went wrong?

Knowledge Check

Which attribute must be applied to a service method that will be called externally via an OData custom action?


Next up: X++ Data Manipulation β€” dive into X++ coding patterns for selecting, inserting, updating, and deleting data (Domain 4 begins).