次の方法で共有


ベスト プラクティス ルールの記述

コミュニティの関心グループが Yammer から Microsoft Viva Engage に移行されました。 Viva Engage コミュニティに参加し、最新のディスカッションに参加するには、「 Finance and Operations Viva Engage Community へのアクセスを要求する 」フォームに入力し、参加するコミュニティを選択します。

この記事では、メタデータと X++ コードの両方について、C# でベスト プラクティス ルールを作成する方法について説明します。 コンパイラはベスト プラクティス チェックを実行します。 それらは出荷コードで許容されない不都合なプラクティスをキャッチするために日単位ビルドで実行することができます。 これらの機能を使用して、アプリケーションに関する情報を収集する単純な 1 回限りのツールを作成することもできます。

ベスト プラクティス ルールを使用して、アプリケーションに関する情報を収集する簡単なツールを作成します。 フレームワークは、XLNT (X++ LaNguage ツールキットの省略形) と呼ばれるマネージド フレームワークの上に構築されています。 フレームワークを使用して、X++ コードから情報を抽出し、変更するカスタム ツールを構築します。 2 種類のベスト プラクティス ルールがあります: メタデータを処理するルールとソース コードを処理するルール。

コードのベスト プラクティス フレームワーク

コード ベスト プラクティス フレームワーク (CBPF) を使用すると、X++ ソース コードを分析するための独自のツールを作成できます。 これらの規則は、X++ ソース コードの問題と見なされる問題を診断します。 このセクションでは、ベスト プラクティス機能の基礎について説明します。 この情報は、独自のルールの作成について詳しく説明する後のセクションの理解に役立ちます。 また、このドキュメントで示されているルールよりも複雑なルールをコーディングする開発者にも役立ちます。 CBPF APIにより、インフラストラクチャの問題を処理しなくても表現するルールに焦点を絞ることができます。 トークンを読み取り、それらを組み合わせて分かり易いものを作成する必要はありません。 代わりに、CBPF は次のパーツを提供します。

  • X++ ソース コードから抽象構文ツリー (AST) を構築するパーサー。
  • X++ コードに対して一連のパスを実行するパイプライン。
  • 作成済みのパスの数。 最初のパスは、ソース コードの解析です。
  • メタデータを読み取るインフラストラクチャ。

規則は AST に基づいているため、ルールの記述を開始する前に、その概念を理解することが重要です。

パーサーと AST

パーサーは X++ コードを読み取り、誤った構文エラーが含まれていない場合はそこから AST を生成します。 パーサーは、組み込みのエラー回復スキームを持つため、大部分の構文エラーから適切に回復できます。 構文エラーが発生した場合、パーサーはセミコロンが検出されるまでシンボルを読み取り、セミコロンが正しいシンボルである状態に達するまで状態のスタックを解除して AST の構築を試みます。 さらに、パーサーは、構文エラーが発生したときに正しいシンボルのセットを提案できます。 パーサーは、API のコンシューマーが直接対話することを意図したものではありませんが、ユーザーの介入なしに動作するブラック ボックスと見なす必要があります。 パーサーはソース コードの言語構成を認識すると、AST を構築します。 AST は、それらが表す X++ コンポーネントの抽象化であるノードで構成されます。 次の図は、いくつかの AST ノードを示しています。

public abstract class Statement : Ast
{
    /// <summary>
    /// Gets or sets the comments in the statement
    /// </summary>
    public string Comments { get; set; }

    public abstract string ToString(int indent);
}

Statement クラスは抽象です。これは、"ステートメント" をインスタンス化しても意味がないためです。 if および while ステートメントなどのコンクリート派生クラスのみのインスタンスを作成することができます。 Comments プロパティはこの基底クラスに配置されるため、すべての派生クラスに適用されます。 つまり、すべてのステートメントはステートメントの前にコメントを持つことができます。 コメントは、特定のプロパティを使用してアクセスできます。 X++ にはさまざまな種類のステートメントがあり、それぞれが前に示した抽象 Statement クラスの 1 つ以上のステップで派生したクラスによって記述されます。 次の例は、while ステートメントの定義を示しています。

