还在手画C#依赖关系图吗?快来试试这个工具吧!
笔者最近见到了一个不错的工具,可以让大家在看代码的时候一键生成C#依赖的类图。非常适合编写文档、查看和学习开源项目设计时使用,比如下方就是笔者通过这个工具生成的Microsoft.Extensions.ObjectPool
依赖图,可以非常清晰明了的告诉我们类与类之间的关系。
GITHUB地址:
https://github.com/pierre3/PlantUmlClassDiagramGenerator
image-20221107232048503
这是一个生成器,用于从C#源代码中创建PlantUML的类图。
image-20221107232754756
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]
例子:
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环境。
image-20221107232315656
打开以后按Alt+D
,或者右击 -> 预览光标位置图表就可以了,当然右击也可以导出图片。
image-20221107232533751
下文是puml-gen工具对PlantUML的一些转换规则,大家有兴趣的可以了解一下。
class
abstract
class ClassA {
}
struct StructA {
}
interface InterfaceA {
}
record RecordA {
}
abstract class AbstractClass {
}
static class StaticClass {
}
sealed partial class ClassB{
}
enum EnumType{
Apple,
Orange,
Grape
}
@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
TypeDeclaration.png
class GenericsType<T1>{
}
class GenericsType<T1,T2>{
}
class "GenericsType`1"<T1>{
}
class "GenericsType`2"<T1,T2>{
}
GenericsTypeDeclaration.png
+
{abstract}
Prop : int <<get>> <<set>>
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";
}
}
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
MemberDeclaration.png
只有常量的初始化才会被输出。
class ClassC
{
private int fieldA = 123;
public double Pi {get;} = 3.14159;
protected List<string> Items = new List<string>();
}
class ClassC {
- fieldA : int = 123
+ Pi : double = 3.14159
# Items : List<string>
}
Initializer.png
嵌套类被展开并与 "OuterClass + - InnerClass "关联。
class OuterClass
{
class InnerClass
{
struct InnerStruct
{
}
}
}
class OuterClass{
}
class InnerClass{
}
<<struct>> class InnerStruct {
}
OuterClass +- InnerClass
InnerClass +- InnerStruct
NestedClass.png
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; }
}
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
InheritanceRelationsips.png
如果你指定了 "createAssociation "选项,对象关联将从字段和属性引用中创建。
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;}
}
@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
InheritanceRelationsips.png
C# 9中的记录类型可以有一个参数列表。在这些情况下,这些参数 被作为属性添加到类中。
record Person(string Name, int Age);
record Group(string GroupName) {
public Person[] Members { get; init; }
}
@startuml
class Person <<record>> {
+ Name : string <<get>> <<init>>
+ Age : int <<get>> <<init>>
}
class Group <<record>> {
+ GroupName : string <<get>> <<init>>
+ Members : Person[] <<get>> <<init>>
}
@enduml
InheritanceRelationsips.png
你可以将PlantUmlClassDiagramGenerator.Attributes[3]包添加到你的C#项目中,用于基于特性的配置。
只有被添加了PlantUmlDiagramAttribute的类型才会被输出。如果-attributeRequired开关被添加到命令行参数中,这个属性就会被启用。
这个属性只能被添加到类型声明中。
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
添加了这个属性的元素被排除在输出之外。
[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
通过添加这个属性,你可以定义类之间的关联。这个属性可以被添加到属性、字段和方法参数。
关联的细节被定义在以下属性中。
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
CustomAssociation.png
这个属性可以被添加到属性和字段中。具有此属性的属性(或字段)被描述为类的成员,没有任何关联。
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
IgnoreAssociation.png
[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