xpp-best-practice-rules

star 12

Best-practice (BP) rules every generated X++ file must satisfy — today/DateTimeUtil, label-typed messages, EDT migration, nested loops, alternate keys, doc comments, label existence. Invoke whenever you scaffold or edit X++ that will eventually be linted by xppbp.exe.

dynamics365ninja By dynamics365ninja schedule Updated 5/25/2026

name: xpp-best-practice-rules description: Best-practice (BP) rules every generated X++ file must satisfy — today/DateTimeUtil, label-typed messages, EDT migration, nested loops, alternate keys, doc comments, label existence. Invoke whenever you scaffold or edit X++ that will eventually be linted by xppbp.exe. applies_when: User intent involves authoring X++ that must pass d365fo bp check / xppbp.exe — i.e. virtually all D365FO code.

NEVER write X++ AOT XML files directly via PowerShell, terminal file commands (Set-Content, Out-File, New-Item), editor write tools, or any raw text approach. The XML schema (<AxClass>, <AxTable>, <AxForm>, <Methods>, <SourceCode>) is proprietary — LLMs have not been trained on it reliably. ALWAYS use d365fo generate … commands to produce correct AOT XML. If d365fo is unavailable in PATH, stop and ask the user to install it.

Best-practice rules — generated X++ must be BP-clean

Source of truth: d365fo bp check — the Windows-VM runner that executes xppbp.exe. The list below covers the non-negotiable BP rules every scaffold and hand-edit must satisfy out of the box.

Per-rule rules

BPUpgradeCodeTodaytoday() is forbidden

today() ignores the user's preferred time-zone. Always use:

TransDate today = DateTimeUtil::getToday(DateTimeUtil::getUserPreferredTimeZone());

BPErrorLabelIsText — no hardcoded UI strings

Every string passed to info() / warning() / error() / Box::yesNo() etc. must be a label token of the form @File:Key. Search before you create:

d365fo labels search "Vehicle is required" --lang en-us --output json
d365fo labels resolve @SYS12345                          # confirm an existing token

If no match → create the label via your model's labels file, then reference it. Never inline.

BPErrorEDTNotMigrated — modern EDT relations

EDT relations must use the EDT.Relations element, not the legacy table-level relations on the EDT. The CLI's d365fo generate edt and d365fo generate extension edt already emit the modern shape — preserve it when hand-editing.

BPCheckNestedLoopinCode — no nested data-access loops

Nested while select blocks are forbidden:

// ❌ WRONG
while select custTable
{
    while select custInvoiceJour where custInvoiceJour.OrderAccount == custTable.AccountNum
    { … }
}

// ✅ CORRECT — single join
while select custTable
    join custInvoiceJour
    where custInvoiceJour.OrderAccount == custTable.AccountNum
{ … }

For filter-only joins use exists join / notExists join. For complex correlations pre-load to a Map or temp table.

BPCheckAlternateKeyAbsent — every table needs an alternate key

A unique index on the natural key, marked AlternateKey = Yes. The CLI's d365fo generate table template emits a <Pkey> index with AlternateKey = Yes — don't strip it.

BPErrorUnknownLabel — labels referenced must exist

@File:Key tokens must resolve to a real entry in an indexed label file. Confirm with:

d365fo labels resolve @File:Key --lang en-us,cs --output json

If the result is ok:false with LABEL_NOT_FOUND, stop and either pick an existing label (d365fo labels search …) or add the entry to the model's labels file before referencing it.

BPXmlDocNoDocumentationComments — meaningful doc comments

Public/protected classes and methods need a non-trivial /// <summary>:

/// <summary>Calculates the customer balance in the company currency.</summary>
/// <param name="_includeOpen">Whether open transactions count.</param>
/// <returns>Balance in MST.</returns>
public AmountMST balanceMST(boolean _includeOpen = true) { … }

Auto-generated stubs ("This method does foo.") do not count. Restate the contract.

BPDuplicateMethod — no dupes on the inheritance chain

Adding a method that already exists on a base class in the same model fails BP. Run d365fo get class <Base> to confirm before adding.

Label-on-field exception

When adding a field whose EDT already carries a label, do NOT set --label on the field — it inherits from the EDT. Override only if you deliberately want a different caption in this table:

d365fo generate table FmVehicle \
    --field VIN:VinEdt:mandatory      \   # ← VinEdt has Label = "VIN" — leave it alone
    --field Make:Name                  \   # ← inherits "Name" from EDT
    --label "@Fleet:Vehicle"

Linting workflow

# In-process heuristics — fast, runs anywhere, useful for CI:
d365fo lint --output sarif > lint.sarif

# Run specific method-flag categories (detected at index-extract time):
d365fo lint --category today-usage          # BPUpgradeCodeToday: today() calls
d365fo lint --category do-insert-update     # doInsert/doUpdate/doDelete usage
d365fo lint --category doc-comment-missing  # BPXmlDocNoDocumentationComments

# Full BP — only on the Windows VM, only on user request:
d365fo bp check --output json

Six categories are now available: table-no-index, ext-named-not-attributed, string-without-edt, today-usage, do-insert-update, doc-comment-missing. The method-flag categories are populated at extract time by scanning <Source> text — no full body is stored. Re-run d365fo index refresh after editing source before linting.

Never auto-run bp check. It blocks the user (slow, Windows-only). Say "Changes scaffolded. Run d365fo bp check when you're ready."

Hard "never" list

  • Never call today().
  • Never hardcode a UI string in info() / warning() / error().
  • Never nest while select blocks.
  • Never ship a table without an alternate key.
  • Never reference a label without verifying it exists.
  • Never auto-run d365fo bp check.

CLI object-discovery best practices

When the user asks a functional question — "which classes print a free invoice", "which classes process sales orders" — translate it into 2–4 English keyword fragments and run a single targeted batch search. Always add --kind class (or the relevant kind) to avoid scanning all 13 object types.

# ✅ CORRECT — kind-filtered, fast
d365fo search batch FreeTextInvoice PrintFreeTxt FreeInv --kind class --output json

# ❌ WRONG — full scan across Tables/Classes/EDTs/Enums/Forms/… slow and noisy
d365fo search batch FreeTextInvoice PrintFreeTxt FreeInv --output json
  • Use at most 4 fragments per call. More fragments add tokens, not precision.
  • Use the English AOT name (FreeTextInvoice, not volná faktura).
  • If the result set has >50 hits, refine with a tighter fragment instead of adding more queries.
  • After identifying candidate names, resolve details with d365fo get class <Name> --output json.
Install via CLI
npx skills add https://github.com/dynamics365ninja/d365fo-cli --skill xpp-best-practice-rules
Repository Details
star Stars 12
call_split Forks 7
navigation Branch main
article Path SKILL.md
More from Creator
dynamics365ninja
dynamics365ninja Explore all skills →