C4Sharp: Uma poderosa biblioteca para construir diagramas C4 Model

Allan Santos
cajudevs
Published in
11 min readMar 5, 2024

No artigo anterior — Entendendo o C4 Model: Uma Abordagem para Arquitetura de Software — foi apresentado o C4 Model, uma documentação para arquitetura de software visual e intuitiva. Foi explorado o histórico, conceitos, características, casos de uso e até ferramentas.

Durante o levantamento para escrita daquele artigo, uma das ferramentas encontradas foi a C4 Sharp: biblioteca criada pelo brasileiro Yan Justino para criar os diagramas através da linguagem de programação C# (Csharp). O projeto de Yan foi tão bem feito e deu tão certo que o idealizador do C4 Model, Simon Brown, a listou como uma das ferramentas para utilização no site oficial.

Eu gostei bastante e decidi criar a parte 2 para o artigo e estudar com mais afinco o C4Sharp. Desta forma, poderei explanar de forma mais detalhada como utilizá-la e criar exemplos para os principais níveis do modelo: Contexto, Contêiner e Componente.

Portanto, este artigo tem como objetivo principal buscar entender as principais classes, suas relações e usar na prática o C4 Sharp. Além disso, assim como no artigo anterior, serve para aplicar o conhecimento e ficarei grato por qualquer feedback fornecido.

O que é C4Sharp?

C4Sharp (C4S) é uma biblioteca (lib ou library) .NET para criar diagramas de código, baseada no Modelo C4. Está disponível no GitHub e funciona como uma extensão do C4-PlantUML. Além de conter as funcionalidades da PlantUML, inclui novos recursos e possibilidades. A lib utiliza C# e permite criar diagramas como código (diagram as code) abrangendo os seguintes diagramas: Contexto, Contêiner, Componente. Além deles a biblioteca contém o diagrama de Sequência, Deployment e Enterprise.

O diagrama de código, como a própria documentação menciona não é obrigatório e normalmente utiliza um diagrama UML para exemplificar classes, relacionamento de entidades, interfaces, objetos, funções, entre outros. Para isso é possível outras ferramentas (até a própria IDE) para documentar os detalhes de codificação. E para finalizar, a biblioteca exporta os diagramas como PNG, SVG, PUML e MERMAID.

Utilizando o C4Sharp

No GitHub da ferramenta informa que para utilizá-la é necessário ter tanto o .NET 5 (no mínimo) quanto o Java instalado. Além disso, é preciso adicionar a referência ao pacote C4Sharp no projeto que está disponível no Nuget Packages.

Conhecendo as principais classes

Para começar a falar das classes da lib, é interessante conhecer a classe Structure. Essa classe é utilizada para representar elementos estruturais do sistema de software na visão do C4 Model. Ela define as propriedades comuns a todos os elementos da especificação C4, possui propriedades como nome, descrição, tags do elemento e Boundary, que indica se é um elemento interno ou externo. Na classe Structure também contém os operators e Relationship que cria um relacionamento entre eles. Abaixo seguem os principais exemplos de classes que herdam de Structure:

  • SoftwareSystem: como o próprio nome já diz, representa um software.
  • Person: representa uma pessoa, ator, consumidor, entre outros.
  • Container: referencia uma base de dados, aplicação, por exemplo. É algo que precisa está funcionando corretamente para o funcionamento correto do software como um todo.
  • Component: a ideia é como o próprio modelo define, é a funcionalidade encapsulada no container. A classe permite identificar além da descrição, a tecnologia e o tipo de componente (database, queue, etc) se assim desejar.

A construção de um diagrama com a biblioteca C4Sharp envolve identificar as estruturas e suas relações através de uma classe que herda propriedades diretamente de DiagramBuildRunner. Vamos ver abaixo como construir na prática.

Exemplos práticos

Observação 1: Para construir os exemplos, criarei com o C4Sharp exatamente os diagramas propostos como exemplo no artigo de Mohan Raj. Segundo o autor, o exemplo representa um provedor de energia que permite os clientes utilizar o sistema para enviar as leituras de gás e energia, receber notificações do provedor sobre o envio bem sucedido, entre outras funcionalidades.

Observação 2: Criei um sistema Console para executar o código.

Diagrama de Contexto

Para iniciar a criação do Diagrama de Contexto, criei primeiro as Structures:

  • Classe People:
using C4Sharp.Elements;

namespace C4ModelDiagramsArticleExamples.Structures;