public class WhileStatement : Statement
{
    /// <summary>
    /// Gets or sets the while condition.
    /// </summary>
    public Expression Condition { get; set; }

    /// <summary>
    /// Gets or sets the constituent while statement.
    /// </summary>
    public Statement Statement { get; set; }
}

while ステートメントは条件 (式) と、条件が true と評価される限り実行される構成ステートメントで構成されています。 パーサーは、表される成果物が開始および終了するソース コードの位置 (つまり、そのエクステント) を維持します。 AST が走査されるため、個々のノードに情報を追加すると便利な場合があります。 たとえば、すべての式にはタイプがあります。 タイプの互換性の問題を診断するためにツリーがスキャンされるので、個々のノードにその情報を配置できるようになると便利です。 各要件の AST ノードを変更する必要はなく、各ノードに名前と値のペアを提供するために使用できるプロパティ コレクションがあります。 各 AST ノードには、ノードの忠実度の高い文字列表現を返す ToString メソッドがあり、デバッグ シナリオで役立ちます。

AstSweeper クラス

AstSweeper クラスは、指定した AST インスタンスにビジター パターンを適用します。 ビジター パターンは、基になるデータ構造 (AST) と、ノードで実行する操作 (コードを推論するためのロジック) を分離します。 AstSweeper クラスには、各 AST ノード型の仮想メソッドがあり、AST の構造に従ってこれらのメソッドを呼び出します。 次の例では、スウィーパーの動作を示しています。 わかりやすくするために、一部の詳細は省略されています。

/// <summary>The AST sweeper visits each node in the AST</summary>
/// <typeparm name="TReturn">The type returned by each of the sweeper methods</typeparm>
/// <typeparm name="TPayload">
/// The type of the payload passed to each of the sweeper methods
/// </typeparm>
public class AstSweeper<TReturn, TPayload> where TReturn : class
{
    protected virtual TReturn VisitStatement(TPayload o, Statement statement)
    {
        var compoundStatement = statement as CompoundStatement;
        if (compoundStatement != null)
        {
            return this.VisitCompoundStatement(o, compoundStatement);
        }

        var whileStatement = statement as WhileStatement;
        if (whileStatement != null)
        {
            return this.VisitWhileStatement(o, whileStatement);
        }
    }

    protected virtual TReturn VisitWhileStatement(TPayload o, WhileStatement statement)
    {
        this.VisitExpression(statement.Condition);
        this.VisitStatement(statement.Statement);

        return null;
    }
}

特定の AST ノードを処理する仮想メソッドの名前は、 Visitで付加された AST クラスの名前です。 パラメーターは、アクセスするノードと、呼び出されるすべての訪問者に渡すことができるペイロードです。 このようにして、スイーパーは、深度優先トラバーサルで渡された AST 内の各ノードに対して仮想メソッドを 1 回呼び出します。 ペイロード パラメーターを使用して、必要に応じて各ノードに情報 (シンボル テーブルなど) を渡します。 開発者は、AstSweeper クラスから派生したクラスをビルドし、特に関心のあるメソッドをオーバーライドします。

X++ のコーディング ガイドラインに従って、アンダースコア文字で始まるパラメーター名の割合を決定する必要があるとします。 パラメーターの数とアンダースコアで始まるパラメーターの数を計算するロジックを使用して 、AstSweeper クラスから派生するクラスを記述します。 次の例は、このコードを示しています。

public class ParameterCounter : AstSweeper<object, object>
{
    /// <summary>
    /// The parameter count maintained as the methods are encountered.
    /// </summary>
    public int ParameterCount { get; set; }

    /// <summary>
    /// The number of parameters that start with an underscore character.
    /// </summary>
    public int UnderscoredParameters { get; set; }

