還在手畫C#依賴關系圖嗎?快來試試這個工具吧!
筆者最近見到了一個不錯的工具,可以讓大家在看代碼的時候一鍵生成C#依賴的類圖。非常適合編寫文檔、查看和學習開源項目設計時使用,比如下方就是筆者通過這個工具生成的Microsoft.Extensions.ObjectPool
依賴圖,可以非常清晰明了的告訴我們類與類之間的關系。
GITHUB地址:
https://github.com/pierre3/PlantUmlClassDiagramGenerator

介紹PlantUmlClassDiagramGenerator
這是一個生成器,用于從C#源代碼中創建PlantUML的類圖。
Visual Studio Code 擴展
C# to PlantUML[1]

.Net Core 全局工具
Nuget Gallery: https://www.nuget.org/packages/PlantUmlClassDiagramGenerator
安裝
下載并安裝.NET 6.0 SDK[2]或更新的版本。安裝后,運行以下命令。
dotnet tool install --global PlantUmlClassDiagramGenerator
使用
運行 "puml-gen" 命令.
puml-gen InputPath [OutputPath] [-dir] [-public | -ignore IgnoreAccessibilities] [-excludePaths ExcludePathList] [-createAssociation]
InputPath: (必須) 設置一個輸入源文件或目錄名稱。
OutputPath: (可選) 設置一個輸出文件或目錄名稱。如果省略此選項,plantuml文件將被輸出到與輸入文件相同的目錄中。
-dir: (可選) 當InputPath和OutputPath為目錄名時,指定。
-public: (可選) ?如果指定,只輸出公共可及性成員。
-ignore: (可選) 指定要忽略的成員的可訪問性,用逗號分隔的列表。
-excludePaths: (可選) 指定排除的文件和目錄。
指定來自 "InputPath "的相對路徑,用逗號分隔的列表。-createAssociation: (可選) 從字段和屬性的引用中創建對象關聯。
-allInOne: (可選) 只有當-dir被設置時:將所有圖表的輸出復制到文件include.puml(這允許PlanUMLServer渲染)。
-attributeRequired: (可選) 當這個開關被啟用時,只有類型聲明中帶有 "PlantUmlDiagramAttribute "的類型會被輸出。
例子:
puml-gen C:\Source\App1\ClassA.cs -public
puml-gen C:\Source\App1 C:\PlantUml\App1 -dir -ignore Private,Protected -createAssociation -allInOne
puml-gen C:\Source\App1 C:\PlantUml\App1 -dir -excludePaths bin,obj,Properties
生成好以后,會在對應的目錄看到有一些*.puml
的文件,然后可以用對應的PlantUML工具打開。
筆者這里使用的是Visual Studio Code打開的PlantUML圖,需要安裝一個插件,可能某些電腦需要安裝Java環境。

打開以后按Alt+D
,或者右擊 -> 預覽光標位置圖表就可以了,當然右擊也可以導出圖片。

下文是puml-gen工具對PlantUML的一些轉換規則,大家有興趣的可以了解一下。
轉換為PlantUML的規范
類型聲明
Type 關鍵字
C# | PlantUML |
---|---|
class | class |
struct | <<struct>> class |
interface | interface |
enum | enum |
record | <<record>> class |
Type 修飾符
C# | PlantUML |
---|---|
abstract | abstract |
static | <<static>> |
partial | <<partial>> |
sealed | <<sealed>> |
C#
class?ClassA?{??
}
struct?StructA?{
}
interface?InterfaceA?{
}
record?RecordA?{
}
abstract?class?AbstractClass?{
}
static?class?StaticClass?{
}
sealed?partial?class?ClassB{
}
enum?EnumType{Apple,Orange,Grape
}
PlantUML
@startuml
class ClassA {
}
class StructA <<struct>> {
}
interface InterfaceA {
}
class RecordA <<record>> {
}
abstract class AbstractClass {
}
class StaticClass <<static>> {
}
class ClassB <<sealed>> <<partial>> {
}
enum EnumType {Apple,Orange,Grape,
}
@enduml

泛型
C#
class?GenericsType<T1>{
}
class?GenericsType<T1,T2>{
}
PlantUML
class "GenericsType`1"<T1>{
}
class "GenericsType`2"<T1,T2>{
}

