When should Chain of Command be used?
CoC is appropriate when a requirement must run before or after an existing class, table, form or data entity method and no more explicit business event is available. It keeps the customization in a separate extension model and provides access to public and protected members.
Before adding a wrapper, I check configuration, standard events, delegates, event handlers and SysExtension strategies. The goal is not to select the most powerful hook, but the least coupled and most stable extension point.
Microsoft documents wrapping rules in Class extension – Method wrapping and Chain of Command.
How the runtime composes the chain
When multiple extensions wrap the same method, the D365 runtime stacks them according to model dependency order: an extension in a model that depends on another extension's model runs on the outside of the chain. Extensions in independent models have no guaranteed relative order.
Practical implications for design:
- Two extensions from independent models (no dependency between them) have no guaranteed order. Each wrapper must be self-contained and must not rely on the side effects of another extension's pre- or post-logic.
- If order matters, make the dependency explicit in the model descriptor and document the reason in the work item.
- The
[ExtensionOf]attribute must reference the exact class, table or form using theclassStr,tableStrorformStrintrinsic functions. Using a string literal or the wrong target silently creates a dead wrapper. - A
finalmodifier on the extension class prevents another extension from wrapping the extension class itself. This is the standard pattern for platform extensions.
Understand the next contract
next forwards execution to the next link, which can be another extension or the base implementation. The order between independent extensions is not a safe functional contract. Every wrapper must therefore remain self-contained.
[ExtensionOf(classStr(SalesFormLetter_Invoice))]
final class QwoSalesFormLetterInvoice_Extension
{
protected void run()
{
QwoInvoiceTelemetry telemetry = QwoInvoiceTelemetry::start(this.parmId());
try
{
next run();
telemetry.markSucceeded();
}
catch (Exception::Error)
{
telemetry.markFailed();
throw;
}
}
}
The wrapper calls standard logic once, preserves the exception and delegates observability to a dedicated component.
Choose logic before or after next
Before next
- Validate preconditions and reject the operation early.
- Normalize or enrich parameters.
- Start telemetry or a trace context.
- Capture
orig()state on table extensions before the write.
After next
- React to the standard result.
- Publish a domain event or business event.
- Enrich or translate the return value.
- Record successful execution and metrics.
Changing input parameters or the return value changes the method contract. Document this in the work item and cover it with focused regression tests. The standard method's callers may depend on the original contract.
Choosing the right extension point
CoC is not always the correct mechanism. The following table compares the four main extension patterns available in D365 F&O:
| Extension point | Trigger mechanism | Typical use case | Coupling level |
|---|---|---|---|
| Chain of Command | Wraps a method; next delegates to the next link | Add pre/post logic to a specific existing method when no event exists | Medium — tied to one method signature |
Event handler ([SubscribesTo]) | Platform raises a pre/post event; handler reacts | React to a standard pre/post event without touching the method itself | Low — decoupled from the method body |
| Delegate | Standard code explicitly raises a delegate; subscriber handles it | Designed extension points where Microsoft intentionally exposes a hook | Very low — stable contract |
| SysExtension / factory | Runtime resolves the correct implementation by attribute | Plug-in strategies: tax engines, document handlers, warehouse strategies | Low — replaces, rather than wraps |
A delegate or pre/post event handler is always preferable to CoC when available, because it is a Microsoft-designed stable contract. Use the extensibility changelog to identify new hooks in each release that can replace existing CoC wrappers.
High-risk area: table methods
insert, update, delete and validateWrite can run from forms, batches, data entities and integrations. Network calls or per-row non-indexed queries quickly become performance incidents.
[ExtensionOf(tableStr(CustTable))]
final class QwoCustTable_Extension
{
public void update()
{
boolean creditGroupChanged = this.orig().CreditRating != this.CreditRating;
next update();
if (creditGroupChanged)
{
QwoCustomerDomainEvent::enqueueCreditRatingChanged(this.AccountNum);
}
}
}
The extension captures original state, lets the platform complete the update and queues asynchronous work instead of calling an external endpoint inside the interactive transaction.
Performance and bulk execution
- Avoid extra selects in per-record methods. Cache lookups in a local variable or a
statichelper if the value is stable within the transaction. - Never call HTTP services from a table transaction. Introduce an outbox pattern (batch queue) instead.
- Use queues, business events or batches for asynchronous effects.
- Measure realistic volume, not only a single DEV scenario. Use trace parser and the SQL statement log.
- Assess DMF imports and set-based operations explicitly.
- Consider the
skipDataMethodsflag on data entity inserts; CoC fires regardless unless the entity skips it.
A 20 ms overhead looks harmless in a form but adds more than 30 minutes across 100,000 records. Validate every table-method wrapper against the target import volume before releasing to production.
Make the extension testable
Keep the wrapper focused on adaptation and orchestration. Move the rule into a domain component that SysTest can exercise directly.
public final class QwoCreditPolicy
{
public static boolean canRelease(SalesTable _salesTable)
{
return _salesTable.CustAccount
&& _salesTable.SalesStatus == SalesStatus::Backorder
&& CustTable::find(_salesTable.CustAccount).CreditMax > 0;
}
}
Unit tests cover the policy; one integration test confirms that the standard method reaches the wrapper. This separation produces fewer brittle tests and simpler update reviews.
When the dependency under test cannot be replaced through a constructor parameter, use an abstract base approach to inject a test double:
// Testable service class with overridable dependency
public class QwoInvoiceTelemetry
{
// Override in SysTest subclass to capture calls without real telemetry
protected void writeEvent(str _message, boolean _succeeded)
{
SysInformationLog::add(_message);
}
public static QwoInvoiceTelemetry start(RecId _invoiceId)
{
QwoInvoiceTelemetry t = new QwoInvoiceTelemetry();
// ... initialise context
return t;
}
}
// SysTest class
class QwoInvoiceTelemetryTest extends SysTestCase
{
public void testMarkSucceededLogsEvent()
{
QwoInvoiceTelemetry telemetry = QwoInvoiceTelemetry::start(12345);
telemetry.markSucceeded();
// assert via captured write
}
}
For CoC wrappers on form data sources or RunBase classes, prefer focused integration tests that exercise the full flow in a controlled dataset rather than attempting to unit-test the wrapper in isolation.
Tech Lead review checklist
- Is this the most stable extension point — or does a delegate, event or SysExtension exist?
- Is
nextcalled exactly once on every code path, including exception paths? - Are standard return values and exceptions preserved?
- Is the code safe in batches, data entities and high-volume imports?
- Are all SQL accesses indexed and bounded?
- Are external side effects decoupled from the transaction using an outbox or queue?
- Does naming follow
Xxx_Extensionconvention and identify the model? - Does the work item record the requirement, risk, impacted scenarios and regression tests?
- Has the wrapper been tested against a realistic import volume?