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.

Ext 1 — QWO Model before next after next Ext 2 — ISV Model before next after next Base — Standard original logic return value next next — next call -- return Runtime-composed call chain outermost (depends on ISV) intermediate innermost (standard)
Model dependency determines chain position. An extension in a model that references another extension's model wraps it from the outside.

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 the classStr, tableStr or formStr intrinsic functions. Using a string literal or the wrong target silently creates a dead wrapper.
  • A final modifier 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 pointTrigger mechanismTypical use caseCoupling level
Chain of CommandWraps a method; next delegates to the next linkAdd pre/post logic to a specific existing method when no event existsMedium — tied to one method signature
Event handler ([SubscribesTo])Platform raises a pre/post event; handler reactsReact to a standard pre/post event without touching the method itselfLow — decoupled from the method body
DelegateStandard code explicitly raises a delegate; subscriber handles itDesigned extension points where Microsoft intentionally exposes a hookVery low — stable contract
SysExtension / factoryRuntime resolves the correct implementation by attributePlug-in strategies: tax engines, document handlers, warehouse strategiesLow — 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 static helper 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 skipDataMethods flag 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

  1. Is this the most stable extension point — or does a delegate, event or SysExtension exist?
  2. Is next called exactly once on every code path, including exception paths?
  3. Are standard return values and exceptions preserved?
  4. Is the code safe in batches, data entities and high-volume imports?
  5. Are all SQL accesses indexed and bounded?
  6. Are external side effects decoupled from the transaction using an outbox or queue?
  7. Does naming follow Xxx_Extension convention and identify the model?
  8. Does the work item record the requirement, risk, impacted scenarios and regression tests?
  9. Has the wrapper been tested against a realistic import volume?

Microsoft Learn references

No section matches this search.