C#ソースジェネレータの使い方と実践ガイド

C#のソースジェネレータは、コードの自動生成とメンテナンスを効率化する強力なツールです。本記事では、ソースジェネレータの基本的な使い方から実際のプロジェクトでの応用方法までを詳しく解説します。これにより、開発プロセスの効率を飛躍的に向上させることができます。

目次

ソースジェネレータとは

ソースジェネレータは、C#のコンパイル時にコードを動的に生成するツールです。これにより、繰り返しの多いコードやテンプレートコードの自動生成が可能になり、コードベースの一貫性とメンテナンス性が向上します。ソースジェネレータを使用することで、開発者は手動でコードを書く手間を省き、バグの発生を減少させることができます。特に、大規模なプロジェクトや複雑なドメインモデルを扱う場合にその真価を発揮します。

ソースジェネレータのセットアップ

C#プロジェクトでソースジェネレータを使用するには、まずプロジェクトに適切なパッケージを追加する必要があります。以下に、ソースジェネレータのセットアップ手順を示します。

NuGetパッケージのインストール

ソースジェネレータを利用するために、まず必要なNuGetパッケージをインストールします。Visual Studioのパッケージマネージャーコンソールで以下のコマンドを実行します。

Install-Package Microsoft.CodeAnalysis.CSharp
Install-Package Microsoft.CodeAnalysis.Analyzers

ソースジェネレータプロジェクトの作成

ソースジェネレータは独立したプロジェクトとして作成します。新しいC#クラスライブラリプロジェクトを作成し、その中にソースジェネレータのロジックを実装します。

プロジェクトファイルの設定

ソースジェネレータプロジェクトの.csprojファイルに、以下の設定を追加します。これにより、プロジェクトがソースジェネレータとして認識されます。

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
    <OutputItemType>Analyzer</OutputItemType>
    <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
  </PropertyGroup>

</Project>

ターゲットプロジェクトへの追加

生成したソースジェネレータプロジェクトをターゲットプロジェクトに追加します。ターゲットプロジェクトの.csprojファイルに、生成したソースジェネレータのパッケージを参照する設定を追加します。

<Project Sdk="Microsoft.NET.Sdk">

  <ItemGroup>
    <PackageReference Include="YourSourceGeneratorPackage" Version="1.0.0" PrivateAssets="all" />
  </ItemGroup>

</Project>

これで、C#プロジェクトでソースジェネレータを利用する準備が整いました。次に、具体的なソースジェネレータの実装に進みます。

ソースジェネレータの基本構造

ソースジェネレータは、特定のインターフェースを実装するクラスとして定義されます。これにより、コンパイル時にコードを動的に生成することができます。以下に、ソースジェネレータの基本構造と主要な要素について説明します。

ISourceGeneratorインターフェースの実装

ソースジェネレータは、Microsoft.CodeAnalysis名前空間に含まれるISourceGeneratorインターフェースを実装することで作成されます。このインターフェースは、以下の2つのメソッドを持ちます。

  • Initialize(GeneratorInitializationContext context): ジェネレータの初期化処理を行います。
  • Execute(GeneratorExecutionContext context): 実際にソースコードを生成するロジックを実装します。

サンプルコード

以下に、シンプルなソースジェネレータの基本構造を示します。

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
using System.Text;

[Generator]
public class HelloWorldGenerator : ISourceGenerator
{
    public void Initialize(GeneratorInitializationContext context)
    {
        // 初期化処理(オプション)
    }

    public void Execute(GeneratorExecutionContext context)
    {
        // 生成するコードの定義
        var source = @"
using System;

namespace GeneratedCode
{
    public class HelloWorld
    {
        public static void SayHello()
        {
            Console.WriteLine(""Hello, World!"");
        }
    }
}";

        // ソースコードを追加
        context.AddSource("HelloWorldGenerator", SourceText.From(source, Encoding.UTF8));
    }
}

初期化メソッド

