0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

プリザンターのソースコードを読んでいると、Models ディレクトリに自動生成されたと思しき大量の .cs ファイルがあることに気づきます。これらのファイルは、CodeDefiner というツールが定義ファイルとテンプレートからC#コードを自動生成したものです。

本連載では、CodeDefinerのコード自動生成エンジンを深く掘り下げ、新規コントリビュータがソースコードを読み解くためのリファレンスとなることを目指します。

本連載で参照するソースコードはすべて Implem/Implem.Pleasanter リポジトリのものです。リンクはコミットハッシュベースのパーマリンクを使用しています。

テーマ
第1回(本記事) 全体像:エントリポイント・コマンド体系・初期化フロー
第2回 定義ファイルとテンプレートシステム
第3回 テンプレート展開エンジン
第4回 コードマージシステム
第5回 RepeatType別のコード生成ハンドラ
第6回 プレースホルダー置換と型変換

CodeDefinerとは

CodeDefinerは、プリザンター(Implem.Pleasanter)のソリューション内に含まれるコマンドラインツールです。主に以下の役割を持っています。

  1. データベースの構成(テーブル作成・マイグレーション)
  2. C#コードの自動生成(Model、Utility、Rds 等)
  3. 定義アクセサコードの生成Def.cs 内の定義クラス)
  4. ソリューションのバックアップ

本連載では、このうち 2. C#コードの自動生成3. 定義アクセサコードの生成 に焦点を当てます。

ソリューション内でのCodeDefinerの位置づけ

CodeDefinerは Implem.DefinitionAccessor を通じて定義ファイルを読み込み、その定義に基づいて Implem.Pleasanter のソースコードを生成・更新します。

エントリポイント:Starter.cs

CodeDefinerの起動は Starter.Main() メソッドから始まります。

コマンドラインの解析

Main() メソッドでは、最初の引数をアクション名として取得し、/ で始まるオプション引数をハッシュテーブルに格納します。

ValidateArgs(args);
var argHash = ArgsType(args);
var action = args[0];

コマンド体系

CodeDefinerが受け付けるコマンドと、コード自動生成との関係を以下にまとめます。

コマンド DB構成 定義コード生成 MVCコード生成 説明
_rds - - DB構成のみ
rds DB構成+全コード生成
_def - - 定義コードのみ
def - 定義コード+MVCコード
mvc - - MVCコードのみ
splits_rds - - - Rds.cs のファイル分割
backup - - - ソリューションのバックアップ
migrate - - DBマイグレーション

コード自動生成に関連するのは rds_defdefmvc の4つです。

コマンドオプション

オプション 引数 説明
/p パス サービスパス(Implem.Pleasanter のディレクトリ)
/t ターゲット 特定の定義IDのみを生成
/l 言語 言語設定
/z タイムゾーン タイムゾーン設定
/f - 強制実行
/y - ユーザ入力なしで実行

/t オプションは、開発時に特定のコード定義だけを再生成したい場合に便利です。

初期化フロー

コード生成の前に、まず定義ファイルの読み込みが行われます。

Initializer.Initialize(
    path,
    assemblyVersion: Assembly.GetExecutingAssembly().GetName().Version.ToString(),
    setLanguage: argHash.Get("l"),
    setTimeZone: argHash.Get("z"),
    codeDefiner: true,
    setSaPassword: argHash.ContainsKey("s"),
    setRandomPassword: argHash.ContainsKey("r"));

Initializer.Initialize の処理

Initializer.Initialize() はプリザンター本体からも呼ばれる共通の初期化メソッドです。codeDefiner: true が渡されることで、CodeDefiner固有の初期化が有効になります。

初期化処理の中で重要なのは SetDefinitions() の呼び出しです。

public static void SetDefinitions()
{
    Displays.DisplayHash = DisplayHash();
    Def.SetCodeDefinition();      // ← コード定義の読み込み
    Def.SetColumnDefinition();    // ← カラム定義の読み込み
    Def.SetTemplateDefinition();
    Def.SetViewModeDefinition();
    Def.SetDemoDefinition();
    Def.SetSqlDefinition();
    SetDisplayAccessor();
    SetColumnDefinitionAccessControl();
}

ここで読み込まれる定義は以下の6種類です。

定義名 ディレクトリ 用途
Code Definition_Code/ C#コードのテンプレートと生成ルール
Column Definition_Column/ テーブルカラムの定義
Template Definition_Template/ サイトテンプレート
ViewMode Definition_ViewMode/ ビューモード定義
Demo Definition_Demo/ デモデータ定義
Sql Definition_Sql/ SQLテンプレート

コード自動生成で特に重要なのは CodeColumn の定義です。

コード生成の3つのフェーズ

コマンドに応じて、コード生成は以下の3つのフェーズで実行されます。

フェーズ1:DB構成(ConfigureDatabase)

テーブル定義に基づいてデータベーススキーマを作成・更新します。本連載のスコープ外ですが、別記事で解説しています。

フェーズ2:定義コード生成(DefinitionAccessorCreator)

DefinitionAccessorCreator.Create() は、定義ファイルにアクセスするためのC#コードを生成します。

internal static void Create()
{
    CreateCode();
}