    /// <summary>
    /// Visits the method parameters.
    /// </summary>
    /// <param name="o">The payload. Not used in this scenario.</param>
    /// <param name="parameters">The list of parameters to visit.</param>
    /// <returns>The method parameters.</returns>
    protected override object VisitMethodParameters(object o,
        IEnumerable<ParameterDeclaration> parameters)
    {
        this.ParameterCount += parameters.Count();
        this.UnderscoredParameters += parameters
            .Where(p => p.Name.StartsWith("_")).Count();

        return null;
    }
}

この場合、集計はクラス スコープで定義されている ParametersCount および UnderscoredParameters プロパティで維持されます。 同様に有効なもう 1 つの方法は、この情報をすべての Visit メソッドに渡すペイロードに渡すことです。 ほとんどの場合、オーバーライドされたメソッドから super() を無条件に呼び出して、 Visit メソッドが、アクセスされるノードの下にあるすべてのノードに対して呼び出されるようにする必要があります。 前の例の場合、違いは生じないため、AST ツリー トラバーサルを排除してパフォーマンスを向上することを選択します。

ベスト プラクティス ルールのコードを記述

ビジネス ルールを作成するには :

  1. AST のプロパティに関して診断したい状況を定義します。 分析を実行できる Visit* メソッドを記述します。
  2. エラー状態が見つかると、診断メッセージを生成します。 この目的で API を使用できます。 診断メッセージごとに定型コードを記述する必要があります。
  3. 新しいベスト プラクティス ルールをフレームワークの残りの部分にフックして、ルールを含めるかどうかをユーザーが決定し、その指示がある場合は実行できるようにします。

Visual Studio でのベスト プラクティス ルール プロジェクトの作成

このチュートリアルでは、次のシナリオを検討します。

  • 一部のメソッドには、コードを記述した個人の名前を提供する Author 属性が含まれます。 この属性は、そのメソッドを含むスタック トレースが表示される場合に便利です。
  • 開発者の離職率が大きいため、一覧表示されている開発者の名前を静的にすることはできません。 Author 属性で使用されている名前を検索し、現在の開発者の名前の一覧と一致させる必要があります。

作成者属性クラスは、次のように定義されます。

class AuthorAttribute extends SysAttribute
{
    private str theAuthor;

    public void new(str _author) 
    {
        this.theAuthor = _author;
    }
}

運用コードで、ドキュメントのコメントとアサーションを追加して、パラメーター値に関する主な前提条件を検証します。 わかりやすくするために、このチュートリアルではこれらの手順を省略しています。 目標を理解したら、Visual Studio を起動し、ベスト プラクティス ルール プロジェクトを作成します。 ルールの目的を適切に伝えるわかりやすい名前を指定します。 Visual Studio では、いくつかのソース コード スニペットとプロジェクト参照が設定されたプロジェクトが作成されます。 このソース コードを自分のコードの出発点として使用することにより、時間を大幅に節約することができます。 事前に設定された例には、任意のメソッド名で "Microsoft" という単語を禁止する規則 (おそらく著作権上の理由から) と、名前内の特定の文字を禁止するメタデータベースの規則が含まれています。 メタデータチェックには関係ないため、プロジェクトから InvalidCharactersDiagnosticItem.cs ファイルと DemoMetadataCheck.cs ファイルを削除します。 また、Microsoft の名前チェックに関心がないため、VisitMethod クラスのDemoAST メソッドの内容を削除します。 最初に行う必要があるのは、特定のメソッドに 1 つ以上の Author 属性があるかどうかを調べることです。 Method型 (VisitMethod メソッドにパラメーターとして渡される) には、Attributes型のAttributeList プロパティがあります。 これを使用して、このメソッドで Author 属性が定義されているかどうかを確認します。