Initializeメソッドは、コンパイルが始まる前に一度だけ呼び出されます。ここでは、追加の解析を設定したり、初期設定を行うことができます。

実行メソッド

Executeメソッドは、コンパイルプロセスの中で呼び出され、ソースコードの生成を実行します。生成するコードは、文字列として定義され、context.AddSourceメソッドを使用してコンパイルに追加されます。

この基本構造を理解することで、次のステップでは具体的なソースジェネレータの実装方法を学びます。

シンプルなソースジェネレータの作成

ここでは、基本的なソースジェネレータの作成方法を具体的に示します。この例では、指定された属性を持つクラスに対してプロパティのバッキングフィールドを自動生成するソースジェネレータを作成します。

属性の定義

まず、対象となるクラスに付与する属性を定義します。

using System;

[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
sealed class AutoNotifyAttribute : Attribute
{
    public AutoNotifyAttribute() { }
}

ソースジェネレータの実装

次に、AutoNotifyAttributeが付与されたクラスに対して、プロパティのバッキングフィールドを自動生成するソースジェネレータを実装します。

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using System.Collections.Generic;
using System.Linq;
using System.Text;

[Generator]
public class AutoNotifyGenerator : ISourceGenerator
{
    public void Initialize(GeneratorInitializationContext context)
    {
        // 初期化処理
    }

    public void Execute(GeneratorExecutionContext context)
    {
        // シンタックスレシーバを作成
        var syntaxReceiver = (SyntaxReceiver)context.SyntaxReceiver;

        // 対象のクラスを取得
        var classes = syntaxReceiver.CandidateClasses;

        foreach (var @class in classes)
        {
            // バッキングフィールド生成ロジック
            var source = GenerateBackingFields(@class);
            context.AddSource($"{@class.Identifier.Text}_generated.cs", SourceText.From(source, Encoding.UTF8));
        }
    }

    private string GenerateBackingFields(ClassDeclarationSyntax classDeclaration)
    {
        var className = classDeclaration.Identifier.Text;
        var properties = classDeclaration.Members.OfType<PropertyDeclarationSyntax>();

        var sb = new StringBuilder();
        sb.AppendLine($"namespace {@classDeclaration.Parent}");
        sb.AppendLine("{");
        sb.AppendLine($"    public partial class {className}");
        sb.AppendLine("    {");

        foreach (var property in properties)
        {
            var propertyName = property.Identifier.Text;
            sb.AppendLine($"        private {property.Type} _{char.ToLower(propertyName[0]) + propertyName.Substring(1)};");
            sb.AppendLine($"        public {property.Type} {propertyName}");
            sb.AppendLine("        {");
            sb.AppendLine("            get => _" + char.ToLower(propertyName[0]) + propertyName.Substring(1) + ";");
            sb.AppendLine("            set");
            sb.AppendLine("            {");
            sb.AppendLine("                _" + char.ToLower(propertyName[0]) + propertyName.Substring(1) + " = value;");
            sb.AppendLine("                OnPropertyChanged();");
            sb.AppendLine("            }");
            sb.AppendLine("        }");
        }

        sb.AppendLine("    }");
        sb.AppendLine("}");

        return sb.ToString();
    }
}

class SyntaxReceiver : ISyntaxReceiver
{
    public List<ClassDeclarationSyntax> CandidateClasses { get; } = new List<ClassDeclarationSyntax>();

    public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
    {
        // 対象のクラスをフィルタリング
        if (syntaxNode is ClassDeclarationSyntax classDeclarationSyntax
            && classDeclarationSyntax.AttributeLists.Count > 0)
        {
            CandidateClasses.Add(classDeclarationSyntax);
        }
    }
}

使用例

このソースジェネレータを使用するには、対象のクラスにAutoNotify属性を付与します。

[AutoNotify]
public partial class SampleClass
{
    public string Name { get; set; }
    public int Age { get; set; }
}

この設定により、SampleClassに対してプロパティのバッキングフィールドが自動生成されます。

これで、シンプルなソースジェネレータの作成方法が理解できました。次は、より高度なソースジェネレータの実装に進みます。

高度なソースジェネレータの実装

シンプルなソースジェネレータを理解したところで、次はより高度なソースジェネレータの実装方法を紹介します。ここでは、属性の解析やコンパイラAPIの活用など、複雑な要件に対応するための技術を取り上げます。

高度な属性解析

ソースジェネレータで高度な処理を行うためには、属性の詳細な解析が必要です。例えば、特定の属性に付随するプロパティを利用して、生成するコードの内容を動的に変更します。

[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
sealed class AutoNotifyAttribute : Attribute
{
    public string PropertyName { get; }

    public AutoNotifyAttribute(string propertyName)
    {
        PropertyName = propertyName;
    }
}

コンパイラAPIの活用

コンパイラAPIを活用することで、より細かい制御が可能になります。以下は、属性解析とコンパイラAPIを組み合わせた高度なソースジェネレータの例です。

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using System.Linq;
using System.Text;

[Generator]
public class AdvancedAutoNotifyGenerator : ISourceGenerator
{
    public void Initialize(GeneratorInitializationContext context)
    {
        context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());
    }

    public void Execute(GeneratorExecutionContext context)
    {
        if (context.SyntaxReceiver is not SyntaxReceiver receiver)
            return;

        var compilation = context.Compilation;

        foreach (var classDeclaration in receiver.CandidateClasses)
        {
            var model = compilation.GetSemanticModel(classDeclaration.SyntaxTree);
            var classSymbol = model.GetDeclaredSymbol(classDeclaration) as INamedTypeSymbol;

            var attributeData = classSymbol.GetAttributes()
                .FirstOrDefault(ad => ad.AttributeClass.ToDisplayString() == "AutoNotifyAttribute");

            if (attributeData != null)
            {
                var propertyName = attributeData.ConstructorArguments[0].Value as string;
                var source = GenerateCode(classSymbol, propertyName);
                context.AddSource($"{classSymbol.Name}_generated.cs", SourceText.From(source, Encoding.UTF8));
            }
        }
    }

    private string GenerateCode(INamedTypeSymbol classSymbol, string propertyName)
    {
        var className = classSymbol.Name;
        var sb = new StringBuilder();
        sb.AppendLine($"namespace {classSymbol.ContainingNamespace}");
        sb.AppendLine("{");
        sb.AppendLine($"    public partial class {className}");
        sb.AppendLine("    {");
        sb.AppendLine($"        private string _{propertyName.ToLower()};");
        sb.AppendLine($"        public string {propertyName}");
        sb.AppendLine("        {");
        sb.AppendLine("            get => _" + propertyName.ToLower() + ";");
        sb.AppendLine("            set");
        sb.AppendLine("            {");
        sb.AppendLine("                _" + propertyName.ToLower() + " = value;");
        sb.AppendLine("                OnPropertyChanged();");
        sb.AppendLine("            }");
        sb.AppendLine("        }");
        sb.AppendLine("    }");
        sb.AppendLine("}");

        return sb.ToString();
    }
}

class SyntaxReceiver : ISyntaxReceiver
{
    public List<ClassDeclarationSyntax> CandidateClasses { get; } = new List<ClassDeclarationSyntax>();

    public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
    {
        if (syntaxNode is ClassDeclarationSyntax classDeclarationSyntax
            && classDeclarationSyntax.AttributeLists.Count > 0)
        {
            CandidateClasses.Add(classDeclarationSyntax);
        }
    }
}

デバッグの改善

高度なソースジェネレータをデバッグする際には、ログ出力やVisual Studioのデバッガを活用することが重要です。以下に、デバッグログを追加する方法を示します。

public void Execute(GeneratorExecutionContext context)
{
    try
    {
        // ソースジェネレータのロジック
    }
    catch (Exception ex)
    {
        context.ReportDiagnostic(Diagnostic.Create(
            new DiagnosticDescriptor("GEN001", "Error", ex.Message, "SourceGenerator", DiagnosticSeverity.Error, true),
            Location.None));
    }
}

これにより、実行時に発生したエラーをVisual Studioの出力ウィンドウに表示できます。

これで、高度なソースジェネレータの実装方法が理解できました。次は、ソースジェネレータのデバッグ方法に進みます。

ソースジェネレータのデバッグ

ソースジェネレータのデバッグは、通常のアプリケーションとは異なる手法が求められます。ここでは、効果的なデバッグ方法と注意点を紹介します。

デバッグの準備

ソースジェネレータをデバッグするためには、まずデバッグモードで実行できるように設定します。Visual Studioを使用する場合、以下の手順に従います。

  1. ソースジェネレータプロジェクトのプロパティを開きます。
  2. 「ビルド」タブで、「Define DEBUG constant」を有効にします。
  3. 「デバッグ」タブで、「外部プログラムを起動」にチェックを入れ、起動するプロジェクトのexeファイル(通常はターゲットプロジェクト)を指定します。

ログ出力の追加

ソースジェネレータ内でログを出力することにより、処理の流れを追跡しやすくなります。以下は、簡単なログ出力の例です。

public void Execute(GeneratorExecutionContext context)
{
    context.AddSource("GeneratorLog", SourceText.From("// Generator execution started", Encoding.UTF8));

    // ソースジェネレータのロジック
    context.AddSource("GeneratorLog", SourceText.From("// Generator execution finished", Encoding.UTF8));
}

このように、context.AddSourceを使用してログをソースコードとして追加することで、ビルド後に生成されたコードと一緒にログを確認できます。

デバッグ中のブレークポイント

ソースジェネレータをデバッグする際には、Debugger.Launch()メソッドを使用して、特定のポイントでデバッガを起動することができます。

public void Execute(GeneratorExecutionContext context)
{
    if (!Debugger.IsAttached)
    {
        Debugger.Launch();
    }

    // ソースジェネレータのロジック
}

これにより、Executeメソッドが呼び出された時点でデバッガが起動し、ブレークポイントを設定して詳細なデバッグが可能になります。

エラーハンドリング

ソースジェネレータ内で例外が発生した場合、適切にハンドリングし、開発者にフィードバックを提供することが重要です。

public void Execute(GeneratorExecutionContext context)
{
    try
    {
        // ソースジェネレータのロジック
    }
    catch (Exception ex)
    {
        context.ReportDiagnostic(Diagnostic.Create(
            new DiagnosticDescriptor("GEN001", "Error", ex.Message, "SourceGenerator", DiagnosticSeverity.Error, true),
            Location.None));
    }
}

このように、例外をキャッチして診断メッセージとして報告することで、エラーの原因を特定しやすくなります。

生成コードの検証

生成されたコードを検証するために、ビルド後に生成されたソースファイルを確認します。Visual Studioでは、「オブジェクトエクスプローラー」を使用して生成されたソースコードを参照できます。

これで、ソースジェネレータのデバッグ方法と注意点を理解できました。次は、ソースジェネレータの応用例として、コードの自動生成に進みます。

応用例:コードの自動生成

ここでは、実際のプロジェクトでソースジェネレータを活用してコードを自動生成する方法を紹介します。この応用例では、モデルクラスから自動的にデータアクセスコードを生成するジェネレータを作成します。

モデルクラスの定義

まず、データアクセスコードを生成する対象となるモデルクラスを定義します。このクラスには、自動生成の基となる属性を付与します。

[GenerateDataAccess]
public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
}