public static class People
{
private static Person? _customer;

public static Person Customer => _customer ??= new Person("customer", "Personal Customer")
{
Description = "A EG customer with valid account."
};
}
  • Classe Systems:
using C4Sharp.Elements;
using C4Sharp.Elements.Relationships;

namespace C4ModelDiagramsArticleExamples.Structures;

public static class Systems
{
private static SoftwareSystem? _userAccountSystem;

public static SoftwareSystem UserAccountSystem => _userAccountSystem ??= new SoftwareSystem(
"UserAccountSystem",
"EG User Account System")
{
Description = "Allows customers to view information about their " +
"account, and share meter readings."
};

private static SoftwareSystem? _notifications;

public static SoftwareSystem Notifications => _notifications ??= new SoftwareSystem(
"Notifications",
"Notifications System")
{
Description = "Push notifications to user´s device" ,
Boundary = Boundary.External
};

private static SoftwareSystem? _payments;

public static SoftwareSystem Payments => _payments ??= new SoftwareSystem(
"Payments",
"Payments System")
{
Description = "Manages direct debit and card payments " ,
Boundary = Boundary.External
};

private static SoftwareSystem? _backend;

public static SoftwareSystem Backend => _backend ??= new SoftwareSystem(
"Backend",
"Backend System")
{
Description = "Stores all core information, user accounts, billing details, etc ",
Boundary = Boundary.External
};
}
  • Classe que representa o Diagrama de Contexto (note que herda da classe DiagramBuildRunner como mencionado acima):
using C4ModelDiagramsArticleExamples.Structures;
using C4Sharp.Diagrams;
using C4Sharp.Diagrams.Interfaces;
using C4Sharp.Elements;
using C4Sharp.Elements.Boundaries;
using C4Sharp.Elements.Plantuml;
using C4Sharp.Elements.Plantuml.Constants;
using C4Sharp.Elements.Relationships;

namespace C4ModelDiagramsArticleExamples.Diagrams.Context;

public class ContextDiagram : DiagramBuildRunner
{
protected override string Title => "System Context Diagram for e-commerce.";

protected override DiagramType DiagramType => DiagramType.Context;

protected override IEnumerable<Structure> Structures => new Structure[]
{
People.Customer,
new EnterpriseBoundary("PLC", "EG PLC",
Systems.UserAccountSystem,
Systems.Notifications,
Systems.Payments,
Systems.Backend)
};

protected override IEnumerable<Relationship> Relationships => new []
{
(People.Customer > Systems.UserAccountSystem | "Uses").AddTags("dashed"),
(Systems.UserAccountSystem > Systems.Backend | "Uses").AddTags("dashed"),
(Systems.Backend > Systems.Payments | "Raises payment using" | Position.Neighbor).AddTags("dashed"),
(People.Customer < Systems.Notifications | "Send notifications to").AddTags("dashed"),
(Systems.UserAccountSystem > Systems.Notifications | "Sends notification using" | Position.Neighbor).AddTags("dashed"),
};

protected override IRelationshipTag? SetRelTags()
{
return new RelationshipTag()
.AddRelTag("dashed", "gray", "gray", LineStyle.DashedLine);
}
}

Observe que utilizei a classe EnterpriseBoundary para separar os Contextos dentro de uma delimitação dos sistemas (observar imagem 1).

Importante prestar atenção também no método que representa os relacionamentos. Vamos analisar um exemplo de forma isolada:

(People.Customer > Systems.UserAccountSystem | "Uses").AddTags("dashed")

Nesse relacionamento estou afirmando que Customer relaciona-se com UserAccountSystem, a descrição vem logo depois do pipe (“Uses”) e ao final adiciono uma Tag para indicar que a é linha tracejada. Esse estilo é definido logo abaixo no método SetRelTags().

Outra linha que gostaria de analisar é a do relacionamento do UserAccountSystem com o Notifications:

(Systems.UserAccountSystem > Systems.Notifications | "Sends notification using" | Position.Neighbor).AddTags("dashed"),

