はじめに
プリザンターのソースコードを読んでいると、Models ディレクトリに自動生成されたと思しき大量の .cs ファイルがあることに気づきます。これらのファイルは、CodeDefiner というツールが定義ファイルとテンプレートからC#コードを自動生成したものです。
本連載では、CodeDefinerのコード自動生成エンジンを深く掘り下げ、新規コントリビュータがソースコードを読み解くためのリファレンスとなることを目指します。
本連載で参照するソースコードはすべて Implem/Implem.Pleasanter リポジトリのものです。リンクはコミットハッシュベースのパーマリンクを使用しています。
| 回 | テーマ |
|---|---|
| 第1回(本記事) | 全体像:エントリポイント・コマンド体系・初期化フロー |
| 第2回 | 定義ファイルとテンプレートシステム |
| 第3回 | テンプレート展開エンジン |
| 第4回 | コードマージシステム |
| 第5回 | RepeatType別のコード生成ハンドラ |
| 第6回 | プレースホルダー置換と型変換 |
CodeDefinerとは
CodeDefinerは、プリザンター(Implem.Pleasanter)のソリューション内に含まれるコマンドラインツールです。主に以下の役割を持っています。
- データベースの構成(テーブル作成・マイグレーション)
- C#コードの自動生成(Model、Utility、Rds 等)
-
定義アクセサコードの生成(
Def.cs内の定義クラス) - ソリューションのバックアップ
本連載では、このうち 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、_def、def、mvc の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テンプレート |
コード自動生成で特に重要なのは Code と Column の定義です。
コード生成の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);
}));
}
全テーブル名(Depts、Groups、Users、Sites、Issues、Results、Wikis など)をループし、各テーブルに対してコード定義を適用します。
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 |
この置換は出力ファイルのパスにも適用されます。たとえば OutputPath が Models\#ModelName#\#ModelName#Model.cs であれば、Models\Issue\IssueModel.cs に展開されます。
まとめ
第1回では、CodeDefinerのコード自動生成エンジンの全体像を把握しました。
- CodeDefinerは コマンドライン引数 によって実行するフェーズを制御する
- 初期化時に 定義ファイル(JSON + Body.txt)が読み込まれ、
CodeDefinitionCollectionとColumnDefinitionCollectionに格納される - コード生成は 定義コード生成(
DefinitionAccessorCreator)と MVCコード生成(MvcCreator)の2系統がある - MVCコード生成は テーブルごとの繰り返し生成 と 共通コードの生成 に分かれる
-
DataContainerがコンテキスト情報を受け渡し、最上位のReplacePlaceholderがテーブル名・モデル名を置換する
次回は、定義ファイル(Definition_Code/ 内の JSON と Body.txt)がどのように構造化されているかを詳しく見ていきます。