属性の定義

次に、モデルクラスに付与するGenerateDataAccess属性を定義します。

[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
sealed class GenerateDataAccessAttribute : Attribute
{
    public GenerateDataAccessAttribute() { }
}

ソースジェネレータの実装

モデルクラスからデータアクセスコードを生成するソースジェネレータを実装します。

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using System.Linq;
using System.Text;

[Generator]
public class DataAccessGenerator : ISourceGenerator
{
    public void Initialize(GeneratorInitializationContext context)
    {
        context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());
    }

    public void Execute(GeneratorExecutionContext context)
    {
        if (context.SyntaxReceiver is not SyntaxReceiver receiver)
            return;

        var compilation = context.Compilation;

        foreach (var classDeclaration in receiver.CandidateClasses)
        {
            var model = compilation.GetSemanticModel(classDeclaration.SyntaxTree);
            var classSymbol = model.GetDeclaredSymbol(classDeclaration) as INamedTypeSymbol;

            var attributeData = classSymbol.GetAttributes()
                .FirstOrDefault(ad => ad.AttributeClass.ToDisplayString() == "GenerateDataAccessAttribute");

            if (attributeData != null)
            {
                var source = GenerateDataAccessCode(classSymbol);
                context.AddSource($"{classSymbol.Name}_DataAccess.cs", SourceText.From(source, Encoding.UTF8));
            }
        }
    }

    private string GenerateDataAccessCode(INamedTypeSymbol classSymbol)
    {
        var className = classSymbol.Name;
        var properties = classSymbol.GetMembers().OfType<IPropertySymbol>();

        var sb = new StringBuilder();
        sb.AppendLine($"namespace {classSymbol.ContainingNamespace}");
        sb.AppendLine("{");
        sb.AppendLine($"    public class {className}DataAccess");
        sb.AppendLine("    {");
        sb.AppendLine($"        public void Insert({className} entity)");
        sb.AppendLine("        {");
        sb.AppendLine("            // Insert logic here");
        sb.AppendLine("        }");

        sb.AppendLine($"        public void Update({className} entity)");
        sb.AppendLine("        {");
        sb.AppendLine("            // Update logic here");
        sb.AppendLine("        }");

        sb.AppendLine($"        public void Delete(int id)");
        sb.AppendLine("        {");
        sb.AppendLine("            // Delete logic here");
        sb.AppendLine("        }");

        sb.AppendLine($"        public {className} GetById(int id)");
        sb.AppendLine("        {");
        sb.AppendLine("            // Get by ID logic here");
        sb.AppendLine("            return new {className}();");
        sb.AppendLine("        }");

        sb.AppendLine("    }");
        sb.AppendLine("}");

        return sb.ToString();
    }
}