Percebe-se que após identificar o label (“Sends notification using") do relacionamento, foi utilizada outra propriedade: Position. A Position orienta visualmente onde o segundo elemento deverá ficar no gráfico. No exemplo, utilizei a Position.Neighbor (vizinho) para exibir o sistema Notifications ao lado. Você verá logo abaixo como ficou esse relacionamento.

Para executar e gerar os diagramas o código é bastante simples:

using C4ModelDiagramsArticleExamples.Diagrams.Context;
using C4Sharp.Elements.Plantuml.IO;

var diagrams = new[]
{
new ContextDiagram().Build()
};

var context = new PlantumlContext();

context
.UseDiagramImageBuilder() // png
.UseDiagramSvgImageBuilder() //svg
.UseDiagramMermaidBuilder() //mermaid
//.UseStandardLibraryBaseUrl() //load the resources from github C4plantuml repository
.Export(diagrams);

Com a execução do código acima, os arquivos serão gerados com o diagrama de Contexto configurado. Abaixo segue a imagem em PNG:

Imagem 1 — Diagrama de Contexto

Agora vamos criar os outros diagramas e exibir um a um os códigos utilizados para conhecimento.

Diagrama de Container

Para o Diagrama de Container seguimos o mesmo raciocínio do Contexto. Primeiro eu defini as novas Structures dentro de uma classe chamada Containers:

using C4Sharp.Elements.Containers;

namespace C4ModelDiagramsArticleExamples.Structures;

public static class Containers
{
private static ServerSideWebApp? _webApp;

public static ServerSideWebApp WebApp => _webApp ??= new ServerSideWebApp(
Alias: "WebApp",
Label: "WebApp",
Description: "Renders login page with necessary assets.",
Technology: "Node.js, JavaScript"
);

private static ClientSideWebApp? _spa;

public static ClientSideWebApp Spa => _spa ??= new ClientSideWebApp(
Alias: "Spa",
Label: "SPA",
Technology: "JavaScript, React",
Description: "Provides user capabilities based on their plan."
);

private static Mobile? _mobileApp;

public static Mobile MobileApp => _mobileApp ??= new Mobile(
Alias: "MobileApp",
Label: "MobileApp",
Description:
"Provides user capabilities based on their plan.",
Technology: "Android"
);

private static Database? _mariaDbDatabase;

public static Database MariaDbDatabase => _mariaDbDatabase ??= new Database(
Alias: "Database",
Label: "MariaDbDatabase",
Description: "Stores user accounts, billing, plans, payment details, etc.",
Technology: "MariaDB Database"
);

private static Api? _backendApi;

public static Api BackendApi => _backendApi ??= new Api(
Alias: "BackendApi",
Label: "BackendApi APP",
Description: "Provides User Account management via JSON/HTTPS API.",
Technology: "Node.js, Javascript"
);

}

Todas os tipos de objeto utilizados são definidos pela C4Sharp: ServerSideWebApp, ClientSideWebApp, Mobile, Database e Api. Isso é algo muito bacana que a biblioteca nos fornece. Diversos tipos de classes de Contêiners para que o diagrama seja o mais detalhado possível.

A próxima classe criada foi a ContainerDiagram que vai corresponder ao diagrama com as Structures e seus relacionamentos. É o mesmo padrão da ContextDiagram. Segue abaixo a classe com suas definições:

using C4ModelDiagramsArticleExamples.Structures;
using C4Sharp.Diagrams;
using C4Sharp.Diagrams.Interfaces;
using C4Sharp.Elements;
using C4Sharp.Elements.Boundaries;
using C4Sharp.Elements.Plantuml;
using C4Sharp.Elements.Plantuml.Constants;
using C4Sharp.Elements.Relationships;

namespace C4ModelDiagramsArticleExamples.Diagrams.Container;

public class ContainerDiagram : DiagramBuildRunner
{
protected override string Title => "Container Diagram for EG User Account";
protected override DiagramType DiagramType => DiagramType.Container;

protected override IEnumerable<Structure> Structures => new Structure[]
{
People.Customer,
Systems.Notifications,
Systems.Payments,
new SoftwareSystemBoundary("ssb", "EG User Account",
Containers.WebApp,
Containers.Spa,
Containers.MobileApp,
Containers.MariaDbDatabase,
Containers.BackendApi
)
};


protected override IEnumerable<Relationship> Relationships => new[]
{
(People.Customer > Containers.WebApp | ("Visits login page", "HTTPS")).AddTags("dashed"),
(People.Customer > Containers.Spa | ("Visits account, readings", "HTTPS")).AddTags("dashed"),
(People.Customer > Containers.MobileApp | "Visits account, readings").AddTags("dashed"),

(Containers.WebApp > Containers.Spa | "Delivers to user browser" | Position.Neighbor).AddTags("dashed"),
(Containers.Spa > Containers.BackendApi | ("Make API Calls", "async, JSON/HTTPS")).AddTags("dashed"),
(Containers.MariaDbDatabase < Containers.BackendApi | "Reads from and writes to" | Position.Neighbor)
.AddTags("dashed"),

(People.Customer < Systems.Notifications | "Send notifications to").AddTags("dashed"),
(Containers.BackendApi > Systems.Notifications | ("Sends notifications using", "JSON/HTTPS") | Position.Up)
.AddTags("dashed"),
(Containers.BackendApi > Systems.Payments | ("Triggers payment system using ", "JSON/HTTPS") |
Position.Neighbor).AddTags("dashed")
};

protected override IRelationshipTag? SetRelTags()
{
return new RelationshipTag()
.AddRelTag("dashed", "gray", "gray", LineStyle.DashedLine);
}
}

Note que é praticamente igual ao Diagrama de Contexto em termos de sintaxe e definições. A única classe que utilizei diferente foi a SoftwareSystemBoundary para definir a fronteira dos Containers frente aos sistemas externos. Ela é instanciada e espera os parâmetros de label, descrição e a lista de Structures que ela conterá.

Observe também que a classe ContainerDiagram herda da DiagramBuildRunner para que possa ser reconhecida como diagrama na classe principal. Vamos agora adicionar o novo diagrama criado à classe principal:

using C4ModelDiagramsArticleExamples.Diagrams.Container;
using C4ModelDiagramsArticleExamples.Diagrams.Context;
using C4Sharp.Elements.Plantuml.IO;

var diagrams = new[]
{
new ContextDiagram().Build(),
new ContainerDiagram().Build()
};

var context = new PlantumlContext();

context
.UseDiagramImageBuilder()
.UseDiagramSvgImageBuilder()
.UseDiagramMermaidBuilder()
//.UseStandardLibraryBaseUrl() //load the resources from github C4plantuml repository
.Export(diagrams);

E o resultado do novo diagrama é:

Imagem 2 — Diagrama de Container

Próximo passo é o diagrama de componente.

Diagrama de Componente

Vamos iniciar a construção do Diagrama de Componente que explora detalhes internos de um contêiner. O contêiner que o artigo em questão explora é o BackendApi APP.

Com o mesmo padrão dos diagramas anteriores, primeiro criei as Structures do Diagrama de Componente:

using C4Sharp.Elements;

namespace C4ModelDiagramsArticleExamples.Structures;

public static class Components
{
private static Component? _loginController;

public static Component LoginController => _loginController ??= new Component("sign", "Login Controller")
{
Description = "Allows users to sign in application",
Technology = "Node.js, Express",
};

private static Component? _resetController;

public static Component ResetController => _resetController ??= new Component("reset", "Reset Controller")
{
Description = "Allows users to reset password using email",
Technology = "Node.js, Express",
};

private static Component? _authController;

public static Component AuthController => _authController ??= new Component("auth", "Auth Controller")
{
Description = "Provides features to login users securely",
Technology = "Auth0, Express",
};

private static Component? _emailComponent;

public static Component EmailComponent => _emailComponent ??= new Component("email", "Email Component")
{
Description = "Send emails to users",
Technology = "Node.js, Express",
};

private static Component? _readingsComponent;

public static Component ReadingsComponent => _readingsComponent ??= new Component("readings", "Readings Component")
{
Description = "Allows users to submit meter readings",
Technology = "Node.js, Express",
};
}

Como é possível notar, cada Structure agora é do tipo Component no qual fica a critério escolher a tecnologia utilizada, descrição e características. No contêiner escolhido, os controllers foram detalhados assim como componentes que o compõe. Isso torna claro e de fácil entendimento o funcionamento interno sem necessariamente entrar em detalhe de código. Vamos continuar com a criação da classe que efetivamente representa o diagrama. Como os anteriores, herda da DiagramBuildRunner:

using C4ModelDiagramsArticleExamples.Structures;
using C4Sharp.Diagrams;
using C4Sharp.Diagrams.Interfaces;
using C4Sharp.Elements;
using C4Sharp.Elements.Boundaries;
using C4Sharp.Elements.Plantuml;
using C4Sharp.Elements.Plantuml.Constants;
using C4Sharp.Elements.Relationships;

namespace C4ModelDiagramsArticleExamples.Diagrams.Component;

public class ComponentDiagram : DiagramBuildRunner
{
protected override string Title => "Component Diagram for Backend API Application";
protected override DiagramType DiagramType => DiagramType.Component;

protected override IEnumerable<Structure> Structures => new Structure[]
{
Containers.MariaDbDatabase,
Systems.Notifications,
Containers.WebApp,
Containers.MobileApp,
Boundary()
};

protected override IEnumerable<Relationship> Relationships => new Relationship[]
{
(Containers.WebApp > Components.LoginController | ("Make API Calls", "JSON/HTTPS")).AddTags("dashed"),
(Containers.WebApp > Components.ReadingsComponent | ("Make API Calls", "JSON/HTTPS") | Position.Down)
.AddTags("dashed"),
(Containers.WebApp > Components.ResetController | ("Make API Calls", "JSON/HTTPS")).AddTags("dashed"),
(Containers.MobileApp > Components.LoginController | ("Make API Calls", "JSON/HTTPS")).AddTags("dashed"),
(Containers.MobileApp > Components.ReadingsComponent | ("Make API Calls", "JSON/HTTPS") | Position.Down)
.AddTags("dashed"),
(Containers.MobileApp > Components.ResetController | ("Make API Calls", "JSON/HTTPS")).AddTags("dashed")
};

private static ContainerBoundary Boundary()
{
return new("c1", "API Application")
{
Components = new[]
{
Components.LoginController,
Components.ResetController,
Components.EmailComponent,
Components.AuthController,
Components.ReadingsComponent,
},
Relationships = new[]
{
(Components.LoginController > Components.AuthController).AddTags("dashed"),
(Components.ResetController > Components.EmailComponent).AddTags("dashed"),
(Components.EmailComponent > Systems.Notifications | ("Sends email using", "")).AddTags("dashed"),
(Components.ResetController > Components.AuthController | ("Uses")).AddTags("dashed"),
(Components.AuthController > Containers.MariaDbDatabase | ("Read & write to DB", "JDBC"))
.AddTags("dashed"),
(Components.ReadingsComponent > Containers.MariaDbDatabase | ("Read & write to DB")).AddTags("dashed"),
(Components.ReadingsComponent > Systems.Notifications | ("Sends reading confirmation using"))
.AddTags("dashed")
}
};
}

protected override IRelationshipTag? SetRelTags()
{
return new RelationshipTag()
.AddRelTag("dashed", "gray", "gray", LineStyle.DashedLine);
}
}

Para demonstrar a fronteira (boundary) , um método foi criado com retorno do tipo ContainerBoundary. Essa classe permite informar quais componentes e seus relacionamentos que ficarão dentro do limite do escopo do contêiner.

Para incluir o novo diagrama na geração, adicione-o na classe principal como mostra abaixo:

using C4ModelDiagramsArticleExamples.Diagrams.Component;
using C4ModelDiagramsArticleExamples.Diagrams.Container;
using C4ModelDiagramsArticleExamples.Diagrams.Context;
using C4Sharp.Elements.Plantuml.IO;

var diagrams = new[]
{
new ContextDiagram().Build(),
new ContainerDiagram().Build(),
new ComponentDiagram().Build()
};

var context = new PlantumlContext();

context
.UseDiagramImageBuilder()
.UseDiagramSvgImageBuilder()
.UseDiagramMermaidBuilder()
//.UseStandardLibraryBaseUrl() //load the resources from github C4plantuml repository
.Export(diagrams);

O resultado do diagrama é esse:

Imagem 3— Diagrama de Componente

Local onde os arquivos dos diagramas ficam salvos

O resultado das imagens é salvo no caminho “\nomeDoProjeto\bin\Debug\net8.0\c4” conforme o print abaixo:

Imagem 4 — Arquivos gerados

Conclusão

No início do uso da lib pode parecer um pouco complexo para entender a disposição da imagem à medida que as alterações vão sendo feitas. Confesso que por algumas vezes usei a “tentativa/erro” para validar o que eu estava fazendo e analisando o resultado. Todavia, com o passar do tempo, o trabalho vai se tornando natural e o desenvolvimento começa a ser mais célere. Caso deseje utilizar “diagram as a code” para criação dos diagramas C4 Model, a C4Sharp é ao meu ver a melhor opção do momento.

À vista do que foi demonstrado, a C4Sharp pode e vai contribuir na construção de diagramas C4 Model. Importante mencionar que foram vistos apenas os principais diagramas, pois a biblioteca ainda tem muito mais. Um trabalho realmente excelente de Yan Justino que merece todo o destaque que tem recebido.

Referências

--

--