TL;DR
- Scribanは.NET向けの軽量・高速なテンプレートエンジン。HTMLからコードまで何でも生成可能
- 基礎: テンプレート構文、変数、ループ、条件分岐、パイプ、組み込み関数
- テンプレート内関数: シンプル関数、パラメトリック、可変長引数、匿名関数など豊富な関数定義機能
- C#連携: カスタム関数の登録でC#の機能をテンプレートから利用可能
- 応用: JSONデータから型安全なC#コードを自動生成するシステムを構築
はじめに
「似たようなコードを何度も書くのが面倒...」
「設定ファイルの定義から、対応するクラスを手作業で書き起こしている...」
こうした繰り返し作業を自動化する手段の1つがコード自動生成です。本記事では、.NET向けのテンプレートエンジン「Scriban」の基礎から、実践的なコード生成システムの構築まで解説します。
本記事の前提
- C# と .NET の基本的な知識
- オブジェクト指向プログラミングの理解
- NuGetパッケージの利用経験
動作環境: .NET 8.0、Scriban 5.x
Scribanの基礎
Scribanとは
Scribanは、.NET向けの軽量で高速なテンプレートエンジンです。
dotnet add package ScribanなぜScribanを選ぶのか
| 特徴 | 説明 |
|---|---|
| 軽量・高速 | 依存関係が少なく、パース・レンダリングが高速 |
| サンドボックス | ファイルシステムやネットワークへのアクセスを制限可能 |
| Liquid互換 | Shopify発のLiquidテンプレート構文をサポート |
| .NET Native | C#オブジェクトを直接テンプレートに渡せる |
| 用途が広い | HTML、Markdown、ソースコード、設定ファイルなど何でも生成可能 |
T4テンプレートより軽量で、Razorよりシンプル。コード生成のような用途には最適な選択肢です。
基本的な使い方
Hello World
using Scriban;
var template = Template.Parse("Hello {{name}}!");
var result = template.Render(new { name = "World" });
// => "Hello World!"{{ と }} で囲まれた部分が変数として展開されます。
オブジェクトのプロパティにアクセス
var template = Template.Parse("{{user.name}} ({{user.age}}歳)");
var result = template.Render(new {
user = new { name = "田中", age = 30 }
});
// => "田中 (30歳)"ドット記法でネストしたプロパティにアクセスできます。
テンプレート構文
ループ処理 (for)
{{~ for item in items ~}}
- {{ item }}
{{~ end ~}}var template = Template.Parse(@"
{{~ for item in items ~}}
- {{ item }}
{{~ end ~}}");
var result = template.Render(new { items = new[] { "Apple", "Banana", "Cherry" } });出力:
- Apple
- Banana
- Cherry{{~ と ~}} の ~ は空白・改行を除去するための記号です。これがないと余分な空行が入ります。
条件分岐 (if)
{{~ if user.is_admin ~}}
管理者メニューを表示
{{~ else ~}}
一般ユーザーメニューを表示
{{~ end ~}}パイプ演算子
パイプ | を使って、値を関数に渡すことができます。
{{ "hello world" | string.capitalize }}→ Hello world
{{ "hello world" | string.upcase }}→ HELLO WORLD
{{ items | array.size }}→ 配列の要素数
組み込み関数
Scribanには多くの組み込み関数があります。
# 文字列操作
{{ "test" | string.capitalize }} # Test
{{ "TeSt" | string.downcase }} # test
{{ "hello" | string.append " world" }} # hello world
# 配列操作
{{ [3, 1, 2] | array.sort }} # [1, 2, 3]
{{ [1, 2, 3] | array.first }} # 1
{{ ["a", "b"] | array.join ", " }} # a, b
# 数値操作
{{ 3.14159 | math.round 2 }} # 3.14
{{ 10 | math.abs }} # 10テンプレート内での関数定義
Scribanではテンプレート内で関数を定義できます。繰り返し使うロジックをまとめるのに便利です。
シンプル関数
funcキーワードで関数を定義します。引数は$0、$1...でアクセスします。
{{~ func greet ~}}
Hello, {{ $0 }}!
{{~ end ~}}
{{ greet "World" }}→ Hello, World!
パラメトリック関数
引数に名前を付けて定義できます。retで値を返します。
{{~ func add(x, y) ~}}
{{~ ret x + y ~}}
{{~ end ~}}
{{ add 3 5 }}
{{ 10 | add 20 }}→ 8 と 30
パイプで渡された値は第1引数になります。
オプション引数と可変長引数
デフォルト値を持つオプション引数や、可変長引数も使えます。
# オプション引数(デフォルト値付き)
{{~ func greet_with_title(name, title = "様") ~}}
{{~ ret name + title ~}}
{{~ end ~}}
{{ greet_with_title "田中" }}
{{ greet_with_title "田中" "さん" }}→ 田中様 と 田中さん
# 可変長引数
{{~ func sum(first, rest...) ~}}
{{~ result = first ~}}
{{~ for val in rest ~}}
{{~ result = result + val ~}}
{{~ end ~}}
{{~ ret result ~}}
{{~ end ~}}
{{ sum 1 2 3 4 5 }}→ 15
インライン関数
シンプルな式は1行で定義できます。
{{ double(x) = x * 2 }}
{{ square(x) = x * x }}
{{ double 5 }}
{{ square 4 }}→ 10 と 16
匿名関数
名前のない関数をdo...endで定義し、変数に代入したり直接使ったりできます。
{{ multiply = do; ret $0 * $1; end }}
{{ 3 | multiply 4 }}→ 12
関数ポインタ(エイリアス)
@演算子で関数への参照を取得し、他の関数に渡せます。
{{ [" hello ", " world "] | array.each @string.strip }}→ ["hello", "world"]
{{ numbers = [3, 1, 4, 1, 5] }}
{{ numbers | array.filter do; ret $0 > 2; end }}→ [3, 4, 5]
C#からカスタム関数を登録
組み込み関数だけでは足りない場合、C#で独自の関数を定義できます。
using Scriban;
using Scriban.Runtime;
var template = Template.Parse("{{ name | to_pascal_case }}");
var context = new TemplateContext();
var scriptObject = new ScriptObject();
// カスタム関数を登録
scriptObject.Import("to_pascal_case", new Func<string, string>(input =>
{
if (string.IsNullOrEmpty(input)) return input;
return char.ToUpper(input[0]) + input.Substring(1).ToLower();
}));
context.PushGlobal(scriptObject);
// データを追加
scriptObject["name"] = "hello_world";
var result = template.Render(context);
// => "Hello_world"ScriptObjectクラスを継承する方法
より整理された方法として、ScriptObjectを継承したクラスを作成できます。
public class MyCustomFunctions : ScriptObject
{
public static string ToPascalCase(string input)
{
if (string.IsNullOrEmpty(input)) return input;
return string.Concat(input.Split('_')
.Select(s => char.ToUpper(s[0]) + s.Substring(1).ToLower()));
}
public static string ToSnakeCase(string input)
{
if (string.IsNullOrEmpty(input)) return input;
return string.Concat(input.Select((c, i) =>
i > 0 && char.IsUpper(c) ? "_" + char.ToLower(c) : char.ToLower(c).ToString()));
}
}使用例:
var context = new TemplateContext();
context.PushGlobal(new MyCustomFunctions());
var template = Template.Parse("{{ 'hello_world' | to_pascal_case }}");
var result = template.Render(context);
// => "HelloWorld"JSONからC#コード生成の実践
基礎を踏まえ、実践的なコード生成システムを構築します。
ゴール
JSONで定義されたコンポーネント情報から、型安全なC#コードを自動生成します。
入力 (JSON):
{
"components": [
{
"category": "Settings",
"items": [
{ "name": "UserName", "type": "string", "label": "ユーザー名" },
{ "name": "Age", "type": "int", "label": "年齢" }
]
}
]
}出力 (C#):
public static class ComponentKeys
{
public static class Settings
{
/// <summary>ユーザー名</summary>
public static readonly TypedKey<string> UserName = new("Settings.UserName");
/// <summary>年齢</summary>
public static readonly TypedKey<int> Age = new("Settings.Age");
}
}全体アーキテクチャ

Step 1: データモデルの定義
まず、JSONの構造に対応するC#クラスを定義します。
using System.Text.Json.Serialization;
namespace CodeGen;
public class ComponentDefinition
{
[JsonPropertyName("components")]
public List<CategoryDefinition> Components { get; set; } = new();
}
public class CategoryDefinition
{
[JsonPropertyName("category")]
public string Category { get; set; } = "";
[JsonPropertyName("items")]
public List<ItemDefinition> Items { get; set; } = new();
}
public class ItemDefinition
{
[JsonPropertyName("name")]
public string Name { get; set; } = "";
[JsonPropertyName("type")]
public string Type { get; set; } = "";
[JsonPropertyName("label")]
public string Label { get; set; } = "";
}Step 2: JSONパーサー
using System.Text.Json;
namespace CodeGen;
public static class DefinitionParser
{
public static ComponentDefinition Parse(string jsonPath)
{
var json = File.ReadAllText(jsonPath);
var options = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
};
return JsonSerializer.Deserialize<ComponentDefinition>(json, options)
?? throw new InvalidOperationException("Failed to parse JSON");
}
}Step 3: テンプレートの作成
Scribanテンプレートを作成します。
// Template: keys.sbn
// Auto-generated code. Do not edit manually.
namespace Generated;
public static class ComponentKeys
{
{{~ for category in components ~}}
public static class {{ category.category }}
{
{{~ for item in category.items ~}}
/// <summary>{{ item.label }}</summary>
public static readonly TypedKey<{{ item.type }}> {{ item.name }} =
new("{{ category.category }}.{{ item.name }}");
{{~ end ~}}
}
{{~ end ~}}
}Step 4: テンプレートレンダラー
.NETオブジェクトをScribanに渡すためのレンダラーを作成します。
using Scriban;
using Scriban.Runtime;
using System.Reflection;
namespace CodeGen;
public static class TemplateRenderer
{
public static string Render(string templateText, object model)
{
var template = Template.Parse(templateText);
if (template.HasErrors)
{
throw new InvalidOperationException(
$"Template parse error: {string.Join(", ", template.Messages)}");
}
var context = new TemplateContext();
// カスタム関数を登録
var functions = new ScriptObject();
functions.Import("to_pascal_case", new Func<string, string>(ToPascalCase));
context.PushGlobal(functions);
// モデルをScriptObjectに変換して登録
var scriptObject = new ScriptObject();
ImportObject(model, scriptObject);
context.PushGlobal(scriptObject);
return template.Render(context);
}
private static void ImportObject(object source, ScriptObject target)
{
if (source == null) return;
var properties = source.GetType()
.GetProperties(BindingFlags.Public | BindingFlags.Instance);
foreach (var prop in properties)
{
var value = prop.GetValue(source);
if (value == null) continue;
// プロパティ名をsnake_caseに変換
var name = ToSnakeCase(prop.Name);
// コレクションの処理(stringはIEnumerableだが除外)
if (value is System.Collections.IEnumerable enumerable && value is not string)
{
var list = new List<object>();
foreach (var item in enumerable)
{
if (item == null || IsSimpleType(item.GetType()))
{
list.Add(item);
}
else
{
var itemScript = new ScriptObject();
ImportObject(item, itemScript);
list.Add(itemScript);
}
}
target[name] = list;
}
else if (IsSimpleType(value.GetType()))
{
target[name] = value;
}
else
{
var nested = new ScriptObject();
ImportObject(value, nested);
target[name] = nested;
}
}
}
private static bool IsSimpleType(Type type) =>
type.IsPrimitive || type == typeof(string) ||
type == typeof(decimal) || type == typeof(DateTime);
private static string ToSnakeCase(string input) =>
string.Concat(input.Select((c, i) =>
i > 0 && char.IsUpper(c) ? "_" + char.ToLower(c) : char.ToLower(c).ToString()));
private static string ToPascalCase(string input) =>
string.IsNullOrEmpty(input) ? input :
char.ToUpper(input[0]) + input.Substring(1);
}Step 5: コードジェネレーターの組み立て
using System.Reflection;
namespace CodeGen;
public static class CodeGenerator
{
public static void Generate(string jsonPath, string outputPath)
{
// 1. JSONをパース
var definition = DefinitionParser.Parse(jsonPath);
// 2. テンプレートを読み込み
var templateText = LoadEmbeddedTemplate("keys.sbn");
// 3. コードを生成
var generatedCode = TemplateRenderer.Render(templateText, definition);
// 4. ファイルに書き出し
Directory.CreateDirectory(Path.GetDirectoryName(outputPath)!);
File.WriteAllText(outputPath, generatedCode);
Console.WriteLine($"Generated: {outputPath}");
}
// 埋め込みリソースからテンプレートを読み込み
// リソース名は「{アセンブリ名}.{フォルダパス}.{ファイル名}」形式
private static string LoadEmbeddedTemplate(string templateName)
{
var assembly = Assembly.GetExecutingAssembly();
var resourceName = $"CodeGen.templates.{templateName}";
using var stream = assembly.GetManifestResourceStream(resourceName)
?? throw new FileNotFoundException($"Template not found: {resourceName}");
using var reader = new StreamReader(stream);
return reader.ReadToEnd();
}
}Step 6: 実行
// Program.cs
CodeGenerator.Generate("definitions.json", "Generated/ComponentKeys.cs");入力JSON (definitions.json):
{
"components": [
{
"category": "Settings",
"items": [
{ "name": "UserName", "type": "string", "label": "ユーザー名" },
{ "name": "Age", "type": "int", "label": "年齢" },
{ "name": "Theme", "type": "string", "label": "テーマ" }
]
},
{
"category": "Profile",
"items": [
{ "name": "Email", "type": "string", "label": "メールアドレス" },
{ "name": "IsVerified", "type": "bool", "label": "認証済み" }
]
}
]
}生成されるコード:
// Auto-generated code. Do not edit manually.
namespace Generated;
public static class ComponentKeys
{
public static class Settings
{
/// <summary>ユーザー名</summary>
public static readonly TypedKey<string> UserName =
new("Settings.UserName");
/// <summary>年齢</summary>
public static readonly TypedKey<int> Age =
new("Settings.Age");
/// <summary>テーマ</summary>
public static readonly TypedKey<string> Theme =
new("Settings.Theme");
}
public static class Profile
{
/// <summary>メールアドレス</summary>
public static readonly TypedKey<string> Email =
new("Profile.Email");
/// <summary>認証済み</summary>
public static readonly TypedKey<bool> IsVerified =
new("Profile.IsVerified");
}
}まとめ
Scribanを使ったコード自動生成について、基礎から実践的なシステム構築まで解説しました。
Scribanの強み
- シンプルな構文: Liquidライクで学習コストが低い
- 高速: パースとレンダリングが高速
- 安全: サンドボックス実行で安全
- 柔軟: HTML、コード、設定ファイルなど何でも生成可能
コード自動生成のメリット
- DRY原則: 定義データを一元管理し、コードの重複を排除
- 型安全性: 生成されたコードでコンパイル時にエラーを検出
- 保守性: 定義を更新してコマンド一発で再生成
ユースケース
- 設定値へのアクセサクラス生成
- APIクライアントのボイラープレート生成
- データベースエンティティのDTO生成
- 多言語リソースファイルの生成
手作業でのコード量産に疲れたら、ぜひScribanを使ったコード自動生成を検討してみてください。