protected override object VisitMethod(BestPracticeCheckerPayload payload, Method method)
{
    var names = new List<string>();
    foreach (var attribute in method.Attributes.Attributes)
    {
        if (string.Compare(attribute.Name, "Author",
            ignoreCase: true, culture: CultureInfo.InvariantCulture) == 0)
        {
            var authorNameLiteral = attribute.Parameters.First().Literal as StringAttributeLiteral;
            // The name contains quotes (either single or double). Get rid of those
            var authorName = authorNameLiteral.Value.Trim('\'', '"');
            names.Add(authorName);
        }
    }

    // More to come...
    return null;
}

この時点で、属性をループ処理し、作成者の名前の一覧を収集します。これは、Author 属性の最初のパラメーターとして指定された名前です。 次に、リストを静的リストに保持する許容可能な作成者のリストと比較する必要があります。 作成者がリストに含まれていない場合は常に、適切な診断メッセージを発行する必要があります。 現時点では、次のようなものがあります。

public class AuthorListRule : BestPracticeAstChecker<BestPracticeCheckerPayload>
{
    private static HashSet<string> authorlist = new HashSet<string>()
    {
        "Alan Kay", "John von Neuman", "C.A.R. Hoare"
    };

    public AuthorListRule() : base()
    {
    }

    protected override object VisitMethod(BestPracticeCheckerPayload payload, Method method)
    {
        var names = new List<string>();
        foreach (var attribute in method.Attributes.Attributes)
        {
            if (string.Compare(attribute.Name, "Author",
                ignoreCase: true, culture: CultureInfo.InvariantCulture) == 0)
            {
                var authorNameLiteral = attribute.Parameters.First().Literal as StringAttributeLiteral;
                // The name contains quotes (either single or double). Get rid of those
                var authorName = authorNameLiteral.Value.Trim('\'', '"');
                names.Add(authorName);
            }
        }

        foreach (var name in names)
        {
            if (!authorlist.Contains(name))
            {
                // TODO: Add a diagnostic message
            }
        }
        return null;
    }
}

つまり、ルールの違反についてユーザーに知らせる診断メッセージを作成する必要があります。 前述のように、ビジターの基本実装を呼び出し、メソッドに含まれるすべてのノードに対してビジター メソッドを呼び出す必要があります。 ただし、この場合、作成者属性がリストに含まれているかどうかを判断した後は、これ以上処理を行いたくありません。

診断メッセージのクラスの追加

プロジェクトには既にエラー メッセージの定型コードが含まれているため、そのコードを開始点として使用して、ルールに違反した場合にルールから返される診断メッセージを作成します。 各メッセージを独自のクラスとして実装します。 各エラー メッセージは、任意の量のコンテキスト情報をエンコードできます。 この場合、コンテキスト情報は、リストに見つからない作成者の名前です。 まず、メッセージリソースファイルにメッセージを追加します。プロジェクトでそのファイルを開き、それに文字列を追加します。 名前 (エラー モニカーとも呼ばれます) AuthorNotCurrent を使用します。 {0}文字列は、コンテキスト情報のプレースホルダーです。この場合、リストに含まれていない作成者の名前です。 エラー メッセージに表示される実際のテキストに加えて、ルールの説明を含む文字列もあります。 Visual Studio はベスト プラクティス ダイアログにこの情報を表示し、システムで有効にするルールをユーザーが決定できるように設計されています。 診断メッセージのクラスを作成し、AuthorNotCurrentDiagnosticItem.cs という名前を付けます。 次のコードをNotAllowedWordDiagnosticItemクラスに触発されて追加します。

namespace CompareAuthorsToList
{
    using System;
    using System.Collections.Generic;
    using System.Runtime.Serialization;
    using System.Xml.Linq;
    using Microsoft.Dynamics.AX.Metadata.XppCompiler;

    [DataContract]
    public class AuthorNotCurrentDiagnosticItem : CustomDiagnosticItem
    {
        private const string AuthorNotCurrentKey = "Author";
        public const string DiagnosticMoniker = "AuthorNotCurrent";

