X++ Chain of Command vs event handlers: which to use?
To extend a standard D365 F&O method, prefer Chain of Command; keep event handlers for table events and existing delegates. Rules, examples and pitfalls.
The short answer
To change the behaviour of a public or protected method of a class, table, data entity or form, use Chain of Command (CoC): an extension class that wraps the method and calls next. Microsoft presents it as the richer, more robust mechanism; its delegates documentation even recommends not creating new delegates and using CoC instead.
Event handlers remain useful in three cases: reacting to a table's data events (Inserting, ValidatedWrite and so on), subscribing to a delegate Microsoft already publishes, and legacy pre/post handlers on code that cannot be wrapped. Microsoft notes that pre/post handlers exist mainly for backward compatibility and break easily when a method's signature changes.
How does Chain of Command work?
You create a final class decorated with ExtensionOf, then redeclare the method with the same signature. The call to next runs the next method in the chain and, at the end, the original implementation. Example from Microsoft Learn:
[ExtensionOf(classStr(BusinessLogic1))]
final class BusinessLogic1_Extension
{
str doSomething(int arg)
{
// Before
var s = next doSomething(arg + 4);
// After
return s;
}
}What CoC gives you: access to the public and protected methods and variables of the extended class, the ability to act before and after the standard call, change parameters or the return value, and manage a transaction in the same method. It works for instance and static methods (with the static keyword), for tables, data entities, forms and, through one extension per concept, for form data sources, data fields and controls.
For a table, the same principle applies:
[ExtensionOf(tableStr(TableToExtend))]
final class TableToExtend_Extension
{
public boolean validateWrite()
{
boolean ret;
ret = next validateWrite();
// Extra check
return ret;
}
}Which rules apply to next?
The wrapper method must always call next, at the first level of the method body:
- no call to next inside an if, or inside a while, do-while or for loop;
- no return before next;
- no next inside a logical expression;
- next can sit inside a try/catch/finally from Platform update 21.
Exception: if the standard method carries the Replaceable attribute, the compiler does not enforce the call to next; Microsoft then expects the chain to be broken only conditionally.
Other documented limits: a method marked final or [Wrappable(false)] cannot be wrapped (except [Wrappable(true)] on a final method); [Hookable(false)] blocks both CoC and pre/post handlers; constructors cannot be extended; a parameter's default value must not be repeated in the wrapper's signature; the execution order between several extensions of the same method is not guaranteed.
When should you keep an event handler?
Tables publish data events before and after their methods: ValidatingWrite / ValidatedWrite, Inserting / Inserted, Updating / Updated, Deleting / Deleted, ModifyingField / ModifiedField, InitializingRecord / InitializedRecord, and so on. Subscribing to them (DataEventHandler) is simple and readable to fill a field or add a check. Validation events return their result through DataEventArgs.
Delegates published by Microsoft are consumed with the SubscribesTo attribute; the call order between subscribers cannot be set.
Finally, pre/post handlers (PreHandlerFor / PostHandlerFor) still exist:
[PostHandlerFor(classStr(MyClass2), methodStr(MyClass2, publisher))]
public static void PostHandler(XppPrePostArgs arguments)
{
int arg = arguments.getArg("i");
int retvalFromMethod = arguments.getReturnValue();
}They read arguments by name and the return value through XppPrePostArgs: a parameter renamed on Microsoft's side is enough to break them. On a wrappable method, prefer CoC.
Common mistakes
- Calling next inside an if to "short-circuit" the standard: rejected by the compiler and against the extension model.
- Forgetting final on the extension class, or using ExtensionOf with the wrong type (classStr, tableStr, formStr, formDataSourceStr…).
- Copying a parameter's default value into the wrapper's signature.
- Wrapping a "pure" X++ method defined in a form data source or control: Learn says the code compiles but the extension is not called at run time.
- Creating a new delegate in your own code instead of small, well-named methods that others can wrap.
- Assuming an execution order between several extensions.
And with AI-generated code?
The CoC / event handler choice is a design decision every code review should check, whether a human or an AI wrote the extension. At Skalp AI, X++ code is generated as extensions (never over-layering), comes with unit tests, is delivered to your Azure DevOps and validated by your IT lead before merge. See custom D365 F&O extensions and, if your code still contains over-layering, moving from over-layering to extensions. For the bigger picture: building X++ without a consultant in 2026. Pricing: Basic, Pro and Max plans, 1 credit (≈ €400 on Basic, €200 on Pro) consumed only when the code is generated.
Sources
- Microsoft Learn, "Class extension - Method wrapping and Chain of Command", updated 27 Mar 2026, accessed 11 Oct 2026.
- Microsoft Learn, "Events and delegates", updated 31 Mar 2026, accessed 11 Oct 2026.
- Microsoft Learn, "Customize through extension and overlayering" (table events), updated 27 Mar 2026, accessed 11 Oct 2026.
- Microsoft Learn, "Delegates" and "Attributes that make methods extensible", accessed 11 Oct 2026.
Frequently asked questions
Can you skip calling next in a Chain of Command method?
No, unless the standard method carries the Replaceable attribute. Otherwise next must be called at the first level of the method, outside any if, loop or logical expression.
Should you still create delegates?
Microsoft recommends not creating new ones: you can subscribe to existing delegates, but for your own code small, well-named methods extensible through Chain of Command are better.
Why can't my method be wrapped?
It is probably private, marked final without [Wrappable(true)], marked [Wrappable(false)] or [Hookable(false)], or it is a constructor. Also check it is not a pure X++ method of a form data source or control.
Ready to transform your D365?
Join the CIOs who develop their D365 tickets in 1h for 1 credit (≈ €400 on Basic, €200 on Pro).