class SyntaxReceiver : ISyntaxReceiver
{
    public List<ClassDeclarationSyntax> CandidateClasses { get; } = new List<ClassDeclarationSyntax>();

    public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
    {
        if (syntaxNode is ClassDeclarationSyntax classDeclarationSyntax
            && classDeclarationSyntax.AttributeLists.Count > 0)
        {
            CandidateClasses.Add(classDeclarationSyntax);
        }
    }
}

使用例

このソースジェネレータを使用することで、モデルクラスに基づいたデータアクセスコードが自動生成されます。生成されたコードを使用する例を示します。

class Program
{
    static void Main(string[] args)
    {
        var customer = new Customer { Id = 1, Name = "John Doe", Email = "john@example.com" };
        var dataAccess = new CustomerDataAccess();

        dataAccess.Insert(customer);
        var retrievedCustomer = dataAccess.GetById(1);
    }
}

この応用例により、ソースジェネレータを使ってプロジェクトの一部を自動化し、コードの一貫性とメンテナンス性を向上させる方法が理解できました。次は、ソースジェネレータの実装力を高めるための演習問題に進みます。

演習問題:自作ソースジェネレータ

ここでは、ソースジェネレータの理解を深めるために、自分でソースジェネレータを作成するための演習問題を提供します。この演習を通して、ソースジェネレータの基本的な使い方から高度な機能までを実践的に学びます。