        public AuthorNotCurrentDiagnosticItem(string path, string elementType, TextPosition textPosition, string author)
            : base(path, elementType, textPosition, DiagnosticType.BestPractices, Severity.Warning, DiagnosticMoniker, Messages.AuthorNotCurrent, author)
        {
            this.AuthorNotCurrent = author;
        }

        public AuthorNotCurrentDiagnosticItem(Stack<Ast> context, TextPosition textPosition, string author)
            : base(context, textPosition, DiagnosticType.BestPractices, Severity.Warning, DiagnosticMoniker, Messages.AuthorNotCurrent, author)
        {
            this.AuthorNotCurrent = author;
        }

        // Serialization support
        public AuthorNotCurrentDiagnosticItem(XElement element)
            : base(element)
        {
        }

        [DataMember]
        public string AuthorNotCurrent { get; private set; }

        /// <summary>
        /// Hydrate the diagnostic item from the given XML element.
        /// </summary>
        /// <param name="itemSpecificNode">The XML element containing the diagnostic.</param>
        protected override void ReadItemSpecificFields(System.Xml.Linq.XElement itemSpecificNode)
        {
            this.AuthorNotCurrent = base.ReadCustomField(itemSpecificNode, AuthorNotCurrentKey);
        }

        /// <summary>
        /// Write the state into the given XML element.
        /// </summary>
        /// <param name="itemSpecificNode">The element into which the state is persisted.</param>
        protected override void WriteItemSpecificFields(System.Xml.Linq.XElement itemSpecificNode)
        {
            this.WriteCustomField(itemSpecificNode, AuthorNotCurrentKey, this.AuthorNotCurrent);
        }
    }
}

診断メッセージは利用可能です。 必要な手続きは1つしかありません。ルールは、発行される可能性のある診断を宣言的に公表する必要があります。 ルールに戻り、新しい診断メッセージを反映するように BestPracticeRule 属性を変更します。

[BestPracticeRule(
    AuthorNotCurrentDiagnosticItem.DiagnosticMoniker,
    typeof(Messages),
    AuthorNotCurrentDiagnosticItem.DiagnosticMoniker + "Description",
    BestPracticeCheckerTargets.Class)]
public class AuthorListRule : BestPracticeAstChecker<BestPracticeCheckerPayload>
{ // ... }

ご覧のように、BestPracticeRule 属性に 4 つのパラメータが指定されています。

  1. ルールの識別子
  2. ルール記述を保持するリソース ファイルのタイプ。 この例では、Messages という名前の既定のリソース ファイルを使用します。Messages というクラスが作成されています。 このクラスの型を 2 番目の引数として使用します。
  3. ルールの説明を含む文字列リソースの名前。 この名前は、先ほどリソース ファイルに追加した AuthorNotCurrentDescription という名前の文字列です。ルールを記述するための人間の判読可能な文字列が含まれています。 この文字列を使用して、Visual Studio 内のベスト プラクティスのダイアログでユーザーにルールを記述します。 Visual Studio で、Dynamics 365 > ベスト プラクティス コンフィギュレーション を選択してダイアログを表示します。
  4. チェックするアーティファクトの説明。 この場合、値は、ルールをクラスにのみ適用する必要があることを指定します。 Modify the description as needed by using one of the other literals in the BestPracticeCheckerTargets リスト内の他のリテラルのいずれかを使用して、必要に応じて説明を変更します。

診断メッセージを記述するクラスのインスタンスを作成し、診断のセットに追加します。

foreach (var name in names)
{
    if (!authorlist.Contains(name))
    {
        // Create the custom error message, including
        // the contextual name information...
        var warning = new AuthorNotCurrentDiagnosticItem(
            this.Context, method.Position, name);

        // and add it to the set of reported messages.
        this.ExtensionContext.AddErrorMessage(warning);
    }
}

この時点で、組織内で価値を提供できる完全なベストプラクティスルールが完成しています。 それを構築しエラーを修正します。

メタデータ ベースのベスト プラクティス ルール

これまでは、コードを扱うルールを記述する方法について説明しました。 このセクションでは、コードではなくメタデータに適用されるルールを作成する方法について説明します。 メタデータ ルールを取り扱うクラスは、BestPracticeMetadataChecker から派生します。 派生インスタンスは、チェックする必要があるコンポーネントを記述するメタデータのインスタンスを受け取ります。 次に、Microsoft.Dynamics.AX.Metadata.Metamodel で API を使用し、メタデータ グラフに対して LINQ クエリを使用します。 ベスト プラクティス チェック用のテンプレートには、メタデータ チェックを実行するクラスと、前のセクションで説明したコード ベースのクラスが含まれています。 診断メッセージの発行に関連するメカニズムは、前に説明したのと同じです。

ルールのインストール、実行、テスト

コードが正常にコンパイルされると、DLL が作成されます。 ツールで新しい規則を取得できるようにするには、実行する前にこの DLL をインストールする必要があります。 DLL は、次の 2 つの方法でインストールできます。