成員聲明
可見性修飾符
C# | PlantUML |
---|---|
public | + |
internal | <<internal>> |
protected internal | # <<internal>> |
protected | # |
private | - |
修飾符
C# | PlantUML |
---|---|
abstract | {abstract} |
static | {static} |
virtual | <<virtual>> |
override | <<override>> |
new | <<new>> |
readonly | <<readonly>> |
event | <<event>> |
屬性訪問器
C# | PlantUML |
---|---|
int Prop {get; set;} | Prop : int <<get>> <<set>> |
int Prop {get;} | Prop : int <get> |
int Prop {get; private set } | Prop : int <<get>><<private set>> |
int Prop => 100; | Prop : int <<get>> |
C#
abstract?class?AbstractClass
{protected?int?_x;internal?int?_y;protected?internal?int?_z;public?abstract?void?AbstractMethod();protected?virtual?void?VirtualMethod(string?s){}public?string?BaseMethod(int?n){return?"";}
}
class?ClassM?:?AbstractClass
{public?static?readonly?double?PI?=3.141592;public?int?PropA?{?get;?set;?}public?int?PropB?{?get;?protected?set;?}public?event?EventHandler?SomeEvent;public?override?void?AbstractMethod(){}protected?override?void?VirtualMethod(string?s){}public?override?string?ToString(){return?"override";}public?new?string?BaseMethod(int?n){return?"new";}
}
PlantUML
abstract class AbstractClass {# _x : int<<internal>> _y : int# <<internal>> _z : int+ {abstract} AbstractMethod() : void# <<virtual>> VirtualMethod(s:string) : void+ BaseMethod(n:int) : string
}
class ClassM {+ {static} <<readonly>> PI : double = 3.141592+ PropA : int <<get>> <<set>>+ PropB : int <<get>> <<protected set>>+ <<event>> SomeEvent : EventHandler + <<override>> AbstractMethod() : void# <<override>> VirtualMethod(s:string) : void+ <<override>> ToString() : string+ <<new>> BaseMethod(n:int) : string
}
AbstractClass <|-- ClassM

字段和屬性初始化
只有常量的初始化才會被輸出。
C#
class?ClassC
{private?int?fieldA?=?123;public?double?Pi?{get;}?=?3.14159;protected?List<string>?Items?=?new?List<string>();?
}
PlantUML
class ClassC {- fieldA : int = 123+ Pi : double = 3.14159# Items : List<string>
}

嵌套類聲明
嵌套類被展開并與 "OuterClass + - InnerClass "關聯。
C#
class?OuterClass?
{class?InnerClass?{struct?InnerStruct?{}}
}
PlantUML
class OuterClass{}
class InnerClass{}
<<struct>> class InnerStruct {}
OuterClass +- InnerClass
InnerClass +- InnerStruct

繼承關系
C#
abstract?class?BaseClass
{public?abstract?void?AbstractMethod();protected?virtual?int?VirtualMethod(string?s)?=>?0;
}
class?SubClass?:?BaseClass
{public?override?void?AbstractMethod()?{?}protected?override?int?VirtualMethod(string?s)?=>?1;
}interface?IInterfaceA?{}
interface?IInterfaceA<T>:IInterfaceA
{T?Value?{?get;?}
}
class?ImplementClass?:?IInterfaceA<int>
{public?int?Value?{?get;?}
}
PlantUML
abstract class BaseClass {+ {abstract} AbstractMethod() : void# <<virtual>> VirtualMethod(s:string) : int
}
class SubClass {+ <<override>> AbstractMethod() : void# <<override>> VirtualMethod(s:string) : int
}
interface IInterfaceA {
}
interface "IInterfaceA`1"<T> {Value : T <<get>>
}
class ImplementClass {+ Value : int <<get>>
}
BaseClass <|-- SubClass
IInterfaceA <|-- "IInterfaceA`1"
"IInterfaceA`1" "<int>" <|-- ImplementClass

關聯(來自字段和屬性的引用)
如果你指定了 "createAssociation "選項,對象關聯將從字段和屬性引用中創建。
C#
class?ClassA{public?IList<string>?Strings{get;}?=?new?List<string>();public?Type1?Prop1{get;set;}public?Type2?field1;
}class?Type1?{public?int?value1{get;set;}
}class?Type2{public?string?string1{get;set;}public?ExternalType?Prop2?{get;set;}
}
PlantUML
@startuml
class ClassA {
}
class Type1 {+ value1 : int <<get>> <<set>>
}
class Type2 {+ string1 : string <<get>> <<set>>
}
class "IList`1"<T> {
}
ClassA o-> "Strings<string>" "IList`1"
ClassA --> "Prop1" Type1
ClassA --> "field1" Type2
Type2 --> "Prop2" ExternalType
@enduml

記錄類型(含參數列表)
C# 9中的記錄類型可以有一個參數列表。在這些情況下,這些參數 被作為屬性添加到類中。
C#
record?Person(string?Name,?int?Age);record?Group(string?GroupName)?{public?Person[]?Members?{?get;?init;?}
}
PlantUML
@startuml
class Person <<record>> {+ Name : string <<get>> <<init>>+ Age : int <<get>> <<init>>
}
class Group <<record>> {+ GroupName : string <<get>> <<init>>+ Members : Person[] <<get>> <<init>>
}
@enduml