演習1: 基本的なソースジェネレータの作成

以下の手順に従って、基本的なソースジェネレータを作成し、指定されたクラスに対して追加のメソッドを自動生成してください。

属性の定義

まず、対象クラスに付与する属性を定義します。

[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
sealed class GenerateToStringAttribute : Attribute
{
    public GenerateToStringAttribute() { }
}

ソースジェネレータの実装

次に、GenerateToStringAttributeが付与されたクラスに対して、ToStringメソッドを生成するソースジェネレータを実装します。

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using System.Linq;
using System.Text;

[Generator]
public class ToStringGenerator : ISourceGenerator
{
    public void Initialize(GeneratorInitializationContext context)
    {
        context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());
    }

    public void Execute(GeneratorExecutionContext context)
    {
        if (context.SyntaxReceiver is not SyntaxReceiver receiver)
            return;

        var compilation = context.Compilation;

        foreach (var classDeclaration in receiver.CandidateClasses)
        {
            var model = compilation.GetSemanticModel(classDeclaration.SyntaxTree);
            var classSymbol = model.GetDeclaredSymbol(classDeclaration) as INamedTypeSymbol;

            var attributeData = classSymbol.GetAttributes()
                .FirstOrDefault(ad => ad.AttributeClass.ToDisplayString() == "GenerateToStringAttribute");

            if (attributeData != null)
            {
                var source = GenerateToStringMethod(classSymbol);
                context.AddSource($"{classSymbol.Name}_ToString.cs", SourceText.From(source, Encoding.UTF8));
            }
        }
    }

    private string GenerateToStringMethod(INamedTypeSymbol classSymbol)
    {
        var className = classSymbol.Name;
        var properties = classSymbol.GetMembers().OfType<IPropertySymbol>();

        var sb = new StringBuilder();
        sb.AppendLine($"namespace {classSymbol.ContainingNamespace}");
        sb.AppendLine("{");
        sb.AppendLine($"    public partial class {className}");
        sb.AppendLine("    {");
        sb.AppendLine("        public override string ToString()");
        sb.AppendLine("        {");
        sb.AppendLine("            return $\"{");

        foreach (var property in properties)
        {
            sb.AppendLine($"{property.Name}: {{{property.Name}}}, ");
        }

        sb.AppendLine("}\";");
        sb.AppendLine("        }");
        sb.AppendLine("    }");
        sb.AppendLine("}");

        return sb.ToString();
    }
}