  • ベスト プラクティス構成ダイアログのボタンを使用します。 [ 拡張機能のインストール] を選択します。 ルールを含むアセンブリ ファイル (ルールのビルド時に生成される DLL) をポイントするように求められます。 [OK] を選択すると、必要な DLL がコピーされます (次のセクションを参照)。
  • C:\Packages\bin\BPExtensions フォルダーに DLL を手動でインストールします。

ルールをデバッグする場合は、 .pdb ファイルをアセンブリと同じディレクトリにコピーすると便利です。 DLL をターゲット ディレクトリに展開した後、Visual Studio を再起動します。 その後は、ルールは使用可能です。 残った問題を解決するために、ルールをデバッグする必要があるかもしれません。 実際、ルールを順に実行して AST を調べることは、始めたばかりのときには価値があります。 ルールをデバッグするには、ベスト プラクティス ルールが xppAgent プロセスによって実行されることを知っている必要があります。 したがって、VS 自体のコンテキスト内では実行されません。 [財務と運用] ページの [Visual Studio のオプション] ダイアログで、[ベスト プラクティス チェックの実行] を選択していることを確認します。 それ以外の場合、チェックは実行されません。 VisitMethod メソッドにブレークポイントを設定し、フリート管理モデルの前に示したように、新しいルールがオンになっているモデルを構築します。 VS インスタンスを xppcAgent プロセスに添付します。 ビルドすると、ブレークポイントにヒットし、コードを詳しく調査し始めることができます。 すべてのプロパティ、宣言およびステートメントの一覧を表示し、それらに関するすべての詳細を見つけることができます。

XppBp.exe でルールを実行

前に説明したように、ベスト プラクティス ルールは、多くの場合、Visual Studio からのプロジェクトのビルドの一部として実行されますが、それらを実行するための専用のコマンド ライン ツールもあります。 このツールは xppbp.exe ツールであり、主に夜間のビルド シナリオを対象としています。 コマンド ラインから起動することにより、有効なコマンド ラインのスイッチと引数の概要が得られます。 次にいくつか役に立つ例を挙げます。

  • モジュール内のすべてのフォームで BP を実行します: xppbp -module:FleetManagement form:*
  • 特定の要素で BP を実行します: xppbp -module:FleetManagement class:MyClass form:MyForm
  • モデル内のすべての項目で BP を実行します (モジュール内のこの 1 つのモデルに対してのみ): xppbp -module:FleetManagement -model:FleetManagement –all
  • モジュール内のすべてのモデルのすべての品目で BP を実行します: xppbp -module:FleetManagement –all
  • ログ ファイルに対する出力を記述します: xppbp -module:FleetManagement -all -xmllog=Log.xml -log=Log.txt