PlantUmlClassDiagramGenerator.SourceGenerator 0.5.1-beta
PlantUmlClassDiagramGenerator.SourceGenerator がベータ版(0.5.1-beta) になりました。
主に、出力対象とするクラスやメンバーを制御する設定の追加や、ファイル出力時のフォルダ構成の見直しなどを行いました。
ソースコード
サンプル
.NET Architectureのサンプルリポジトリ"eShopOnWeb" のソースコードをまるっとクラス図にしてみました。
PlantUMLのコードはこちらにまとめて出力しています。
https://github.com/pierre3/eShopOnWeb/tree/main/generated-uml
レンダリングしたクラス図はこちらで確認できます。
https://github.com/pierre3/eShopOnWeb/tree/main/generated-uml/ClassDiagram
アルファ版 (0.1.8-alpha) からの変更点
前回の記事 からの変更点は以下の通りです。
- プロジェクト設定の追加
- PlantUmlDiagramAttributeにプロパティを追加
- PlantUmlIgnoreAttributeを追加
- ファイル出力先のフォルダ構成変更
- ネストした型の表現
- プロパティ、フィールドの関連において、初期化子またはコンストラクタ内で初期化を行っている場合はCompositionとするように変更
使い方
1. NuGet パッケージのインストール
PlantUmlClassDiagramGenerator.SourceGenerator パッケージを NuGet Gallery から取得し、.NET プロジェクトにインストールします。
2. プロジェクトファイルの編集
2.1 条件付きコンパイルシンボルに "GENERATE_PLANTUML" を含める
このツールは、プリプロセッサシンボルに "GENERATE_PLANTUML" が定義されている場合にのみ動作します。ツールはコーディング中に常に動作する必要がなく、必要になったタイミングで 1 度だけ実行すれば十分です。そのため、特定のビルド構成時のみ動作する仕組みとしています。
プロジェクトのビルド構成の条件付きコンパイルシンボルに "GENERATE_PLANTUML" を追加します。
リリースビルド時にツールを実行するように設定するには、.csproj ファイルに以下のセクションを追加します。
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<DefineConstants>$(DefineConstants);GENERATE_PLANTUML</DefineConstants>
</PropertyGroup>
2.2 プロジェクト設定の追加
プロジェクトファイル(.csproj)に下記セクションを追加します。
<ItemGroup>
<CompilerVisibleProperty Include="PlantUmlGenerator_OutputDir" />
<CompilerVisibleProperty Include="PlantUmlGenerator_AttributeRequierd" />
<CompilerVisibleProperty Include="PlantUmlGenerator_IncludeMemberAccessibilities" />
<CompilerVisibleProperty Include="PlantUmlGenerator_ExcludeMemberAccessibilities" />
</ItemGroup>
<PropertyGroup>
<PlantUmlGenerator_OutputDir>$(ProjectDir)generated-uml</PlantUmlGenerator_OutputDir>
<PlantUmlGenerator_AttributeRequierd>false</PlantUmlGenerator_AttributeRequierd>
<PlantUmlGenerator_IncludeMemberAccessibilities>All</PlantUmlGenerator_IncludeMemberAccessibilities>
<PlantUmlGenerator_ExcludeMemberAccessibilities>None</PlantUmlGenerator_ExcludeMemberAccessibilities>
</PropertyGroup>
プロパティ | 説明 | 既定値 |
---|---|---|
PlantUmlGenerator_OutputDir | 生成したUMLファイルの配置先ディレクトリを指定します。 異なるプロジェクト間で関連付けを行いたい場合は、関連する全てのプロジェクトで同じディレクトリを指定してください。 |
$(ProjectDir)generated_uml |
PlantUmlGenerator_AttributeRequierd | PlantUmlDiagramAttributeを付けた型のみを出力対象とする場合はtrueを、プロジェクト内で定義されている全ての型を出力の対象とする場合はfalseを指定します。 | true |
PlantUmlGenerator_IncludeMemberAccessibilities | 出力対象に含めるメンバーのアクセシビリティを指定します。設定可能な値は下記の通り。 ・None ・Public ・Protected ・Internal ・ProtectedInternal ・PrivateProtected ・Private ・All カンマ区切りで複数の値を指定できます。 例: Public,Protected |
All |
PlantUmlGenerator_ExcludeMemberAccessibilities | 出力対象外とするメンバーのアクセシビリティを指定します。指定の方法はPlantUmlGenerator_IncludeMemberAccessibilitiesと同様です。 | None |
3. 属性値の設定
必要に応じて、型やそのメンバーに対して属性値を設定します。
PlantUmlDiagramAttribute
プロジェクトの設定PlantUmlGenerator_AttributeRequierd
をtrueとした場合、出力対象としたい型にはこの属性を付加する必要があります。
[PlantUmlDiagram]
class ClassA
{
public int X { get;set;}
//....
}
また、IncludeMemberAccessibilities
プロパティを設定することで表示対象とするメンバーのアクセシビリティを指定することができます。このプロパティはプロジェクトの設定 (PlantUmlGenerator_IncludeMemberAccessibilities
)よりも優先されます。
[PlantUmlDiagram(IncludeMemberAccessibilities = Accessibilities.Public| Accessibilities.Protected)]
class ClassA
{
private int n = 0;
public int X { get;set;}
protected string A { get;set; }
}
逆にIncludeMemberAccessibilities
プロパティを使って表示対象外とするメンバーを指定することもできます。
[PlantUmlDiagram(ExcludeMemberAccessibilities = Accessibilities.Private)]
class ClassA
{
private int n = 0;
public int X { get;set;}
protected string A { get;set; }
}
PlantUmlIgnoreAttribute
プロジェクトの設定PlantUmlGenerator_AttributeRequierdをfalseとした場合、プロジェクト内で定義されている全ての型が出力の対象となります。
一部の型を出力の対象外としたい場合は、その型にPlantUmlIgnoreAttribute
を設定します。
[PlantUmlIgnore]
class ClassA
{
}
class ClassB
{
}
PlantUmlIgnoreAttribute
はある型の特定のメンバーのみを非表示としたい場合にも利用できます。
class ClassA
{
public int X {get;set;}
[PlantUmlIgnore]
public int Y {get;set;}
public void MethodA(){}
[PlantUmlIgnore]
public void MethodB(){}
}
仕様
UMLファイルの出力
PlantUmlGenerator_OutputDir
で指定した出力先ディレクトリ配下に「アセンブリ名」のフォルダ、その下に「名前空間」のフォルダが作成されます。
例:
以下のプロジェクト構成のソリューションがあるとします。
プロジェクト | アセンブリ名 | 名前空間 |
---|---|---|
Consoto.App1.csproj | Consoto.App1 | ・Consoto.App1 ・Consoto.App1.ViewModel ・Consoto.App1.Model |
Consoto.App1.Core.csproj | Consoto.App1.Core | ・Consoto.App1.Core ・Consoto.App1.Core.Extensions |
各プロジェクト毎にPlantUmlClassDiagramGenerator.SourceGenerator
のパッケージをインストールし、PlantUmlGenerator_OutputDir
を$(SolutionDir)generated_uml
とした場合、下図のようなフォルダ構成でUMLファイルが出力されます。
型の表現
PlantUMLで利用できる型のキーワードは以下の通りです。
- class
- struct
- interface
- abstract class
- enum
record
, static
,sealed
などの修飾子はステレオタイプ(<<keyword>>
)で表現します。
C#
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
}
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 (<|--)
の関連を追加します。
public abstract class PlanetBase
{
}
public class Earth : PlanetBase
{
}
abstract class PlanetBase {
}
class Earth {
}
PlanetBase <|-- Earth
インターフェイスの実装
対象の型がインターフェイスを実装している場合、Realization (<|..)
の関連を追加します。
interface ILogger
{
}
class Logger : ILogger
{
}
interface ILogger {
}
class Logger {
}
ILogger <|.. Logger
プロパティまたはフィールドの関連
プロパティ、フィールドの型が出力対象の型の場合、Aggrigation (o--)
またはComposition (*--)
の関連を追加します。
次の条件を満たす場合にComposition
、それ以外はAggrigation
となります。
- プロパティやフィールドの初期化子で初期化している
- コンストラクタ内で初期化している
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
配列型またはIEnumerable<T>
を実装している型の場合
プロパティやフィールドの型との関連ではなく、要素の型との関連を追加します。その際、要素の型側に多重度"*"
を付与します。
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
メソッドのパラメータ
メソッドのパラメータの型が出力対象の型の場合に Dependency (..>)
の関連を追加します。
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})");
}
}
ネストした型
メンバーに型の定義が含まれる場合 Nested (+--)
の関連を追加します。
ネストした型の名前は {親の型名}::{型名}
の形式で付けられます。
C#
class Parent
{
class ChiledA
{
class GrandchildA
{
class GreatGrandchild
{
}
}
class GrandchildB
{
class GreatGrandchild
{
}
}
}
class ChildeB
{
class GrandchildA
{
class GreatGrandchild
{
}
}
class GrandchildB
{
}
}
}
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されます。
例:
//Assembly: Consoto.App1.Core
namespace Consoto.App1.Core;
class Parameters
{
}
//Assembly: Consoto.App1
namespace Consoto.App1;
class ClassA
{
public void Run(Parameter parameters)
{
//...
}
}
上記のようなアセンブリと名前空間にクラスの定義がある場合、UMLファイルは下記のフォルダ構成で出力されます。
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
出力例
生成される1ファイルの出力例を示します。ファイルは以下のような構成になっています。
- 関連クラスの
!include
- クラスの定義
- 関連の定義
C#
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();
}
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);
}
}
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;
}
PlantUML
@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
C#
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);
}
}
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);
}
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
}
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;
}
PlantUML
@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