dotnet new のカスタムテンプレート開発では、「choice パラメーターの選択肢に応じて switch ジェネレーターで短縮文字列を作り、テンプレート内の文字列を置換したい」のに、なぜか特定のシンボルだけ置換されない…という落とし穴があります。この記事では、choice→switch 連携で {cshparameter} が置換されない原因を2つに分けて整理し、最小の修正で確実に直す手順までまとめます。
起きている問題(症状の整理)
前提として、dotnet new <template> 用に .template.config/template.json を用意し、必須パラメーター ChoiceInput を datatype: "choice" として定義します。さらに ChoiceInput の値を条件にして、switch ジェネレーターで別シンボル ChoiceShortHand を生成し、テンプレート内の {cshparameter} を c1〜c9 のような短縮表記に置換したい、という状況です。
| シンボル | 種類 | 役割 | 結果 |
|---|---|---|---|
| ChoiceInput | parameter(choice) | ユーザーが Choice 1〜9 を選ぶ | 正常 |
| ChoiceDerived | derived | ChoiceInput から派生(例:空白やハイフン除去など) | 置換も正常 |
| ChoiceDerivedLower | generated(casing) | 派生シンボルを小文字化 | 置換も正常 |
| ChoiceShortHand | generated(switch) | ChoiceInput に応じて c1〜c9 を返す | 置換されない |
「他は置換できているのに、switch だけ効かない」というと switch の条件式ばかり疑いがちですが、実は 設定の置き場所 と datatype の扱い が原因になるケースが多いです。
まず押さえる:template.json の“置換”はどこで決まる?
dotnet new テンプレートは template.json の symbols セクションで「シンボル(変数)」を定義し、replaces を指定したシンボルについては、テンプレート内容の文字列をそのシンボル値で置換します。重要なのは、replaces は generator の parameters の中ではなく、シンボル定義の直下に置くという点です。
一方、switch のような generated symbol は、parameters の中にジェネレーター固有の設定(cases など)を書きます。switch は「条件を上から評価し、最初に true になった value を返す」仕様です。デフォルト分岐を入れたい場合は、最後に常に true になる条件を置く(または condition を空にする)という考え方になります。
よくある勘違い(見た目が似ている)
| 項目 | 正しい置き場所 | 間違えやすい置き場所 | 起きがちな症状 |
|---|---|---|---|
| replaces | シンボル定義の直下 | parameters の中 | 置換対象として認識されず、プレースホルダーが残る |
| cases | parameters の中 | シンボル定義の直下 | switch が成立せず、値が生成できない |
| datatype | (シンボル or generator により意味が違う) | とりあえず string にする | choice と比較すると条件が期待通り評価されないことがある |
原因①:datatype の型不一致(choice と string のすれ違い)
ChoiceInput は datatype: "choice" のパラメーターです。この choice を条件式で比較するとき、テンプレートエンジンは choice を「単なる文字列」としてではなく、choice として扱います(条件式では choice のリテラルが使える、という扱い)。
ここで、ChoiceShortHand 側の switch 設定(parameters)で datatype を "string" にしていると、条件式 (ChoiceInput == 'Choice 1') が期待通りに評価されず、すべての case が外れる(結果として値が生成されない)ことがあります。この点は Microsoft Q&A でも「型不一致の可能性があるので datatype を choice に合わせる」旨で案内されています。
修正ポイント(datatype を choice に揃える)
以下のように、ChoiceShortHand の switch ジェネレーターで指定している datatype を "choice" に変更します。
"ChoiceShortHand": {
"type": "generated",
"generator": "switch",
"parameters": {
"datatype": "choice",
"cases": [
{ "condition": "(ChoiceInput == 'Choice 1')", "value": "c1" },
{ "condition": "(ChoiceInput == 'Choice 2')", "value": "c2" },
{ "condition": "(ChoiceInput == 'Choice 3')", "value": "c3" },
{ "condition": "(ChoiceInput == 'Choice 4')", "value": "c4" },
{ "condition": "(ChoiceInput == 'Choice 5')", "value": "c5" },
{ "condition": "(ChoiceInput == 'Choice 6')", "value": "c6" },
{ "condition": "(ChoiceInput == 'Choice 7')", "value": "c7" },
{ "condition": "(ChoiceInput == 'Choice 8')", "value": "c8" },
{ "condition": "(ChoiceInput == 'Choice 9')", "value": "c9" },
{ "condition": "true", "value": "c1" }
]
},
"replaces": "{cshparameter}"
}
最後の { "condition": "true", ... } はデフォルト分岐の例です。choice の想定外入力が起きない設計でも、テンプレート改修中のテスト時に「case が1つも当たらず空になる」を防げます(switch は上から順に最初の true を採用します)。
原因②:replaces の記述場所の誤り(parameters に入れてしまう)
症状として「{cshparameter} がまったく置換されず、そのまま残る」場合、switch の条件以前に 置換設定が読み取られていないことが多いです。
典型例が、replaces を parameters の中に書いてしまうパターンです。replaces はシンボルのメタ情報として扱われるため、シンボル定義の直下(type と同じ階層)に置く必要があります。
NG例(replaces を parameters に入れている)
"ChoiceShortHand": {
"type": "generated",
"generator": "switch",
"parameters": {
"datatype": "string",
"replaces": "{cshparameter}",
"cases": [
{ "condition": "(ChoiceInput == 'Choice 1')", "value": "c1" }
]
}
}
OK例(replaces は直下へ)
"ChoiceShortHand": {
"type": "generated",
"generator": "switch",
"parameters": {
"datatype": "choice",
"cases": [
{ "condition": "(ChoiceInput == 'Choice 1')", "value": "c1" }
]
},
"replaces": "{cshparameter}"
}
Microsoft Q&A のやり取りでも、最終的に「datatype の見直し」と「replaces の置き場所」がポイントだったと確認されています。
最終的な完成形(ChoiceInput → 派生 → 小文字化 → 短縮コード置換)
質問の構成に合わせ、ChoiceDerived / ChoiceDerivedLower を残しつつ、ChoiceShortHand を正しく置換できる形に整えた例です。フォーム(forms)で空白とハイフンを除去する想定も含めています。
{
"$schema": "http://json.schemastore.org/template",
"author": "YourName",
"classifications": [ "dotnet", "template" ],
"name": "Choice Switch Sample",
"identity": "Company.Template.ChoiceSwitchSample",
"shortName": "choiceswitch",
"symbols": {
"ChoiceInput": {
"type": "parameter",
"isRequired": true,
"datatype": "choice",
"description": "選択肢を選んでください",
"choices": [
{ "choice": "Choice 1", "description": "Choice 1" },
{ "choice": "Choice 2", "description": "Choice 2" },
{ "choice": "Choice 3", "description": "Choice 3" },
{ "choice": "Choice 4", "description": "Choice 4" },
{ "choice": "Choice 5", "description": "Choice 5" },
{ "choice": "Choice 6", "description": "Choice 6" },
{ "choice": "Choice 7", "description": "Choice 7" },
{ "choice": "Choice 8", "description": "Choice 8" },
{ "choice": "Choice 9", "description": "Choice 9" }
]
},
"ChoiceDerived": {
"type": "derived",
"valueSource": "ChoiceInput",
"valueTransform": "Combine",
"replaces": "{ChoiceDerivedParameter}"
},
"ChoiceDerivedLower": {
"type": "generated",
"generator": "casing",
"parameters": {
"source": "ChoiceDerived",
"toLower": true
},
"replaces": "{choicederivedparameter}"
},
"ChoiceShortHand": {
"type": "generated",
"generator": "switch",
"parameters": {
"datatype": "choice",
"cases": [
{ "condition": "(ChoiceInput == 'Choice 1')", "value": "c1" },
{ "condition": "(ChoiceInput == 'Choice 2')", "value": "c2" },
{ "condition": "(ChoiceInput == 'Choice 3')", "value": "c3" },
{ "condition": "(ChoiceInput == 'Choice 4')", "value": "c4" },
{ "condition": "(ChoiceInput == 'Choice 5')", "value": "c5" },
{ "condition": "(ChoiceInput == 'Choice 6')", "value": "c6" },
{ "condition": "(ChoiceInput == 'Choice 7')", "value": "c7" },
{ "condition": "(ChoiceInput == 'Choice 8')", "value": "c8" },
{ "condition": "(ChoiceInput == 'Choice 9')", "value": "c9" },
{ "condition": "true", "value": "c1" }
]
},
"replaces": "{cshparameter}"
}
},
"forms": {
"Combine": {
"identifier": "replace",
"pattern": " |-",
"replacement": ""
}
}
}
ポイントは次の2つです。
- ChoiceShortHand の parameters.datatype を choice にする(choice を条件に使うなら揃える)
- ChoiceShortHand の replaces を parameters の外に出す(シンボル定義直下へ)
テンプレート側ファイルはこう書く(置換される側の例)
テンプレートのファイル(例:appsettings.json や README.md、あるいは任意のソースコード)には、置換対象として replaces で指定した文字列をそのまま書きます。
choice short hand: {cshparameter}
derived: {ChoiceDerivedParameter}
lower: {choicederivedparameter}
スペル・大文字小文字・括弧({})まで完全一致が前提です。ここがズレていると、設定が正しくても当然置換されません。
動作確認の手順(最短で「直った」を確認する)
1) ローカルでインストールして試す
# テンプレートフォルダへ移動
cd path/to/your-template
# インストール
dotnet new install .
2) ヘルプで option が見えているか確認
dotnet new choiceswitch --help
ここで ChoiceInput がテンプレートオプションとして表示され、選択肢が出ていれば第一関門クリアです(表示されない場合は template.json の配置や shortName、キャッシュを疑います)。
3) 実行して、置換結果を確認
dotnet new choiceswitch --ChoiceInput "Choice 2" -o out-test
out-test の生成物を開いて、{cshparameter} が c2 になっているかを確認します。
テンプレート開発あるある:キャッシュで「直ってないように見える」対策
テンプレートをローカルで何度もインストール・アンインストールしていると、変更が反映されづらく感じることがあります。テンプレートエンジンにはキャッシュ関連のデバッグ用オプションが用意されており、代表的なものに --debug:rebuildcache(テンプレートキャッシュ削除)や --debug:reinit(設定リセット)があります。
# キャッシュを削除してから実行(例)
dotnet new choiceswitch --ChoiceInput "Choice 2" -o out-test --debug:rebuildcache
また、ローカルフォルダテンプレートの開発体験でキャッシュが影響する事例も報告されています。変更を入れたのに挙動が変わらないときは、テンプレートIDの変更やインストール元フォルダの変更が効く場合もあります。
原因切り分けチェックリスト(これだけ見れば大体潰せる)
| チェック項目 | 確認方法 | NGだと起きること | 対処 |
|---|---|---|---|
| replaces はシンボル直下? | ChoiceShortHand の JSON で parameters の外にあるか | プレースホルダーがそのまま残る | replaces をシンボル定義直下へ移動 |
| switch の datatype は choice に合わせた? | parameters.datatype が choice になっているか | 条件が当たらず値が生成されない | datatype を choice にする(または評価方法を見直す) |
| 置換対象文字列は完全一致? | テンプレートファイル側の {cshparameter} の綴り確認 | 置換されない | テンプレート内の文字列を修正 |
| cases にデフォルトを用意した? | 最後に true 条件を置く / condition 空など | 想定外で空になり原因が追いづらい | デフォルト分岐を追加 |
| テンプレートのキャッシュを疑った? | --debug:rebuildcache で挙動が変わるか | 修正が反映されないように見える | rebuildcache / reinit / custom-hive を試す |
最終的な動作イメージ(期待値を固定する)
ここまで直すと、テンプレート実行時の結果は次のように安定します。
| ChoiceInput | ChoiceShortHand(期待値) | テンプレート内の {cshparameter} |
|---|---|---|
| Choice 1 | c1 | c1 に置換 |
| Choice 2 | c2 | c2 に置換 |
| … | … | … |
| Choice 9 | c9 | c9 に置換 |
choice パラメーターから派生させたシンボル(ChoiceDerived / ChoiceDerivedLower)が既に動いている場合でも、ChoiceShortHand は「datatype の整合」と「replaces の階層」が崩れると途端に置換されなくなります。逆に言えば、ここさえ揃えれば switch 生成でも普通に置換できます。
まとめ:直すべきは switch の式より“設定の型と階層”
- choice を条件にした switch では、datatype の不一致で case が当たらないことがある(
datatype: "choice"に揃える) - replaces は parameters の中ではなく、シンボル定義直下に置く(置換対象として登録される場所が決まっている)
- ローカル開発ではキャッシュが絡むことがあるので、挙動が変なら
--debug:rebuildcacheも試す

コメント