Quand utiliser le Chain of Command ?
Le CoC convient quand une exigence doit s'exécuter avant ou après une méthode existante de classe, de table, de formulaire ou d'entité de données, et qu'aucun événement métier explicite n'est disponible. Il maintient la personnalisation dans un modèle d'extension séparé et donne accès aux membres publics et protégés.
Avant d'ajouter un wrapper, je vérifie la configuration standard, les événements, les délégués, les gestionnaires d'événements et les stratégies SysExtension. L'objectif n'est pas de sélectionner le point d'extension le plus puissant, mais le plus stable et le moins couplé.
Microsoft documente les règles de wrapping dans Extension de classe – Wrapping de méthode et Chain of Command.
Comment le runtime compose la chaîne
Lorsque plusieurs extensions enveloppent la même méthode, le runtime D365 les empile selon l'ordre de dépendance des modèles : une extension dans un modèle qui dépend du modèle d'une autre extension se place à l'extérieur de la chaîne. Les extensions dans des modèles indépendants n'ont pas d'ordre garanti.
Implications pratiques :
- Deux extensions de modèles indépendants (sans dépendance mutuelle) n'ont pas d'ordre garanti. Chaque wrapper doit être autonome et ne pas dépendre des effets de bord d'une autre extension.
- Si l'ordre importe, rendez la dépendance explicite dans le descripteur de modèle et documentez la raison dans le work item.
- L'attribut
[ExtensionOf]doit référencer la classe, table ou formulaire exact via les fonctions intrinsèquesclassStr,tableStrouformStr. Une chaîne littérale ou une cible incorrecte crée silencieusement un wrapper sans effet.
Comprendre le contrat next
next transfère l'exécution au maillon suivant, qui peut être une autre extension ou l'implémentation de base. L'ordre entre extensions indépendantes n'est pas un contrat fonctionnel fiable. Chaque wrapper doit donc rester autonome.
[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;
}
}
}
Le wrapper appelle la logique standard une seule fois, préserve l'exception et délègue l'observabilité à un composant dédié.
Choisir la logique avant ou après next
Avant next
- Valider les préconditions et rejeter l'opération tôt.
- Normaliser ou enrichir les paramètres.
- Démarrer la télémétrie ou un contexte de trace.
- Capturer l'état
orig()avant l'écriture sur les extensions de table.
Après next
- Réagir au résultat standard.
- Publier un événement de domaine ou un événement métier.
- Enrichir ou traduire la valeur de retour.
- Enregistrer l'exécution réussie et les métriques.
Modifier les paramètres d'entrée ou la valeur de retour change le contrat de la méthode. Documentez cela dans le work item et couvrez-le par des tests de régression ciblés.
Choisir le bon point d'extension
Le CoC n'est pas toujours le mécanisme correct. Le tableau suivant compare les quatre principaux patterns d'extension disponibles en D365 F&O :
| Point d'extension | Mécanisme de déclenchement | Cas d'usage typique | Niveau de couplage |
|---|---|---|---|
| Chain of Command | Enveloppe une méthode ; next délègue au maillon suivant | Ajouter de la logique pré/post à une méthode existante quand aucun événement n'existe | Moyen — lié à une signature de méthode |
Gestionnaire d'événement ([SubscribesTo]) | La plateforme déclenche un événement pré/post ; le gestionnaire réagit | Réagir à un événement standard sans toucher au corps de la méthode | Faible — découplé du corps de méthode |
| Délégué | Le code standard déclenche explicitement un délégué ; l'abonné le traite | Points d'extension conçus par Microsoft exposant un hook intentionnel | Très faible — contrat stable |
| SysExtension / factory | Le runtime résout l'implémentation correcte par attribut | Stratégies plug-in : moteurs de taxe, gestionnaires de documents, stratégies d'entrepôt | Faible — remplace plutôt qu'enveloppe |
Un délégué ou un gestionnaire d'événement pré/post est toujours préférable au CoC lorsqu'il existe, car c'est un contrat stable conçu par Microsoft. Utilisez le journal des modifications d'extensibilité pour identifier de nouveaux hooks pouvant remplacer des wrappers CoC existants.
Zone à risque élevé : les méthodes de table
insert, update, delete et validateWrite peuvent être appelés depuis des formulaires, des traitements par lots, des entités de données et des intégrations. Les appels réseau ou les requêtes non indexées par enregistrement deviennent rapidement des incidents de performance.
[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);
}
}
}
L'extension capture l'état d'origine, laisse la plateforme effectuer la mise à jour et met en file d'attente un travail asynchrone au lieu d'appeler un endpoint externe dans la transaction interactive.
Performance et exécution en masse
- Évitez les sélections supplémentaires dans les méthodes par enregistrement. Mettez en cache les lookups dans une variable locale.
- N'appelez jamais de services HTTP depuis une transaction de table. Introduisez un pattern outbox (file d'attente de traitement par lot).
- Utilisez les files d'attente, les événements métier ou les traitements par lot pour les effets asynchrones.
- Mesurez avec un volume réaliste, pas seulement un scénario DEV unique. Utilisez le trace parser et le journal SQL.
- Un overhead de 20 ms paraît inoffensif dans un formulaire mais ajoute plus de 30 minutes sur 100 000 enregistrements.
Rendre l'extension testable
Gardez le wrapper centré sur l'adaptation et l'orchestration. Déplacez la règle dans un composant de domaine que SysTest peut exercer directement.
public final class QwoCreditPolicy
{
public static boolean canRelease(SalesTable _salesTable)
{
return _salesTable.CustAccount
&& _salesTable.SalesStatus == SalesStatus::Backorder
&& CustTable::find(_salesTable.CustAccount).CreditMax > 0;
}
}
Les tests unitaires couvrent la politique ; un test d'intégration confirme que la méthode standard atteint le wrapper. Cette séparation produit moins de tests fragiles et des revues de mise à jour plus simples.
Lorsque la dépendance sous test ne peut pas être remplacée par un paramètre de constructeur, utilisez une approche classe abstraite pour injecter un test double :
// Classe de service testable avec dépendance surchargeables
public class QwoInvoiceTelemetry
{
// Surcharger dans la sous-classe SysTest pour capturer les appels
protected void writeEvent(str _message, boolean _succeeded)
{
SysInformationLog::add(_message);
}
public static QwoInvoiceTelemetry start(RecId _invoiceId)
{
QwoInvoiceTelemetry t = new QwoInvoiceTelemetry();
return t;
}
}
Liste de contrôle Tech Lead
- S'agit-il du point d'extension le plus stable — ou existe-t-il un délégué, un événement ou un SysExtension ?
nextest-il appelé exactement une fois sur chaque chemin de code, y compris les chemins d'exception ?- Les valeurs de retour et les exceptions standard sont-elles préservées ?
- Le code est-il sûr dans les traitements par lot, les entités de données et les imports en volume ?
- Tous les accès SQL sont-ils indexés et bornés ?
- Les effets de bord externes sont-ils découplés de la transaction via un outbox ou une file d'attente ?
- Le nommage suit-il la convention
Xxx_Extensionet identifie-t-il le modèle ? - Le work item documente-t-il l'exigence, le risque, les scénarios impactés et les tests de régression ?