• ホーム
  • ブログ
  • Scribanで実現するC#コード自動生成 - 基礎から実践的なコード生成システムまで

Scribanで実現するC#コード自動生成 - 基礎から実践的なコード生成システムまで

鶴田 篤広のプロフィール画像

鶴田 篤広

ソフトウェアエンジニア

作成日:

C#.NETScribanコード生成テンプレートエンジン
Scribanで実現するC#コード自動生成 - 基礎から実践的なコード生成システムまで - Scribanテンプレートエンジンの基礎から、JSONデータを使った実践的なコード自動生成システムの...

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 }}

830

パイプで渡された値は第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 }}

1016

匿名関数

名前のない関数を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");
    }
}

全体アーキテクチャ

ダイアログ1

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の強み

  1. シンプルな構文: Liquidライクで学習コストが低い
  2. 高速: パースとレンダリングが高速
  3. 安全: サンドボックス実行で安全
  4. 柔軟: HTML、コード、設定ファイルなど何でも生成可能

コード自動生成のメリット

  1. DRY原則: 定義データを一元管理し、コードの重複を排除
  2. 型安全性: 生成されたコードでコンパイル時にエラーを検出
  3. 保守性: 定義を更新してコマンド一発で再生成

ユースケース

  • 設定値へのアクセサクラス生成
  • APIクライアントのボイラープレート生成
  • データベースエンティティのDTO生成
  • 多言語リソースファイルの生成

手作業でのコード量産に疲れたら、ぜひScribanを使ったコード自動生成を検討してみてください。

参考リンク

こんなお悩みはありませんか?

1

システムの改修・刷新

古いシステムを使い続けているが、そろそろ限界を感じている

2

技術の相談相手がいない

社内にエンジニアがおらず、技術的な判断を相談できる人がいない

3

新規サービスを小さく始めたい

アイデアはあるが、まずは最小限の形で試してみたい

4

業務の効率化・自動化

手作業やExcel管理から脱却し、業務をシステム化したい

5

AIを活用したい

ChatGPTなどのAIを業務に取り入れたいが、どう始めればいいかわからない

このようなお悩みをお持ちの企業様に、
クレインテックが伴走支援いたします。

初回のご相談・お見積もりは無料です。

この記事をシェア

クレインテックに相談する

お客様と一緒に課題を整理し、小さく始めて育てる「共創型開発」を行っています。

「こんなシステムは作れる?」「費用感を知りたい」など、どんな段階でもお気軽にご相談ください。

初回のご相談は無料です。

お問い合わせ