はじめに
前回は RepeatType 別のハンドラを解説しました。最終回となる今回は、テンプレート内の値プレースホルダーがどのように置換されるか、特に型変換(Converts.cs)と予約語エスケープ(ReservedWords.cs)のロジックを詳しく見ていきます。
本連載で参照するソースコードはすべて Implem/Implem.Pleasanter リポジトリのものです。リンクはコミットハッシュベースのパーマリンクを使用しています。
| 回 | テーマ |
|---|---|
| 第1回 | 全体像:エントリポイント・コマンド体系・初期化フロー |
| 第2回 | 定義ファイルとテンプレートシステム |
| 第3回 | テンプレート展開エンジン |
| 第4回 | コードマージシステム |
| 第5回 | RepeatType別のコード生成ハンドラ |
| 第6回(本記事) | プレースホルダー置換と型変換 |
Column.ReplaceCode:カラムの値置換
Column.ReplaceCode() は、カラム定義の各属性をテンプレート内のプレースホルダーに置換します。すべてのカラム関連プレースホルダーの処理がここに集約されています。
基本プレースホルダー
| プレースホルダー | 説明 | 値の例 |
|---|---|---|
#ColumnName# |
カラム名(先頭大文字) |
UserId, LoginId
|
#columnName# |
カラム名(先頭小文字) |
userId, loginId
|
#Type# |
C#型(TypeCs → TypeName の優先順) |
string, int, Title
|
#RecordingType# |
DB記録時の型 |
int, nvarchar
|
#RecordingData# |
記録時のデータ変換式 | .ToJson() |
#CastType# |
キャスト式 |
.ToInt(), .ToString()
|
#DefaultData# |
デフォルト値 |
0, string.Empty
|
#InitialValue# |
初期値 |
0, false, "[]"
|
#Hash# |
ハッシュ変換式(Hash列の場合) | .Sha512Cng() |
#MaxLength# |
最大長バリデーション | .MaxLength(100) |
#Calc# |
計算式 | テーブル名・モデル名を置換済みの式 |
#ColumnCount# |
対象カラムの総数 | 15 |
#GridEnable# |
一覧表示の有効/無効 |
1, 0
|
動的プレースホルダー
上記の固定プレースホルダー以外にも、カラム定義のプロパティ名がそのままプレースホルダーとして使えます。
private static string ReplaceCode(
string code, ColumnDefinition columnDefinition, string placeholder)
{
if (Def.ColumnXls.XlsSheet.Columns.Contains(placeholder))
{
code = code.Replace(
"#" + placeholder + "#", columnDefinition[placeholder].ToString());
}
return code;
}
定義ファイルのカラム名がそのままプレースホルダーとして展開されるため、新しいプロパティを追加するだけで自動的に使えるようになります。
型変換プレースホルダー
型変換プレースホルダーは、フォーム入力やAPI入力、DataRowからの読み込みなど、データの入口に応じた型変換コードを自動生成します。
| プレースホルダー | 用途 | ソースメソッド |
|---|---|---|
#ByForm#(value) |
フォーム入力からの変換 | Converts.ByForm() |
#ByApi#(data.Property) |
API入力からの変換 | Converts.ByApi() |
#ByDataRow#(dataRow) |
DataRowからの変換 | Converts.ByDataRow() |
#BySession#(session) |
セッションからの変換 | Converts.BySession() |
テンプレートでの使い方
// テンプレートの例
#ColumnName# = #ByForm#(value);
Users テーブルの LoginId(TypeName: nvarchar)の場合:
// 展開結果
LoginId = value.ToString();
Users テーブルの UserId(TypeName: int)の場合:
// 展開結果
UserId = value.ToInt();
Converts.cs:型変換ロジックの詳細
変換の優先順位
型変換は以下の優先順位で決定されます。
CastType():基本型のキャスト変換
internal static string CastType(this string type)
{
switch (type.CsType())
{
case Types.CsString: return ".ToString()";
case Types.CsInt: return ".ToInt()";
case Types.CsLong: return ".ToLong()";
case Types.CsDecimal: return ".ToDecimal()";
case Types.CsSingle: return ".ToSingle()";
case Types.CsDouble: return ".ToDouble()";
case Types.CsDateTime: return ".ToDateTime()";
case Types.CsBool: return ".ToBool()";
default: return string.Empty;
}
}
データベース型名からC#のキャスト式への変換マッピングです。
| DB型名 | C#型分類 | キャスト式 |
|---|---|---|
nvarchar, varchar, char 等 |
CsString |
.ToString() |
int |
CsInt |
.ToInt() |
bigint |
CsLong |
.ToLong() |
decimal, money 等 |
CsDecimal |
.ToDecimal() |
float |
CsSingle |
.ToSingle() |
real |
CsDouble |
.ToDouble() |
datetime |
CsDateTime |
.ToDateTime() |
bit |
CsBool |
.ToBool() |
TypeCs による特殊変換
TypeCs(C#独自型)が設定されているカラムは、単純なキャストではなくオブジェクト生成が行われます。
private static string TypeCs(
string code, string convertFrom,
ColumnDefinition columnDefinition,
string placeholder, string codeVariable)
{
switch (columnDefinition.TypeCs + convertFrom)
{
case "Title#ByForm#":
case "Body#ByForm#":
case "Status#ByForm#":
return code.Replace(placeholder,
CreateObjectByForm(columnDefinition));
case "Time#ByForm#":
case "CompletionTime#ByForm#":
return code.Replace(placeholder,
CreateObjectByForm(columnDefinition, ", byForm: true"));
case "Title#ByApi#":
case "Body#ByApi#":
case "Status#ByApi#":
return code.Replace(placeholder,
CreateObjectByApi(columnDefinition));
// ... DataRow 版も同様 ...
default:
return code.Replace(placeholder,
AsPrefix(columnDefinition, codeVariable));
}
}
TypeCs + 変換元の組み合わせで出力が異なります。
| TypeCs | 変換元 | 生成されるコード |
|---|---|---|
Title |
ByForm |
new Title(value.ToString()) |
Time |
ByForm |
new Time(context, value.ToDateTime(), byForm: true) |
Status |
ByApi |
new Status(data.Status.ToInt()) |
CompletionTime |
ByDataRow |
new CompletionTime(context, dataRow, column.ColumnName) |
| その他 | - | value as TypeCs ?? defaultValue |
ByForm の Body カラム特殊処理
Issues と Results テーブルの Body カラムには、バイナリパスの正規化という特殊処理が入ります。
internal static string ByForm(this string code, ColumnDefinition columnDefinition)
{
var placeholder = Placeholder(code, "#ByForm#");
if ((columnDefinition.TableName == "Issues" || columnDefinition.TableName == "Results")
&& columnDefinition.ColumnName == "Body")
{
return code.Replace(placeholder,
"Implem.Pleasanter.Models.BinaryUtilities" +
".NormalizeFormBinaryPath(context, value.ToString())");
}
// ... 通常の変換処理 ...
}
DefaultData / InitialValue の生成
カラムのデフォルト値と初期値の生成も型に応じて行われます。
DefaultData
private static string DefaultData(this ColumnDefinition columnDefinition, ...)
{
switch (/* 型の分類 */)
{
case Types.CsString:
switch (columnDefinition.RecordingData)
{
case ".ToJson()":
case ".RecordingJson()":
// JSON配列/オブジェクトの初期値
return columnDefinition.TypeCs switch
{
"List<long>" or "Attachments" => "\"[]\"",
_ => "\"{}\""
};
default:
return DefaultData(columnDefinition, "string.Empty", bracket: true);
}
case Types.CsNumeric: return DefaultData(columnDefinition, "0");
case Types.CsDateTime: return "0.ToDateTime()";
case Types.CsBool: return columnDefinition.Default == "1" ? "true" : "false";
case Types.CsBytes: return "null";
}
}
| 型分類 | デフォルト値 |
|---|---|
| 文字列 | string.Empty |
| 文字列(JSON記録) |
"[]" or "{}"
|
| 数値 | 0 |
| 日時 | 0.ToDateTime() |
| ブール |
false(Default="1" のとき true) |
| バイナリ | null |
カスタムデフォルト値
DefaultCs プロパティが設定されている場合はそちらが優先されます。
private static string DefaultData(
ColumnDefinition columnDefinition, string _default, bool bracket = false)
{
if (!columnDefinition.DefaultCs.IsNullOrEmpty())
return bracket ? "\"" + columnDefinition.DefaultCs + "\"" : columnDefinition.DefaultCs;
else if (!columnDefinition.Default.IsNullOrEmpty())
return bracket ? "\"" + columnDefinition.Default + "\"" : columnDefinition.Default;
else
return _default;
}
ReservedWords:C#予約語のエスケープ
コード自動生成では、定義ファイルのIDやカラム名がそのままC#の識別子として使われることがあります。C#の予約語と衝突する場合は、エスケープが必要です。
EscapeReservedWord
public static string EscapeReservedWord(
this string word, string additionalPrefix = "_", string additionalSuffix = "")
{
return CsReservedWordArray.Contains(word)
? additionalPrefix + word + additionalSuffix
: word;
}
予約語に一致する場合、デフォルトではアンダースコアのプレフィックスが追加されます。
例:定義カラム名が class の場合 → _class に変換
エスケープ対象の予約語一覧
private static readonly string[] CsReservedWordArray =
{
"abstract", "as", "async", "await", "base", "bool", "break", "byte",
"case", "catch", "char", "checked", "class", "const", "continue",
"decimal", "default", "delegate", "do", "double", "else", "enum",
"event", "explicit", "extern", "false", "finally", "fixed", "float",
"for", "foreach", "goto", "if", "implicit", "in", "int", "interface",
"internal", "is", "lock", "long", "namespace", "new", "null", "object",
"operator", "out", "override", "params", "private", "protected",
"public", "readonly", "ref", "return", "sbyte", "sealed", "short",
"sizeof", "stackalloc", "static", "string", "struct", "switch", "this",
"throw", "true", "try", "typeof", "uint", "ulong", "unchecked",
"unsafe", "ushort", "using", "virtual", "volatile", "void", "while",
"add", "dynamic", "get", "partial", "remove", "set", "value",
"var", "where", "yield"
};
C#のキーワードに加えて、コンテキストキーワード(add、dynamic、get、set、value、var、where、yield など)もエスケープ対象に含まれています。
ValidName:特殊文字の変換
public static string ValidName(string valiableName)
{
return valiableName
.Replace(" ", "_space_")
.Replace(">", "_")
.Replace(".", "_dot_")
.Replace("#", "_sharp_")
.Replace(",", "_comma_")
.Replace(":", "_colon_")
.Replace("[", "_")
.Replace("]", "_")
.Replace("(", "_")
.Replace(")", "_")
.Replace("-", "_")
.Replace("+", "_plus_")
.Replace("=", "_equal_")
.Replace("^", "_caret_")
.Replace("\"", "_yen_")
.Replace("*", "_asterisk_")
.Replace("@", "_atmark_");
}
定義IDに含まれる可能性のある特殊文字を、C#識別子として有効な文字列に変換します。DefinitionRow ハンドラで使われ、定義行のIDをC#のプロパティ名に変換する際に適用されます。
| 元の文字 | 変換後 | 用途の例 |
|---|---|---|
| 空白 | _space_ |
表示文字列ID |
. |
_dot_ |
バージョン番号を含むID |
# |
_sharp_ |
C#関連の定義名 |
- |
_ |
ハイフン区切りの名前 |
@ |
_atmark_ |
メールアドレス関連 |
プレースホルダー置換の全体像
連載を通じて解説してきたプレースホルダー置換の全体像をまとめます。
置換の実行順序
-
構造的プレースホルダー(
<!--ID-->)が再帰的に展開される - 各ハンドラの
ReplaceCode()でカラム名・型名などの値置換が行われる -
Creators.ReplaceCode()でID列の型情報が置換される - 最後に
MvcCreator.ReplacePlaceholder()でテーブル名・モデル名が置換される
全プレースホルダー一覧
連載を通じて登場したプレースホルダーをカテゴリ別にまとめます。
テーブル/モデル関連
| プレースホルダー | 置換元 | 説明 |
|---|---|---|
#ServiceName# |
Environments.ServiceName |
サービス名 |
#TableName# |
dataContainer.TableName |
テーブル名 |
#tableName# |
先頭小文字化 | テーブル名(小文字始まり) |
#tablename# |
全小文字化 | テーブル名(全小文字) |
#ModelName# |
dataContainer.ModelName |
モデル名 |
#modelName# |
先頭小文字化 | モデル名(小文字始まり) |
#modelname# |
全小文字化 | モデル名(全小文字) |
カラム関連
| プレースホルダー | 置換元 | 説明 |
|---|---|---|
#ColumnName# |
columnDefinition.ColumnName |
カラム名 |
#columnName# |
先頭小文字化 | カラム名(小文字始まり) |
#Type# |
TypeCs or TypeName
|
C#型 |
#RecordingType# |
TypeName |
DB記録型 |
#RecordingData# |
columnDefinition.RecordingData |
記録時変換式 |
#CastType# |
TypeName.CastType() |
キャスト式 |
#DefaultData# |
型に基づく生成 | デフォルト値 |
#InitialValue# |
型に基づく生成 | 初期値 |
#Hash# |
Hash フラグ | ハッシュ式 |
#MaxLength# |
MaxLength |
最大長バリデーション |
#Calc# |
columnDefinition.Calc |
計算式 |
型変換関連
| プレースホルダー | 説明 |
|---|---|
#ByForm#(expr) |
フォーム入力からの変換 |
#ByApi#(expr) |
API入力からの変換 |
#ByDataRow#(expr) |
DataRow読込からの変換 |
#BySession#(expr) |
セッション読込からの変換 |
#IdType# |
ID列のC#型 |
#IdTypeDefault# |
ID列のデフォルト値 |
#CastIdType# |
ID列のキャスト式 |
JOIN 関連
| プレースホルダー | 説明 |
|---|---|
#JoinTableName# |
結合先テーブル名 |
#JoinType# |
結合タイプ |
#JoinExpression# |
結合条件式 |
#TableNameAlias# |
テーブルエイリアス |
#ColumnBracket# |
カラムブラケット |
#ColumnBrackets# |
計算列含むブラケット |
定義メタ関連
| プレースホルダー | 説明 |
|---|---|
#File# / #file#
|
定義ファイル名 |
#ColumnNames# |
カラム名リスト |
#Id# |
定義行のID |
#DefColumnName# |
定義カラム名(エスケープ済み) |
#DefColumnNameOriginal# |
定義カラム名(原本) |
#SetDefault# |
デフォルト値初期化式 |
表示関連
| プレースホルダー | 説明 |
|---|---|
#DisplayId# |
表示ID |
#DisplayContent# |
表示テキスト |
#DisplayCssClass# |
CSSクラス名 |
#DisplayContentEncoded# |
HTMLエンコード済みテキスト |
#FormName# |
フォーム名 |
連載のまとめ
全6回にわたって、CodeDefinerのコード自動生成エンジンを体系的に解説してきました。
アーキテクチャの全体像
各回で学んだこと
| 回 | テーマ | キーポイント |
|---|---|---|
| 第1回 | 全体像 | コマンド体系、初期化フロー、3つの生成フェーズ |
| 第2回 | 定義ファイル | JSON + Body.txt のペア構成、フィルタプロパティ体系 |
| 第3回 | 展開エンジン | 2種類のプレースホルダー、再帰展開、インラインオプション |
| 第4回 | コードマージ |
/// Fixed: マーカー、Parser による構造解析、差分書き込み |
| 第5回 | ハンドラ | 10種類の RepeatType、フィルタリングロジック |
| 第6回 | 型変換 | CastType、TypeCs 特殊変換、予約語エスケープ |
コントリビュータへの実践的アドバイス
-
新しいカラムを追加する場合:
Definition_Column/にJSONファイルを追加し、CodeDefinerのdefコマンドを実行する -
手動修正を保護する場合:
/// Fixed:コメントをメソッドの直前に追加する -
特定のテーブルのみにコードを生成する場合:
Include/Excludeプロパティを定義JSONに追加する -
コード生成のデバッグ:
/tオプションで特定の定義IDだけを再生成して動作確認する - 定義ファイルの新規プロパティ追加:カラム定義のプロパティ名がそのまま動的プレースホルダーとして使えることを活用する
CodeDefinerのコード自動生成は、一見複雑に見えますが、基本的な仕組みはテンプレート + プレースホルダー置換 + 再帰展開というシンプルなパターンの組み合わせです。このリファレンスが、新規コントリビュータの皆さんのソースコード理解の一助になれば幸いです。