还在手画C#依赖关系图吗?快来试试这个工具吧!

还在手画C#依赖关系图吗?快来试试这个工具吧!

笔者最近见到了一个不错的工具,可以让大家在看代码的时候一键生成C#依赖的类图。非常适合编写文档、查看和学习开源项目设计时使用,比如下方就是笔者通过这个工具生成的Microsoft.Extensions.ObjectPool依赖图,可以非常清晰明了的告诉我们类与类之间的关系。

GITHUB地址:

https://github.com/pierre3/PlantUmlClassDiagramGenerator

4a1e4aef0f1aec0c1867f6962386bf6f.png
image-20221107232048503

介绍PlantUmlClassDiagramGenerator

这是一个生成器,用于从C#源代码中创建PlantUML的类图。

Visual Studio Code 扩展

  • C# to PlantUML[1]

aaa32791c4d573592d6e938ecf3e206c.png
image-20221107232754756

.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环境。

8bfd2d3b8591518c11b93edb5e237911.png
image-20221107232315656

打开以后按Alt+D,或者右击 -> 预览光标位置图表就可以了,当然右击也可以导出图片。

1b63cc897e01fe2347c8b0e6cee5aa82.png
image-20221107232533751

下文是puml-gen工具对PlantUML的一些转换规则,大家有兴趣的可以了解一下。

转换为PlantUML的规范

类型声明

Type 关键字
C#PlantUML
classclass
struct<<struct>> class
interfaceinterface
enumenum
record<<record>> class
Type 修饰符
C#PlantUML
abstractabstract
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
e2802776fa57242512fcb3e69346da53.png
TypeDeclaration.png
泛型
  • C#

class GenericsType<T1>{
}
class GenericsType<T1,T2>{
}
  • PlantUML

class "GenericsType`1"<T1>{
}
class "GenericsType`2"<T1,T2>{
}
6bc0fefe426731349ec4f4bdcc008545.png
GenericsTypeDeclaration.png

成员声明

可见性修饰符
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
14edb26d3e34072fe482fe4f4b4d67f1.png
MemberDeclaration.png
字段和属性初始化

只有常量的初始化才会被输出。

  • 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>
}
0db9704b49fb90ae862f7248bed6bde2.png
Initializer.png

嵌套类声明

嵌套类被展开并与 "OuterClass + - InnerClass "关联。

  • C#

class OuterClass 
{
  class InnerClass 
  {
    struct InnerStruct 
    {

    }
  }
}
  • PlantUML

class OuterClass{

}
class InnerClass{

}
<<struct>> class InnerStruct {

}
OuterClass +- InnerClass
InnerClass +- InnerStruct
190785d8a81c9b408ab03df603c34692.png
NestedClass.png

继承关系

  • 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
825b269e0801e7b50b3e20ed3464fb64.png
InheritanceRelationsips.png

关联(来自字段和属性的引用)

如果你指定了 "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
0934fbff2ce0aad53dfeb02aa590d728.png
InheritanceRelationsips.png

记录类型(含参数列表)

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
58894db4c2b18d5c311cc0ea88796b7f.png
InheritanceRelationsips.png

基于特性的配置

你可以将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
2690228fb64179c97c9ef0ed65e09797.png
CustomAssociation.png

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
36b96e2920954fdbcd465cc7eb31601f.png
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

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值