private static void CreateCode()
{
    Def.CodeDefinitionCollection
        .OrderBy(o => o.Id)
        .Where(o => o.Source == "Def")    // Source が "Def" のものだけ
        .ForEach(codeDefinition =>
    {
        var code = Creators.Create(codeDefinition, new DataContainer("DefinitionFile"));
        if (!code.IsNullOrEmpty())
        {
            Merger.Merge(codeDefinition.OutputPath, code, codeDefinition.MergeToExisting);
        }
    });
}

Source"Def" の定義だけを抽出し、テンプレートを展開してからマージ処理を行います。

フェーズ3:MVCコード生成(MvcCreator)

MvcCreator.Create() は、Model・Utility・Rds などのC#コードを生成します。

internal static void Create(string target)
{
    CreateEachTable(target);    // テーブルごとに繰り返し生成
    CreateNotRepeat(target);    // 繰り返しなしの生成
}

MVCコード生成は大きく2つに分かれます。

CreateEachTable:テーブルごとの生成

private static void CreateEachTable(string target)
{
    Def.TableNameCollection().ForEach(tableName =>
        Def.CodeDefinitionCollection
            .Where(o => target.IsNullOrEmpty() || o.Id == target)
            .Where(o => o.Source == "Mvc")        // Source が "Mvc" のもの
            .Where(o => o.RepeatType == "Table")  // RepeatType が "Table"
            .Where(o => !Table.CheckExclude(o, tableName))
            .ForEach(codeDefinition =>
            {
                var dataContainer = new DataContainer("Table");
                var modelName = Def.ModelNameByTableName(tableName);
                dataContainer.TableName = tableName;
                dataContainer.ModelName = modelName;
                var code = ReplacePlaceholder(
                    Creators.Create(codeDefinition, dataContainer),
                    tableName, modelName);
                var fileName = ReplacePlaceholder(
                    Directories.Outputs(codeDefinition.OutputPath),
                    tableName, modelName);
                Merger.Merge(fileName, code, codeDefinition.MergeToExisting);
            }));
}

全テーブル名(DeptsGroupsUsersSitesIssuesResultsWikis など)をループし、各テーブルに対してコード定義を適用します。

CreateNotRepeat:繰り返しなしの生成

RepeatType が空の定義は、テーブルに依存しない共通コードです。テーブルのループなしで1回だけ生成されます。

コード生成の全体フロー図

ここまでの内容を全体フロー図にまとめます。

DataContainer:コンテキスト情報の受け渡し

コード生成中にテーブル名やカラム名といったコンテキスト情報を各部品に受け渡すために、DataContainer クラスが使われます。

internal class DataContainer
{
    internal Dictionary<string, XlsIo> XlsIoCollection;
    internal string Type = string.Empty;
    internal string DefinitionName = string.Empty;
    internal string ModelName = string.Empty;
    internal string TableName = string.Empty;
    internal string FormName = string.Empty;
    internal string ColumnName = string.Empty;

    internal DataContainer(string type)
    {
        XlsIoCollection = DefinitionFiles.Collection();
        Type = type;
    }
}
プロパティ 説明
XlsIoCollection 定義ファイル(Excel/JSON)のコレクション
Type コンテナの種類("Table""DefinitionFile"
DefinitionName 処理中の定義ファイル名("Code""Column" など)
ModelName モデル名("User""Issue" など)
TableName テーブル名("Users""Issues" など)
FormName フォーム名
ColumnName カラム名

DataContainer は、コード生成エンジンのさまざまな部品を通じて引き回され、テンプレート内のプレースホルダーを実際の値に置き換えるための情報源として機能します。

ReplacePlaceholder:最上位のプレースホルダー置換

MvcCreator.ReplacePlaceholder() は、生成されたコードとファイル名に対して最上位のプレースホルダー置換を行います。

private static string ReplacePlaceholder(
    string self, string tableName, string modelName)
{
    return self
        .Replace("#ServiceName#", Environments.ServiceName)
        .Replace("#TableName#", tableName)
        .Replace("#tableName#", tableName.ToLowerFirstChar())
        .Replace("#ModelName#", modelName)
        .Replace("#modelName#", modelName.ToLowerFirstChar());
}
プレースホルダー 置換内容
#ServiceName# サービス名 Implem.Pleasanter
#TableName# テーブル名(先頭大文字) Issues
#tableName# テーブル名(先頭小文字) issues
#ModelName# モデル名(先頭大文字) Issue
#modelName# モデル名(先頭小文字) issue

この置換は出力ファイルのパスにも適用されます。たとえば OutputPathModels\#ModelName#\#ModelName#Model.cs であれば、Models\Issue\IssueModel.cs に展開されます。

まとめ

第1回では、CodeDefinerのコード自動生成エンジンの全体像を把握しました。

  • CodeDefinerは コマンドライン引数 によって実行するフェーズを制御する
  • 初期化時に 定義ファイル(JSON + Body.txt)が読み込まれ、CodeDefinitionCollectionColumnDefinitionCollection に格納される
  • コード生成は 定義コード生成DefinitionAccessorCreator)と MVCコード生成MvcCreator)の2系統がある
  • MVCコード生成は テーブルごとの繰り返し生成共通コードの生成 に分かれる
  • DataContainer がコンテキスト情報を受け渡し、最上位の ReplacePlaceholder がテーブル名・モデル名を置換する

次回は、定義ファイル(Definition_Code/ 内の JSON と Body.txt)がどのように構造化されているかを詳しく見ていきます。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?