C#のソースコードからPlantUmlのクラス図を生成するSourceGenerator 0.6.0-beta をリリースしました

## PlantUmlClassDiagramGenerator.SourceGenerator 0.6.0-beta
C#のソースコードからPlantUmlのクラス図を生成するツールをリリースしました。

https://www.nuget.org/packages/PlantUmlClassDiagramGenerator.SourceGenerator/0.6.0-beta

### 過去の記事はこちら

https://qiita.com/pierusan2010/items/cdd628f202a9767cf65d

https://qiita.com/pierusan2010/items/266656eb9d9f5d2bd24f

### ソースコード

https://github.com/pierre3/PlantUmlClassDiagramGenerator/tree/master/src/PlantUmlClassDiagramGenerator.SourceGenerator


## 0.5.1-beta からの変更点
今回はSourceGeneratorで使える属性をいくつか増やしました。
また、プロジェクト（アセンブリ）単位の設定をプロジェクトファイル(.csproj)で定義していましたが、assemblyに属性を付ける方式に変更しました。

::: note
リリースノート
- プロジェクトファイル(.csproj)で設定するプロパティを一部廃止しました。  
  このバージョンから`PlantUmlGenerator_OutputDir`のみが設定可能となります
- [PlantUmlDiagramAttribute](#plantumldiagramattribute)をアセンブリ単位で付与できるように変更しました。  
  アセンブリ単位で付与した場合、その中で定義されている全ての型を出力対象とします
- [PlantUmlAssociationAttribute](#plantumlassociationattribute)を追加しました。  
  独自の関連付けの定義が可能となりました
- [PlantUmlIgnoreAssociationAttribute](#plantumlignoreassociationattribute)を追加しました。  
  関連を作成したくないメンバーやメソッドのパラメータに付与し、それらを関連の作成対象から除外します
- [PlantUmlExtraAssociationTargetsAttribute](#plantumlextraassociationtargets)を追加しました。  
  関連付けの作成対象となる型を追加で指定します
:::

## 使い方

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

[PlantUmlClassDiagramGenerator.SourceGenerator](https://www.nuget.org/packages/PlantUmlClassDiagramGenerator.SourceGenerator) パッケージを NuGet Gallery から取得し、.NET プロジェクトにインストールします。

### 2. プロジェクトファイルの編集

#### 2.1 条件付きコンパイルシンボルに "GENERATE_PLANTUML" を含める
  
このツールは、プリプロセッサシンボルに "GENERATE_PLANTUML" が定義されている場合にのみ動作します。ツールはコーディング中に常に動作する必要がなく、必要になったタイミングで 1 度だけ実行すれば十分です。そのため、特定のビルド構成時のみ動作する仕組みとしています。

プロジェクトのビルド構成の条件付きコンパイルシンボルに "GENERATE_PLANTUML" を追加します。

リリースビルド時にツールを実行するように設定するには、.csproj ファイルに以下のセクションを追加します。
```xml
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
    <DefineConstants>$(DefineConstants);GENERATE_PLANTUML</DefineConstants>
</PropertyGroup>
```

#### 2.2 プロジェクト設定の追加
プロジェクトファイル(.csproj)に下記セクションを追加します。

```xml
<ItemGroup>
	<CompilerVisibleProperty Include="PlantUmlGenerator_OutputDir" />
</ItemGroup>
<PropertyGroup>
	<PlantUmlGenerator_OutputDir>$(ProjectDir)generated-uml</PlantUmlGenerator_OutputDir>
</PropertyGroup>
```

|プロパティ|説明|既定値|
|--|--|--|
|PlantUmlGenerator_OutputDir|生成したUMLファイルの配置先ディレクトリを指定します。<br>異なるプロジェクト間で関連付けを行いたい場合は、関連する全てのプロジェクトで同じディレクトリを指定してください。|$(ProjectDir)generated_uml|


### 3. 属性値の設定
必要に応じて、型やそのメンバーに対して属性値を設定します。

#### PlantUmlDiagramAttribute

アセンブリと型の定義（クラス、構造体、インターフェイス、列挙型）に付与することができます。
この属性を付与した型がクラス図の出力対象となります。
アセンブリに付与した場合はアセンブリ内で定義している全ての型を出力の対象とします。

```cs
[assembly:PlantUmlDiagram]
```

```cs
[PlantUmlDiagram]
class ClassA
{

}
```

また、`IncludeMemberAccessibilities`プロパティを設定することで表示対象とするメンバーのアクセシビリティを指定することができます。
アセンブリと個別の型定義の両方に指定がある場合は、型定義の設定が優先されます。

```cs
[PlantUmlDiagram(IncludeMemberAccessibilities = Accessibilities.Public| Accessibilities.Protected)]
class ClassA
{
    private int n = 0;
    public int X { get;set;}
    protected string A { get;set; }
}
```

```plantuml
class ClassA{
    + X : int <<get>><<set>>
    # A : string <<get>><<set>>
}
```

逆に`IncludeMemberAccessibilities`プロパティを使って表示対象外とするメンバーを指定することもできます。

```cs
[PlantUmlDiagram(ExcludeMemberAccessibilities = Accessibilities.Private)]
class ClassA
{
    private int n = 0;
    public int X { get;set;}
    protected string A { get;set; }
}
```

#### PlantUmlIgnoreAttribute

アセンブリ単位で`PlantUmlDiagramAttribute`を定義している場合、アセンブリ内で定義されている全ての型が出力の対象となります。
一部の型を出力の対象外としたい場合は、その型に`PlantUmlIgnoreAttribute`を設定します。

```csharp
[PlantUmlIgnore]
class ClassA
{
}

class ClassB
{

}
```

```plantuml
class ClassB{

}
```

`PlantUmlIgnoreAttribute`はある型の特定のメンバーのみを非表示としたい場合にも利用できます。

```cs
class ClassA
{
    public int X {get;set;}
    [PlantUmlIgnore]
    public int Y {get;set;}

    public void MethodA(){}
    [PlantUmlIgnore]
    public void MethodB(){}
}
```

```plantuml
class ClassA {
   + X : int <<get>><<set>>
   + MethodA() : void
}
```

#### PlantUmlAssociationAttribute
この属性は型のメンバーやメソッドのパラメータに付与し、独自の関連付けを作成する場合に使用します。
下記プロパティで作成する関連の詳細を指定します。ここでは属性が付与されている型をRoot Type、関連する相手方の型をLeaf Type とします。

|プロパティ|型|説明|
|--|--|--|
|Node|string|関連の種類に対応した文字列を指定します。（`o--`、`..>`など）|
|LeafType|System.Type|Leaf側の型を指定|
|RootLabel|string|Root側に付加するラベルを指定|
|NodeLabel|string|RootとLeafを結ぶ線上に付加するラベルを指定|
|LeafLabel|string|Leaf側に付加するラベルを指定|


```cs
internal class SampleModel
{
    private readonly ILogger logger;

    [PlantUmlAssociation("*--",
        LeafType = typeof(Item),
        RootLabel = "IDictionary<string,Item>",
        LeafLabel = "*",
        NodeLabel = nameof(Items))]
    public IDictionary<string, Item> Items { get; } = new Dictionary<string, Item>();

    public SampleModel([PlantUmlAssociation("..>", NodeLabel = "Injection")] ILogger logger)
    {
        this.logger = logger;
    }
}
```

```
@startuml SampleModel
class SampleModel {
    - <<readonly>> logger : ILogger
    + <<readonly>> Items : IDictionary<string, Item> <<get>>
    + SampleModel(logger : ILogger)
}
SampleModel o-l- ILogger : logger
SampleModel "IDictionary<string,Item>" *-- "*" Item : Items
SampleModel ..> ILogger : Injection
@enduml
```

```plantuml
@startuml SampleModel
class SampleModel {
    - <<readonly>> logger : ILogger
    + <<readonly>> Items : IDictionary<string, Item> <<get>>
    + SampleModel(logger : ILogger)
}
SampleModel o-l- ILogger : logger
SampleModel "IDictionary<string,Item>" *-- "*" Item : Items
SampleModel ..> ILogger : Injection
@enduml
```
#### PlantUmlIgnoreAssociationAttribute
関連を作成したくないメンバーに付与することで、自動的な関連の生成を抑制します。

```csharp
internal class SampleModel
{
    [PlantUmlIgnoreAssociation]
    private readonly ILogger logger;

    [PlantUmlAssociation("*--",
        LeafType = typeof(Item),
        RootLabel = "IDictionary<string,Item>",
        LeafLabel = "*",
        NodeLabel = nameof(Items))]
    public IDictionary<string, Item> Items { get; } = new Dictionary<string, Item>();

    public SampleModel([PlantUmlIgnoreAssociation] ILogger logger)
    {
        this.logger = logger;
    }
}
```

```
@startuml SampleModel
class SampleModel  {
    - <<readonly>> logger : ILogger
    + <<readonly>> Items : IDictionary<string, Item> <<get>>
    + SampleModel(logger : ILogger)
}
SampleModel "IDictionary<string,Item>" *-- "*" Item : Items
@enduml
```

```plantuml
@startuml SampleModel
class SampleModel  {
    - <<readonly>> logger : ILogger
    + <<readonly>> Items : IDictionary<string, Item> <<get>>
    + SampleModel(logger : ILogger)
}
SampleModel "IDictionary<string,Item>" *-- "*" Item : Items
@enduml
```

#### PlantUmlExtraAssociationTargetsAttribute
関連付けの対象とする追加の型を指定します。

このツールでは、次の条件に当てはまる型を関連付けの対象としています。
- プロジェクト内で出力対象となっている型
- 出力先フォルダ内に.pumlファイルが存在する型

これ以外の型との関連を作成したい場合は `PlantUmlExtraAssociationTargetsAttribute` で登録します。
この属性はアセンブリと型定義に付与できます。

```csharp
[assembly: PlantUmlExtraAssociationTargets(
    typeof(KeyValuePair<,>),
    typeof(System.Net.Http.HttpClient))]

[PlantUmlExtraAssociationTargets(typeof(System.IO.Textwriter))]
internal class SampleModel
{
    private HttpClient httpClient;
    public IDictionary<string, Item> Items { get; set; } = new Dictionary<string, Item>();
    
    public SampleModel(HttpClient httpClient)
    {
        this.httpClient = httpClient;
    }

    public void Write(TextWriter writer)
    {
        writer.Write("hoge");
    }
}
```

```
@startuml SampleModel
class SampleModel  {
    - httpClient : HttpClient
    + Items : IDictionary<string, Item> <<get>> <<set>>
    + SampleModel(httpClient : HttpClient)
    + Write(writer : TextWriter) : void
}
SampleModel o-- HttpClient : httpClient
SampleModel *-- "*" "KeyValuePair`2" : Items
SampleModel ..> HttpClient
SampleModel ..> TextWriter
@enduml
```


```plantuml
@startuml SampleModel
class SampleModel  {
    - httpClient : HttpClient
    + Items : IDictionary<string, Item> <<get>> <<set>>
    + SampleModel(httpClient : HttpClient)
    + Write(writer : TextWriter) : void
}
SampleModel o-- HttpClient : httpClient
SampleModel *-- "*" "KeyValuePair`2" : Items
SampleModel ..> HttpClient
SampleModel ..> TextWriter
@enduml
```
## 仕様

### UMLファイルの出力
`PlantUmlGenerator_OutputDir`で指定した出力先ディレクトリ配下に「アセンブリ名」のフォルダ、その下に「名前空間」のフォルダが作成されます。

例: 
以下のプロジェクト構成のソリューションがあるとします。
|プロジェクト|アセンブリ名|名前空間|
|--|--|--|
|Consoto.App1.csproj|Consoto.App1|・Consoto.App1<br>・Consoto.App1.ViewModel<br>・Consoto.App1.Model|
|Consoto.App1.Core.csproj|Consoto.App1.Core|・Consoto.App1.Core<br>・Consoto.App1.Core.Extensions|

各プロジェクト毎に`PlantUmlClassDiagramGenerator.SourceGenerator`のパッケージをインストールし、`PlantUmlGenerator_OutputDir` を`$(SolutionDir)generated_uml`とした場合、下図のようなフォルダ構成でUMLファイルが出力されます。

```plantuml
salt
{
  {T
    + <&folder> <b>$(SolutionDir)</b>
      ++ <&folder> <b>generated_uml</b>
        +++ <&folder> <b>Consoto.App1</b> | -- [Assembly Name]
          ++++ <&folder> <b>Consoto</b>
            +++++ <&folder> <b>App1</b> | -- Consoto.App1
              ++++++ <&file> class001.puml
              ++++++ <&file> class002.puml
              ++++++ <&folder> <b>ViewModel</b> | -- Consoto.App1.ViewModel
                +++++++ <&file> class003.puml
                +++++++ <&file> class004.puml
              ++++++ <&folder> <b>Model</b> | -- Consoto.App1.Model
                +++++++ <&file> class005.puml
            
        +++ <&folder> <b>Consoto.App1.Core</b> | -- [Assembly Name]
          ++++ <&folder> <b>Consoto</b>
            +++++ <&folder> <b>App1</b>
              ++++++ <&folder> <b>Core</b> | -- Consoto.App1.Core
                +++++++ <&file> class008.puml
                +++++++ <&file> class009.puml
                  ++++++++ <&folder> <b>Extensions</b> | -- Consoto.App1.Core.Extensions
                    +++++++++ <&file> class010.puml
                    +++++++++ <&file> class011.puml
  }
}
```

### 型の表現
PlantUMLで利用できる型のキーワードは以下の通りです。
- class
- struct
- interface
- abstract class
- enum
  
`record`, `static`,`sealed`などの修飾子はステレオタイプ(`<<keyword>>`)で表現します。

<details><summary>C#</summary>

```cs
class ClassA
{
    public string Name { get; }
    public int Value { get; }
    public ClassA(string name, int value) => (Name, Value) = (name, value);
}

static class StaticClass
{
    public static string SpecificName = "Hoge";
    public static string Piyo(int count) => string.Join(" ", Enumerable.Repeat("Pyyo", count));
}

abstract class AbstractClass
{
    public abstract void MethodA();
    public abstract void MethodB();
}

record RecordA(string Name,int Value);

public struct StructA()
{
    public float X { get; set; }
    public float Y { get; set; }
    public float Z { get; set; }
}

public record struct RecordStruct(float X, float Y, float Z);

public enum LogLevel
{
    Trace,
    Debug,
    Info,
    Warn,
    Error,
    Fatal
}

[Flags]
enum Accessibilities
{
    None = 0,
    Public = 0x01,
    Protected = 0x02,
    Internal = 0x04,
    ProtectedInternal = 0x08,
    PrivateProtected = 0x10,
    Private = 0x20,
    All = Public | Protected | Internal | ProtectedInternal | PrivateProtected | Private
}
```
</details>

<details><summary>PlantUml</summary>

```
class ClassA  {
    + <<readonly>> Name : string <<get>>
    + <<readonly>> Value : int <<get>>
    + ClassA(name : string, value : int)
}
class StaticClass <<static>>  {
    + {static} SpecificName : string
    + {static} Piyo(count : int) : string
    - {static} StaticClass()
}
abstract class AbstractClass  {
    + {abstract} MethodA() : void
    + {abstract} MethodB() : void
    # AbstractClass()
}
class RecordA <<record>>  {
    + RecordA(Name : string, Value : int)
    # <<readonly>> <<virtual>> EqualityContract : Type <<get>>
    + Name : string <<get>> <<set>>
    + Value : int <<get>> <<set>>
    + <<override>> ToString() : string
    # <<virtual>> PrintMembers(builder : StringBuilder) : bool
    + {static} operator !=(left : RecordA?, right : RecordA?) : bool
    + {static} operator ==(left : RecordA?, right : RecordA?) : bool
    + <<override>> GetHashCode() : int
    + <<override>> Equals(obj : object?) : bool
    + <<virtual>> Equals(other : RecordA?) : bool
    # RecordA(original : RecordA)
    + Deconstruct(Name : string, Value : int) : void
}
struct StructA <<sealed>>  {
    + StructA()
    + X : float <<get>> <<set>>
    + Y : float <<get>> <<set>>
    + Z : float <<get>> <<set>>
}
struct RecordStruct <<sealed>> <<record>>  {
    + RecordStruct(X : float, Y : float, Z : float)
    + X : float <<get>> <<set>>
    + Y : float <<get>> <<set>>
    + Z : float <<get>> <<set>>
    + <<readonly>> <<override>> ToString() : string
    - <<readonly>> PrintMembers(builder : StringBuilder) : bool
    + {static} operator !=(left : RecordStruct, right : RecordStruct) : bool
    + {static} operator ==(left : RecordStruct, right : RecordStruct) : bool
    + <<readonly>> <<override>> GetHashCode() : int
    + <<readonly>> <<override>> Equals(obj : object) : bool
    + <<readonly>> Equals(other : RecordStruct) : bool
    + <<readonly>> Deconstruct(X : float, Y : float, Z : float) : void
    + RecordStruct()
}
enum LogLevel <<sealed>>  {
    Trace = 0
    Debug = 1
    Info = 2
    Warn = 3
    Error = 4
    Fatal = 5
    + LogLevel()
}
enum Accessibilities <<Flags>> <<sealed>>  {
    None = 0
    Public = 1
    Protected = 2
    Internal = 4
    ProtectedInternal = 8
    PrivateProtected = 16
    Private = 32
    All = Public | Protected | Internal | ProtectedInternal | PrivateProtected | Private
    + Accessibilities()
}
```

</details>

```plantuml
class ClassA  {
    + <<readonly>> Name : string <<get>>
    + <<readonly>> Value : int <<get>>
    + ClassA(name : string, value : int)
}
class StaticClass <<static>>  {
    + {static} SpecificName : string
    + {static} Piyo(count : int) : string
    - {static} StaticClass()
}
abstract class AbstractClass  {
    + {abstract} MethodA() : void
    + {abstract} MethodB() : void
    # AbstractClass()
}
class RecordA <<record>>  {
    + RecordA(Name : string, Value : int)
    # <<readonly>> <<virtual>> EqualityContract : Type <<get>>
    + Name : string <<get>> <<set>>
    + Value : int <<get>> <<set>>
    + <<override>> ToString() : string
    # <<virtual>> PrintMembers(builder : StringBuilder) : bool
    + {static} operator !=(left : RecordA?, right : RecordA?) : bool
    + {static} operator ==(left : RecordA?, right : RecordA?) : bool
    + <<override>> GetHashCode() : int
    + <<override>> Equals(obj : object?) : bool
    + <<virtual>> Equals(other : RecordA?) : bool
    # RecordA(original : RecordA)
    + Deconstruct(Name : string, Value : int) : void
}
struct StructA <<sealed>>  {
    + StructA()
    + X : float <<get>> <<set>>
    + Y : float <<get>> <<set>>
    + Z : float <<get>> <<set>>
}
struct RecordStruct <<sealed>> <<record>>  {
    + RecordStruct(X : float, Y : float, Z : float)
    + X : float <<get>> <<set>>
    + Y : float <<get>> <<set>>
    + Z : float <<get>> <<set>>
    + <<readonly>> <<override>> ToString() : string
    - <<readonly>> PrintMembers(builder : StringBuilder) : bool
    + {static} operator !=(left : RecordStruct, right : RecordStruct) : bool
    + {static} operator ==(left : RecordStruct, right : RecordStruct) : bool
    + <<readonly>> <<override>> GetHashCode() : int
    + <<readonly>> <<override>> Equals(obj : object) : bool
    + <<readonly>> Equals(other : RecordStruct) : bool
    + <<readonly>> Deconstruct(X : float, Y : float, Z : float) : void
    + RecordStruct()
}
enum LogLevel <<sealed>>  {
    Trace = 0
    Debug = 1
    Info = 2
    Warn = 3
    Error = 4
    Fatal = 5
    + LogLevel()
}
enum Accessibilities <<Flags>> <<sealed>>  {
    None = 0
    Public = 1
    Protected = 2
    Internal = 4
    ProtectedInternal = 8
    PrivateProtected = 16
    Private = 32
    All = Public | Protected | Internal | ProtectedInternal | PrivateProtected | Private
    + Accessibilities()
}
```

### 関連(Association)
ある型と他の型との関連付けは、個々の型定義の下に追加されます。関連付けを行う条件と付与する関連の種類は以下の通りです。



#### 継承
対象の型がObject、ValueType、Struct以外の型を継承している場合、`Inheritance (<|--)`の関連を追加します。

```cs
public abstract class PlanetBase
{
}

public class Earth : PlanetBase
{
}
```

```
abstract class PlanetBase {
}

class Earth {
}

PlanetBase <|-- Earth
```

```plantuml
abstract class PlanetBase{
}
class Earth {
}
PlanetBase <|-- Earth
```

#### インターフェイスの実装
対象の型がインターフェイスを実装している場合、`Realization (<|..)`の関連を追加します。

```cs
interface ILogger 
{
}
class Logger : ILogger
{
}
```

```
interface ILogger {
}

class Logger {
}

ILogger <|.. Logger
```

```plantuml
interface ILogger {
}

class Logger {
}

ILogger <|.. Logger
```

#### プロパティまたはフィールドの関連
プロパティ、フィールドの型が出力対象の型の場合、`Aggrigation (o--)`または`Composition (*--)`の関連を追加します。

次の条件を満たす場合に`Composition`、それ以外は`Aggrigation`となります。
- プロパティやフィールドの初期化子で初期化している
- コンストラクタ内で初期化している

```cs
class Class1
{
    public Item ItemA {get;} = new Item();  //初期化子で初期化
    public Item ItemB {get;}
    public Item ItemC {get;}
    public Class1(Item item) 
    {
        ItemB = new Item();  //コンストラクタで初期化
        ItemC = item;        //外部から注入
    }
}
```

```
class Class1 {
    + <<readonly>> ItemA : Item <<get>>
    + <<readonly>> ItemB : Item <<get>>
    + <<readonly>> ItemC : Item <<get>>
    + Class1(item : Item)
}
Class1 *-- Item : ItemA
Class1 *-- Item : ItemB
Class1 o-- Item : ItemC
Class1 ..> Item
```

```plantuml
class Class1 {
    + <<readonly>> ItemA : Item <<get>>
    + <<readonly>> ItemB : Item <<get>>
    + <<readonly>> ItemC : Item <<get>>
    + Class1(item : Item)
}
Class1 *-- Item : ItemA
Class1 *-- Item : ItemB
Class1 o-- Item : ItemC
Class1 ..> Item
```


##### 配列型または`IEnumerable<T>`を実装している型の場合
プロパティやフィールドの型との関連ではなく、要素の型との関連を追加します。その際、要素の型側に多重度`"*"`を付与します。

```cs
public abstract class PlanetBase(string name)
{
    public string Name { get; set; } = name;
    public IList<Moon> Moons { get; } = new List<Moon>();
    protected void AddMoon(Moon moon)
    {
        Moons.Add(moon);
    }
}
```
```
abstract class PlanetBase  {
    # PlanetBase(name : string)
    + Name : string <<get>> <<set>>
    + <<readonly>> Moons : IList<Moon> <<get>>
    # AddMoon(moon : Moon) : void
}
PlanetBase *-- "*" Moon : Moons
PlanetBase ..> Moon
```

```plantuml
abstract class PlanetBase  {
    # PlanetBase(name : string)
    + Name : string <<get>> <<set>>
    + <<readonly>> Moons : IList<Moon> <<get>>
    # AddMoon(moon : Moon) : void
}
PlanetBase *-- "*" Moon : Moons
PlanetBase ..> Moon
```

#### メソッドのパラメータ
メソッドのパラメータの型が出力対象の型の場合に `Dependency (..>)`の関連を追加します。

```cs
class Parameters
{
   public int A {get;set;}
   public int B {get;set;}
}
class ClassA
{
    public void Execute(Parameters parameters)
    {
       Console.WriteLine($"({parameters.A},{parameters.B})");
    }
}
```

```plantuml
class Parameters {
    + A : int <<get>><<set>>
    + B : int <<get>><<set>>
}
class ClassA {
    + Execute(parameters : Parameters) : void
}
ClassA ..> Parameters
```

#### ネストした型
メンバーに型の定義が含まれる場合 `Nested (+--)` の関連を追加します。
ネストした型の名前は `{親の型名}::{型名}`の形式で付けられます。

<details><summary>C#</summary>

```cs
class Parent
{
    class ChiledA
    {
        class GrandchildA
        {
            class GreatGrandchild
            {

            }
        }
        class GrandchildB
        {
            class GreatGrandchild
            {

            }
        }
    }
    class ChildeB
    {
        class GrandchildA
        {
            class GreatGrandchild
            {
            }
        }
        class GrandchildB
        {

        }
    }
}
```

</details>

<details><summary>PlantUML</summary>

```
class Parent::ChiledA::GrandchildA::GreatGrandchild  {
    + GreatGrandchild()
}

class Parent::ChiledA::GrandchildB::GreatGrandchild  {
    + GreatGrandchild()
}

class Parent::ChiledA::GrandchildB  {
    + GrandchildB()
}
Parent::ChiledA::GrandchildB +.. Parent::ChiledA::GrandchildB::GreatGrandchild

class Parent::ChiledA::GrandchildA  {
    + GrandchildA()
}
Parent::ChiledA::GrandchildA +.. Parent::ChiledA::GrandchildA::GreatGrandchild

class Parent::ChiledA  {
    + ChiledA()
}
Parent::ChiledA +.. Parent::ChiledA::GrandchildA
Parent::ChiledA +.. Parent::ChiledA::GrandchildB

class Parent::ChildeB::GrandchildA::GreatGrandchild  {
    + GreatGrandchild()
}

class Parent::ChildeB::GrandchildA  {
    + GrandchildA()
}
Parent::ChildeB::GrandchildA +.. Parent::ChildeB::GrandchildA::GreatGrandchild

class Parent::ChildeB::GrandchildB  {
    + GrandchildB()
}

class Parent::ChildeB  {
    + ChildeB()
}
Parent::ChildeB +.. Parent::ChildeB::GrandchildA
Parent::ChildeB +.. Parent::ChildeB::GrandchildB

class Parent  {
    + Parent()
}
Parent +.. Parent::ChiledA
Parent +.. Parent::ChildeB

```

</details>

```plantuml
class Parent::ChiledA::GrandchildA::GreatGrandchild  {
    + GreatGrandchild()
}

class Parent::ChiledA::GrandchildB::GreatGrandchild  {
    + GreatGrandchild()
}

class Parent::ChiledA::GrandchildB  {
    + GrandchildB()
}
Parent::ChiledA::GrandchildB +.. Parent::ChiledA::GrandchildB::GreatGrandchild

class Parent::ChiledA::GrandchildA  {
    + GrandchildA()
}
Parent::ChiledA::GrandchildA +.. Parent::ChiledA::GrandchildA::GreatGrandchild

class Parent::ChiledA  {
    + ChiledA()
}
Parent::ChiledA +.. Parent::ChiledA::GrandchildA
Parent::ChiledA +.. Parent::ChiledA::GrandchildB

class Parent::ChildeB::GrandchildA::GreatGrandchild  {
    + GreatGrandchild()
}

class Parent::ChildeB::GrandchildA  {
    + GrandchildA()
}
Parent::ChildeB::GrandchildA +.. Parent::ChildeB::GrandchildA::GreatGrandchild

class Parent::ChildeB::GrandchildB  {
    + GrandchildB()
}

class Parent::ChildeB  {
    + ChildeB()
}
Parent::ChildeB +.. Parent::ChildeB::GrandchildA
Parent::ChildeB +.. Parent::ChildeB::GrandchildB

class Parent  {
    + Parent()
}
Parent +.. Parent::ChiledA
Parent +.. Parent::ChildeB

```

### ファイル参照
各関連を追加すると同時に、`!include`ディレクティブを追加して、関連する相手方の型定義を参照するように設定します。

プロジェクトの設定`PlantUmlGenerator_OutputDir`で同じディレクトリを指定しておけば、別プロジェクトの出力ファイルであっても、フォルダ構成の規則に従って対応するファイルを探索しincludeされます。

例: 

```cs
//Assembly: Consoto.App1.Core
namespace Consoto.App1.Core;
class Parameters
{
}
```

```cs
//Assembly: Consoto.App1
namespace Consoto.App1;
class ClassA
{
    public void Run(Parameter parameters)
    {
        //...
    }
}
```
上記のようなアセンブリと名前空間にクラスの定義がある場合、UMLファイルは下記のフォルダ構成で出力されます。
```plantuml
salt
{
  {T
    + <&folder> <b>OutputDir</b>
      ++ <&folder> <b>Consoto.App1</b> | -- [Assembly Name]
        +++ <&folder> <b>Consoto</b>
          ++++ <&folder> <b>App1</b>
            +++++ <&file> ClassA.puml
      ++ <&folder> <b>Consoto.App1.Core</b> | -- [Assembly Name]
          +++ <&folder> <b>Consoto</b>
            ++++ <&folder> <b>App1</b>
              +++++ <&folder> <b>Core</b>
                ++++++ <&file> Parameters.puml
  }
}
```

`ClassA`のUMLファイルを作る際に`Parameters`との関連がある場合、出力ディレクトリ内の`Parameters.puml`ファイルを探索します。
ファイルが存在する場合に関連と`!include`が追加されます。ファイルのパスは`ClassA.puml`からの相対パスになります。

```
@startuml ClassA
!include ../../../../Consoto.App1.Core/Consoto/App1/Core/Parameters.puml
class ClassA {
    + Run(parameters : Parameters) : void 
}
ClassA ..> Parameters
@enduml
```

## 出力例　

生成される１ファイルの出力例を示します。ファイルは以下のような構成になっています。

- 関連クラスの`!include`
- クラスの定義
- 関連の定義

<details><summary>C#</summary>

```cs
namespace SourceGeneratorTest.Planets.BaseTypes;
public abstract class PlanetBase(string name)
{
    public string Name { get; set; } = name;
    public double Diameter { get; protected set; }
    public double Mass { get; protected set; }
    public double DistanceFromSun { get; protected set; }
    public double OrbitalPeriod { get; protected set; }
    public double SurfaceTemperature { get; protected set; }
    public IList<Moon> Moons { get; } = new List<Moon>();

    protected void AddMoon(Moon moon)
    {
        Moons.Add(moon);
    }
    public abstract Task Orbit();
    public abstract Task Rotate();
}
```

```cs
namespace SourceGeneratorTest.Planets;
public class Earth : PlanetBase
{
    public Earth() : base("Earth")
    {
        Diameter = 12742;
        Mass = 5.972e24;
        DistanceFromSun = 149.6e6;
        OrbitalPeriod = 365.26;
        SurfaceTemperature = 288;
        AddMoon(new Moon("Moon", 3474, 7.35e22, 384400));
    }

    public override async Task Orbit()
    {
        await Task.Delay(5000);
    }

    public override async Task Rotate()
    {
        await Task.Delay(5000);
    }
}
```

```cs
namespace SourceGeneratorTest.Planets;
public sealed class Moon(string name, double diameter, double mass, double distanceFromPlanet)
{
    public string Name { get; } = name;
    public double Diameter { get; } = diameter;
    public double Mass { get; } = mass;
    public double DistanceFromPlanet { get; } = distanceFromPlanet;
}
```

</details>

<details><summary>PlantUML</summary>

```
@startuml PlanetBase
!include ../Moon.puml
abstract class PlanetBase  {
    # PlanetBase(name : string)
    + Name : string <<get>> <<set>>
    + Diameter : double <<get>> <<protected set>>
    + Mass : double <<get>> <<protected set>>
    + DistanceFromSun : double <<get>> <<protected set>>
    + OrbitalPeriod : double <<get>> <<protected set>>
    + SurfaceTemperature : double <<get>> <<protected set>>
    + <<readonly>> Moons : IList<Moon> <<get>>
    # AddMoon(moon : Moon) : void
    + {abstract} Orbit() : Task
    + {abstract} Rotate() : Task
}
PlanetBase *-- "*" Moon : Moons
PlanetBase ..> Moon
@enduml
```
```
@startuml Earth
!include BaseTypes/PlanetBase.puml
class Earth  {
    + Earth()
    + <<override>> <<async>> Orbit() : Task
    + <<override>> <<async>> Rotate() : Task
}
PlanetBase <|-- Earth
@enduml
```

```
@startuml Moon
class Moon <<sealed>>  {
    + Moon(name : string, diameter : double, mass : double, distanceFromPlanet : double)
    + <<readonly>> Name : string <<get>>
    + <<readonly>> Diameter : double <<get>>
    + <<readonly>> Mass : double <<get>>
    + <<readonly>> DistanceFromPlanet : double <<get>>
}
@enduml
```

</details>


```plantuml
abstract class PlanetBase  {
    # PlanetBase(name : string)
    + Name : string <<get>> <<set>>
    + Diameter : double <<get>> <<protected set>>
    + Mass : double <<get>> <<protected set>>
    + DistanceFromSun : double <<get>> <<protected set>>
    + OrbitalPeriod : double <<get>> <<protected set>>
    + SurfaceTemperature : double <<get>> <<protected set>>
    + <<readonly>> Moons : IList<Moon> <<get>>
    # AddMoon(moon : Moon) : void
    + {abstract} Orbit() : Task
    + {abstract} Rotate() : Task
}
PlanetBase *-- "*" Moon : Moons
PlanetBase ..> Moon
class Earth  {
    + Earth()
    + <<override>> <<async>> Orbit() : Task
    + <<override>> <<async>> Rotate() : Task
}
PlanetBase <|-- Earth
class Moon <<sealed>>  {
    + Moon(name : string, diameter : double, mass : double, distanceFromPlanet : double)
    + <<readonly>> Name : string <<get>>
    + <<readonly>> Diameter : double <<get>>
    + <<readonly>> Mass : double <<get>>
    + <<readonly>> DistanceFromPlanet : double <<get>>
}
```

<details><summary>C#</summary>

```cs
namespace SourceGeneratorTest.Classes;
internal class SampleModel
{
    private readonly ILogger logger;
    private readonly IList<StructA> structures;

    public IReadOnlyList<Item> Items { get; } = new List<Item>();

    public SampleModel(ILogger logger, IList<StructA> structures)
    {
        this.logger = logger;
        this.structures = structures;
    }

    public async ValueTask Execute(Parameters parameters)
    {
        await Task.Delay(1000);
    }
}
```

```cs
namespace SourceGeneratorTest.Classes;
internal class Logger : ILogger
{
    public void Write(string message, LogLevel logLevel, Exception? exception = null)
    {
        Console.WriteLine($"[{logLevel}] {message}");
        if (exception != null)
        {
            Console.WriteLine(exception.Message);
        }
    }
    public void WriteDebug(string message) => Write(message, LogLevel.Debug);
    public void WriteInfo(string message) => Write(message, LogLevel.Info);
    public void WriteTrace(string message) => Write(message, LogLevel.Trace);
    public void WriteWarn(string message) => Write(message, LogLevel.Warn);
    public void WriteError(string message, Exception? exception = null) => Write(message, LogLevel.Error, exception);
    public void WriteFatal(string message, Exception? exception = null) => Write(message, LogLevel.Fatal, exception);
}
```

```cs
namespace SourceGeneratorTest.Library.Logs;
public interface ILogger
{
    void Write(string message, LogLevel logLevel, Exception exception);
    void WriteTrace(string message);
    void WriteDebug(string message);
    void WriteInfo(string message);
    void WriteWarn(string message);
    void WriteError(string message, Exception exception);
    void WriteFatal(string message, Exception exception);
}

public enum LogLevel
{
    Trace,
    Debug,
    Info,
    Warn,
    Error,
    Fatal
}
```
```cs
namespace SourceGeneratorTest.Library.Types;
public struct StructA()
{
    public float X { get; set; }
    public float Y { get; set; }
    public float Z { get; set; }
}

public record Item(string Name, double Value);

public record Parameters
{
    public int X { get; }
    public int Y { get; }
    public Parameters(int x, int y) => (X, Y) = (x, y);

    public int Area() => X * Y;
}
```

</details>

<details><summary>PlantUML</summary>

```
@startuml SampleModel
!include ../../../SourceGeneratorTest.Library/SourceGeneratorTest/Library/Logs/ILogger.puml
!include ../../../SourceGeneratorTest.Library/SourceGeneratorTest/Library/Types/StructA.puml
!include ../../../SourceGeneratorTest.Library/SourceGeneratorTest/Library/Types/Item.puml
!include ../../../SourceGeneratorTest.Library/SourceGeneratorTest/Library/Types/Parameters.puml
class SampleModel  {
    - <<readonly>> logger : ILogger
    - <<readonly>> structures : IList<StructA>
    + <<readonly>> Items : IReadOnlyList<Item> <<get>>
    + SampleModel(logger : ILogger, structures : IList<StructA>)
    + <<async>> Execute(parameters : Parameters) : ValueTask
}
SampleModel o-- ILogger : logger
SampleModel o-- "*" StructA : structures
SampleModel *-- "*" Item : Items
SampleModel ..> ILogger
SampleModel ..> Parameters
@enduml
```

```
@startuml Logger
!include ../../../SourceGeneratorTest.Library/SourceGeneratorTest/Library/Logs/ILogger.puml
!include ../../../SourceGeneratorTest.Library/SourceGeneratorTest/Library/Logs/LogLevel.puml
class Logger  {
    + Write(message : string, logLevel : LogLevel, exception : Exception?) : void
    + WriteDebug(message : string) : void
    + WriteInfo(message : string) : void
    + WriteTrace(message : string) : void
    + WriteWarn(message : string) : void
    + WriteError(message : string, exception : Exception?) : void
    + WriteFatal(message : string, exception : Exception?) : void
    + Logger()
}
ILogger <|.. Logger
Logger ..> LogLevel
@enduml
```

```
@startuml ILogger
!include ./LogLevel.puml
interface ILogger  {
    + Write(message : string, logLevel : LogLevel, exception : Exception) : void
    + WriteTrace(message : string) : void
    + WriteDebug(message : string) : void
    + WriteInfo(message : string) : void
    + WriteWarn(message : string) : void
    + WriteError(message : string, exception : Exception) : void
    + WriteFatal(message : string, exception : Exception) : void
}
ILogger ..> LogLevel
@enduml
```

```
@startuml LogLevel
enum LogLevel <<sealed>>  {
    Trace = 0
    Debug = 1
    Info = 2
    Warn = 3
    Error = 4
    Fatal = 5
    + LogLevel()
}
@enduml
```

```
@startuml Item
class Item <<record>>  {
    + Item(Name : string, Value : double)
    # <<readonly>> <<virtual>> EqualityContract : Type <<get>>
    + Name : string <<get>> <<set>>
    + Value : double <<get>> <<set>>
    + <<override>> ToString() : string
    # <<virtual>> PrintMembers(builder : StringBuilder) : bool
    + {static} operator !=(left : Item?, right : Item?) : bool
    + {static} operator ==(left : Item?, right : Item?) : bool
    + <<override>> GetHashCode() : int
    + <<override>> Equals(obj : object?) : bool
    + <<virtual>> Equals(other : Item?) : bool
    # Item(original : Item)
    + Deconstruct(Name : string, Value : double) : void
}
"IEquatable`1" "<Item>" <|.. Item
@enduml
```

```
@startuml StructA
struct StructA <<sealed>>  {
    + StructA()
    + X : float <<get>> <<set>>
    + Y : float <<get>> <<set>>
    + Z : float <<get>> <<set>>
}
@enduml
```

```
@startuml Parameters
class Parameters <<record>>  {
    # <<readonly>> <<virtual>> EqualityContract : Type <<get>>
    + <<readonly>> X : int <<get>>
    + <<readonly>> Y : int <<get>>
    + Parameters(x : int, y : int)
    + Area() : int
    + <<override>> ToString() : string
    # <<virtual>> PrintMembers(builder : StringBuilder) : bool
    + {static} operator !=(left : Parameters?, right : Parameters?) : bool
    + {static} operator ==(left : Parameters?, right : Parameters?) : bool
    + <<override>> GetHashCode() : int
    + <<override>> Equals(obj : object?) : bool
    + <<virtual>> Equals(other : Parameters?) : bool
    # Parameters(original : Parameters)
}
"IEquatable`1" "<Parameters>" <|.. Parameters
@enduml
```



</details>

```plantuml
enum LogLevel <<sealed>>  {
    Trace = 0
    Debug = 1
    Info = 2
    Warn = 3
    Error = 4
    Fatal = 5
    + LogLevel()
}
struct StructA <<sealed>>  {
    + StructA()
    + X : float <<get>> <<set>>
    + Y : float <<get>> <<set>>
    + Z : float <<get>> <<set>>
}
class Item <<record>>  {
    + Item(Name : string, Value : double)
    # <<readonly>> <<virtual>> EqualityContract : Type <<get>>
    + Name : string <<get>> <<set>>
    + Value : double <<get>> <<set>>
    + <<override>> ToString() : string
    # <<virtual>> PrintMembers(builder : StringBuilder) : bool
    + {static} operator !=(left : Item?, right : Item?) : bool
    + {static} operator ==(left : Item?, right : Item?) : bool
    + <<override>> GetHashCode() : int
    + <<override>> Equals(obj : object?) : bool
    + <<virtual>> Equals(other : Item?) : bool
    # Item(original : Item)
    + Deconstruct(Name : string, Value : double) : void
}
"IEquatable`1" "<Item>" <|.. Item

class Parameters <<record>>  {
    # <<readonly>> <<virtual>> EqualityContract : Type <<get>>
    + <<readonly>> X : int <<get>>
    + <<readonly>> Y : int <<get>>
    + Parameters(x : int, y : int)
    + Area() : int
    + <<override>> ToString() : string
    # <<virtual>> PrintMembers(builder : StringBuilder) : bool
    + {static} operator !=(left : Parameters?, right : Parameters?) : bool
    + {static} operator ==(left : Parameters?, right : Parameters?) : bool
    + <<override>> GetHashCode() : int
    + <<override>> Equals(obj : object?) : bool
    + <<virtual>> Equals(other : Parameters?) : bool
    # Parameters(original : Parameters)
}
"IEquatable`1" "<Parameters>" <|.. Parameters
interface ILogger  {
    + Write(message : string, logLevel : LogLevel, exception : Exception) : void
    + WriteTrace(message : string) : void
    + WriteDebug(message : string) : void
    + WriteInfo(message : string) : void
    + WriteWarn(message : string) : void
    + WriteError(message : string, exception : Exception) : void
    + WriteFatal(message : string, exception : Exception) : void
}
ILogger ..> LogLevel
class Logger  {
    + Write(message : string, logLevel : LogLevel, exception : Exception?) : void
    + WriteDebug(message : string) : void
    + WriteInfo(message : string) : void
    + WriteTrace(message : string) : void
    + WriteWarn(message : string) : void
    + WriteError(message : string, exception : Exception?) : void
    + WriteFatal(message : string, exception : Exception?) : void
    + Logger()
}
ILogger <|.. Logger
Logger ..> LogLevel
class SampleModel  {
    - <<readonly>> logger : ILogger
    - <<readonly>> structures : IList<StructA>
    + <<readonly>> Items : IReadOnlyList<Item> <<get>>
    + SampleModel(logger : ILogger, structures : IList<StructA>)
    + <<async>> Execute(parameters : Parameters) : ValueTask
}
SampleModel o-- ILogger : logger
SampleModel o-- "*" StructA : structures
SampleModel *-- "*" Item : Items
SampleModel ..> ILogger
SampleModel ..> Parameters
```