基于特性的配置
你可以將PlantUmlClassDiagramGenerator.Attributes[3]包添加到你的C#項目中,用于基于特性的配置。
PlantUmlDiagramAttribute
只有被添加了PlantUmlDiagramAttribute的類型才會被輸出。如果-attributeRequired開關被添加到命令行參數中,這個屬性就會被啟用。
這個屬性只能被添加到類型聲明中。
class
struct
enum
record
class?ClassA
{public?string?Name?{?get;?set;?}public?int?Age?{?get;?set;?}
}[PlantUmlDiagram]
class?ClassB
{public?string?Name?{?get;?set;?}public?int?Age?{?get;?set;?}
}
只有帶有PlantUmlDiagramAttribute的ClassB會被輸出。
@startuml
class ClassB {+ Name : string <<get>> <<set>>+ Age : int <<get>> <<set>>
}
@enduml
PlantUmlIgnoreAttribute
添加了這個屬性的元素被排除在輸出之外。
[PlantUmlIgnore]
class?ClassA
{public?string?Name?{?get;?set;?}public?int?Age?{?get;?set;?}
}class?ClassB
{public?string?Name?{?get;?set;?}[PlantUmlIgnore]public?int?Age?{?get;?set;?}
}class?ClassC
{public?string?Name?{?get;?set;?}public?int?Age?{?get;?set;?}[PlantUmlIgnore]public?ClassC(string?name,?int?age)?=>?(Name,?Age)?=?(name,?age);public?void?MethodA();[PlantUmlIgnore]public?void?MethodB();
}
@startuml
class ClassB {+ Name : string
}
class ClassC {+ Name : string+ Age : int+ MethodA() : void
}
@enduml
PlantUmlAssociationAttribute
通過添加這個屬性,你可以定義類之間的關聯。這個屬性可以被添加到屬性、字段和方法參數。
關聯的細節被定義在以下屬性中。
Name
指定葉子節點一側的類型名稱。
如果省略,則使用添加該屬性的元素的名稱。
Association
指定關聯的邊緣部分。在PlantUML中設置一個有效的字符串。
如果省略,則使用"--"。
RootLabel
指定顯示在根節點一側的標簽。
如果省略,則不顯示。
Label
指定要顯示在邊緣中心的標簽。
如果省略,則不顯示。
LeafLabel
指定顯示在葉子節點一側的標簽。
如果省略,則不顯示。
class?Parameters
{public?string?A?{?get;?set;?}public?string?B?{?get;?set;?}
}class?CustomAssociationSample
{[PlantUmlAssociation(Name?=?"Name",?Association?=?"*-->",?LeafLabel?=?"LeafLabel",?Label=?"Label",?RootLabel?=?"RootLabel")]?public?ClassA?A?{?get;?set;?}
}class?CollectionItemsSample
{[PlantUmlAssociation(Name?=?"Item",?Association?=?"o--",?LeafLabel?=?"0..*",?Label?=?"Items")]public?IList<Item>?Items?{?get;?set;?}
}class?MethodParamtersSample
{public?void?Run([PlantUmlAssociation(Association?=?"..>",?Label?=?"use")]?Parameters?p){Console.WriteLine($"{p.A},{p.B}");}private?ILogger?logger;public?MyClass([PlantUmlAssociation(Association?=?"..>",?Label?=?"Injection")]?ILogger?logger){this.logger?=?logger;}
}
@startuml
class Parameters {+ A : string <<get>> <<set>>+ B : string <<get>> <<set>>
}
class CustomAssociationSample {
}
class CollectionItemsSample {
}
class MethodParamtersSample {+ Run(p:Parameters) : void+ MyClass(logger:ILogger)
}
CustomAssociationSample "RootLabel" *--> "LeafLabel" Name : "Label"
CollectionItemsSample o-- "0..*" Item : "Items"
MethodParamtersSample ..> Parameters : "use"
MethodParamtersSample ..> ILogger : "Injection"
@enduml

PlantUmlIgnoreAssociationAttribute
這個屬性可以被添加到屬性和字段中。具有此屬性的屬性(或字段)被描述為類的成員,沒有任何關聯。
class?User
{public?string?Name?{?get;?set;?}public?int?Age?{?get;?set;?}
}class?ClassA
{public?static?User?DefaultUser?{?get;?}public?IList<User>?Users?{?get;?}public?ClassA(IList<User>?users){Users?=?users;DefaultUser?=?new?User(){Name?=?"DefaultUser",Age?=?"20"};}
}class?ClassB
{[PlantUmlIgnoreAssociation]public?static?User?DefaultUser?{?get;?}[PlantUmlIgnoreAssociation]public?IList<User>?Users?{?get;?}public?ClassB(IList<User>?users){Users?=?users;DefaultUser?=?new?User(){Name?=?"DefaultUser",Age?=?"20"};}
}
@startuml
class User {+ Name : string <<get>> <<set>>+ Age : int <<get>> <<set>>
}
class ClassA {+ ClassA(users:IList<User>)
}
class ClassB {+ {static} DefaultUser : User <<get>>+ Users : IList<User> <<get>>+ ClassB(users:IList<User>)
}
class "IList`1"<T> {
}
ClassA --> "DefaultUser" User
ClassA --> "Users<User>" "IList`1"
@enduml

參考資料
[1]
C# to PlantUML: https://marketplace.visualstudio.com/items?itemName=pierre3.csharp-to-plantuml
[2].NET 6.0 SDK: https://www.microsoft.com/net/download/windows
[3]PlantUmlClassDiagramGenerator.Attributes: https://www.nuget.org/packages/PlantUmlClassDiagramGenerator.Attributes