class SyntaxReceiver : ISyntaxReceiver
{
    public List<ClassDeclarationSyntax> CandidateClasses { get; } = new List<ClassDeclarationSyntax>();

    public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
    {
        if (syntaxNode is ClassDeclarationSyntax classDeclarationSyntax
            && classDeclarationSyntax.AttributeLists.Count > 0)
        {
            CandidateClasses.Add(classDeclarationSyntax);
        }
    }
}

使用例

最後に、GenerateToString属性をクラスに付与し、ToStringメソッドが生成されることを確認します。

[GenerateToString]
public partial class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}

class Program
{
    static void Main(string[] args)
    {
        var person = new Person { Name = "John", Age = 30 };
        Console.WriteLine(person.ToString());
    }
}

演習2: 高度なソースジェネレータの実装

以下の手順に従って、より高度なソースジェネレータを作成し、モデルクラスからデータバインディングのためのINotifyPropertyChangedインターフェースを実装してください。

属性の定義

まず、対象クラスに付与する属性を定義します。

[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
sealed class ImplementNotifyPropertyChangedAttribute : Attribute
{
    public ImplementNotifyPropertyChangedAttribute() { }
}

ソースジェネレータの実装

次に、ImplementNotifyPropertyChangedAttributeが付与されたクラスに対して、INotifyPropertyChangedインターフェースの実装を生成するソースジェネレータを実装します。

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using System.Linq;
using System.Text;

[Generator]
public class NotifyPropertyChangedGenerator : ISourceGenerator
{
    public void Initialize(GeneratorInitializationContext context)
    {
        context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());
    }

    public void Execute(GeneratorExecutionContext context)
    {
        if (context.SyntaxReceiver is not SyntaxReceiver receiver)
            return;

        var compilation = context.Compilation;

        foreach (var classDeclaration in receiver.CandidateClasses)
        {
            var model = compilation.GetSemanticModel(classDeclaration.SyntaxTree);
            var classSymbol = model.GetDeclaredSymbol(classDeclaration) as INamedTypeSymbol;

            var attributeData = classSymbol.GetAttributes()
                .FirstOrDefault(ad => ad.AttributeClass.ToDisplayString() == "ImplementNotifyPropertyChangedAttribute");

            if (attributeData != null)
            {
                var source = GenerateNotifyPropertyChangedImplementation(classSymbol);
                context.AddSource($"{classSymbol.Name}_NotifyPropertyChanged.cs", SourceText.From(source, Encoding.UTF8));
            }
        }
    }

    private string GenerateNotifyPropertyChangedImplementation(INamedTypeSymbol classSymbol)
    {
        var className = classSymbol.Name;
        var properties = classSymbol.GetMembers().OfType<IPropertySymbol>();

        var sb = new StringBuilder();
        sb.AppendLine($"namespace {classSymbol.ContainingNamespace}");
        sb.AppendLine("{");
        sb.AppendLine($"    public partial class {className} : System.ComponentModel.INotifyPropertyChanged");
        sb.AppendLine("    {");
        sb.AppendLine("        public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;");
        sb.AppendLine("        protected void OnPropertyChanged(string propertyName)");
        sb.AppendLine("        {");
        sb.AppendLine("            PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName));");
        sb.AppendLine("        }");

        foreach (var property in properties)
        {
            var propertyName = property.Name;
            var fieldName = $"_{char.ToLower(propertyName[0])}{propertyName.Substring(1)}";
            sb.AppendLine($"        private {property.Type} {fieldName};");
            sb.AppendLine($"        public {property.Type} {propertyName}");
            sb.AppendLine("        {");
            sb.AppendLine($"            get => {fieldName};");
            sb.AppendLine("            set");
            sb.AppendLine("            {");
            sb.AppendLine($"                if (!EqualityComparer<{property.Type}>.Default.Equals({fieldName}, value))");
            sb.AppendLine("                {");
            sb.AppendLine($"                    {fieldName} = value;");
            sb.AppendLine("                    OnPropertyChanged(nameof(" + propertyName + "));");
            sb.AppendLine("                }");
            sb.AppendLine("            }");
            sb.AppendLine("        }");
        }

        sb.AppendLine("    }");
        sb.AppendLine("}");

        return sb.ToString();
    }
}

class SyntaxReceiver : ISyntaxReceiver
{
    public List<ClassDeclarationSyntax> CandidateClasses { get; } = new List<ClassDeclarationSyntax>();

    public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
    {
        if (syntaxNode is ClassDeclarationSyntax classDeclarationSyntax
            && classDeclarationSyntax.AttributeLists.Count > 0)
        {
            CandidateClasses.Add(classDeclarationSyntax);
        }
    }
}

使用例

最後に、ImplementNotifyPropertyChanged属性をクラスに付与し、INotifyPropertyChangedインターフェースが正しく実装されることを確認します。

[ImplementNotifyPropertyChanged]
public partial class Product
{
    public string Name { get; set; }
    public decimal Price { get; set; }
}

class Program
{
    static void Main(string[] args)
    {
        var product = new Product { Name = "Laptop", Price = 999.99m };
        product.PropertyChanged += (sender, e) => Console.WriteLine($"{e.PropertyName} has changed");
        product.Name = "Gaming Laptop";
    }
}

これらの演習問題を通じて、自作のソースジェネレータを作成し、その機能を試してみてください。各演習を行うことで、ソースジェネレータの実践的な知識とスキルを深めることができます。

コメント

コメントする

目次