Skip to content

Commit 24bbc34

Browse files
authored
Merge pull request #4 from NikiforovAll/feature/dependency-explorer
feat: add dependency explorer
2 parents 83eebc8 + c6339d2 commit 24bbc34

18 files changed

+383
-35
lines changed

README.md

+73-11
Original file line numberDiff line numberDiff line change
@@ -6,33 +6,68 @@
66
[![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg)](https://conventionalcommits.org)
77
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/nikiforovall/dependify/blob/main/LICENSE.md)
88

9+
Dependify is a tool to visualize dependencies in your .NET application. You can start dependify in `serve` mode to visualize dependencies in a browser or use the `CLI` if you prefer the terminal.
10+
11+
| Package | Version | Description |
12+
| ---------------- | -------------------------------------------------------------------------------------------------------- | ------------------------------------ |
13+
| `Dependify.Cli` | [![Nuget](https://img.shields.io/nuget/v/Dependify.Cli.svg)](https://nuget.org/packages/Dependify.Cli) | CLI |
14+
| `Dependify.Core` | [![Nuget](https://img.shields.io/nuget/v/Dependify.Core.svg)](https://nuget.org/packages/Dependify.Core) | Core library |
15+
| ` Dependify.Aspire.Hosting` | [![Nuget](https://img.shields.io/nuget/v/Dependify.Aspire.Hosting.svg)](https://nuget.org/packages/Dependify.Aspire.Hosting) | Aspire support |
916

1017
## Install
1118

1219
```bash
1320
dotnet tool install -g Dependify.Cli
1421
```
1522

16-
## Usage
23+
```bash
24+
❯ dependify -h
25+
USAGE:
26+
Dependify.Cli.dll [OPTIONS] <COMMAND>
27+
28+
EXAMPLES:
29+
Dependify.Cli.dll graph scan ./path/to/folder --framework net8
30+
Dependify.Cli.dll graph show ./path/to/project --framework net8
1731

18-
You can start dependify in `serve mode` and open the browser to navigate the generated graph.
32+
OPTIONS:
33+
-h, --help Prints help information
34+
35+
COMMANDS:
36+
graph
37+
serve <path>
38+
```
39+
40+
## Usage
1941

2042
```bash
21-
dependify serve $dev/keycloak-authorization-services-dotnet/
43+
dependify serve $dev/path-to-folder/
2244
```
2345

24-
You will see the following output in the terminal. Open <http:localhost:9999/> and browse the graph.
46+
You will see something like the following output in the terminal.
2547

2648
![serve-terminal](./assets/serve-terminal.png)
2749

28-
![serve-main-window](./assets/serve-main-window.png)
50+
### Features
51+
52+
- Workbench ⚙️
53+
- Dependency Explorer 🔎
54+
55+
Workbench gives you high level overview of the dependencies in the solution.
56+
57+
<video src="https://github.com/user-attachments/assets/e3eecf59-864d-4a7b-9411-60ee7a364c57" controls="controls">
58+
</video>
2959

3060
You can open the mermaid diagram right in the browser.
3161

3262
![serve-graph-view](./assets/serve-graph-view.png)
3363

64+
Dependency Explorer allows you to select the dependencies you want to see.
65+
66+
<video src="https://github.com/user-attachments/assets/555df3ef-b0c3-4354-911f-81d4dfd07607" controls="controls">
67+
</video>
3468

3569
### Aspire support
70+
3671
You can add `Dependify.Web` as resource to your Aspire project.
3772

3873
Add the package to AppHost:
@@ -63,11 +98,10 @@ See the [samples/aspire-project](./samples/aspire-project) for more details.
6398

6499
You can use the CLI for the automation or if you prefer the terminal.
65100

66-
67101
```bash
68102
dependify graph --help
69103
```
70-
104+
71105
```text
72106
USAGE:
73107
dependify graph [OPTIONS] <COMMAND>
@@ -84,19 +118,20 @@ COMMANDS:
84118
show <path> Shows the dependencies of a project or solution located in the specified path
85119
```
86120

121+
The command `scan` will scan the folder for projects and solutions and retrieve their dependencies. The ouput can be in `tui` or `mermaid` format. The `tui` or terminal user interface is the default output format.
122+
87123
```bash
88-
dependify graph scan \
89-
$dev/keycloak-authorization-services-dotnet/ \
90-
--framework net8
124+
dependify graph scan $dev/keycloak-authorization-services-dotnet/
91125
```
92126

93127
![tui-demo1](./assets/tui-demo1.png)
94128

129+
Here is how to change the output format to `mermaid`.
130+
95131
```bash
96132
dependify graph scan \
97133
$dev/keycloak-authorization-services-dotnet/ \
98134
--exclude-sln \
99-
--framework net8 \
100135
--format mermaid \
101136
--output ./graph.md
102137
```
@@ -173,7 +208,34 @@ graph LR
173208
Keycloak.AuthServices.IntegrationTests.csproj --> TestWebApiWithControllers.csproj
174209
classDef project fill:#74200154;
175210
classDef package fill:#22aaee;
211+
```
212+
213+
### API
214+
215+
You can use the API to build your own tools.
216+
217+
```bash
218+
dotnet add package Dependify.Core
219+
```
220+
221+
```csharp
222+
var services = new ServiceCollection()
223+
.AddLogging()
224+
.AddSingleton<ProjectLocator>()
225+
.AddSingleton<MsBuildService>();
226+
227+
var provider = services.BuildServiceProvider();
228+
229+
var locator = provider.GetRequiredService<ProjectLocator>();
230+
var msBuildService = provider.GetRequiredService<MsBuildService>();
231+
232+
var nodes = locator.FullScan("C:\\Users\\joel\\source\\repos\\Dependify");
233+
234+
var solution = nodes.OfType<SolutionReferenceNode>().FirstOrDefault();
235+
236+
var graph = msBuildService.AnalyzeReferences(solution, MsBuildConfig.Default);
176237

238+
var subgraph = graph.SubGraph(n => n.Id.Contains("AwesomeProjectName"));
177239
```
178240

179241
## Build and Development

assets/dependency-explorer-demo.mp4

29 MB
Binary file not shown.

assets/workbench-demo.mp4

32.5 MB
Binary file not shown.

src/Dependify.Core/Graph/DependencyGraph.cs

+21
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,27 @@ public DependencyGraph SubGraph(Node node, Predicate<Node>? filter = default)
3838
return new DependencyGraph(node, nodes, edges);
3939
}
4040

41+
public DependencyGraph SubGraph(Predicate<Node>? filter = default)
42+
{
43+
var nodes = this
44+
.Nodes.SelectMany(n =>
45+
{
46+
if (filter is not null && !filter(n))
47+
{
48+
return [];
49+
}
50+
51+
return this.FindAllDescendants(n, filter).Concat([n]);
52+
})
53+
.ToList();
54+
55+
// TODO: bug - fix, filter?.Invoke(edge.End) == true
56+
57+
var edges = this.Edges.Where(edge => nodes.Contains(edge.Start) && filter?.Invoke(edge.End) == true).ToList();
58+
59+
return new DependencyGraph(new SolutionReferenceNode(), nodes, edges);
60+
}
61+
4162
private IEnumerable<Node> FindAllDescendants(Node node, Predicate<Node>? filter = default)
4263
{
4364
var nodes = new List<Node>();

src/Dependify.Core/MsBuildService.cs

+3
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ public enum NodeEventType
183183
ProjectLoaded,
184184
SolutionLoading,
185185
SolutionLoaded,
186+
RegistryLoaded,
186187
Other
187188
}
188189

@@ -197,6 +198,8 @@ public MsBuildConfig(bool includePackages, bool fullScan, string? framework)
197198
this.Framework = framework;
198199
}
199200

201+
public static MsBuildConfig Default => new(includePackages: true, fullScan: true, framework: null);
202+
200203
public bool IncludePackages { get; set; }
201204
public bool FullScan { get; set; }
202205
public string? Framework { get; set; }

src/Dependify.Core/ProjectLocator.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ public class ProjectLocator(ILogger<ProjectLocator> logger)
99
private const string ProjectFileExtension = ".csproj";
1010

1111
/// <summary>
12-
/// Scans the specified path for .csproj and solution files.
12+
/// Scans the specified path for .csproj and solution files recursively.
1313
/// </summary>
1414
/// <param name="path"></param>
1515
/// <returns></returns>
@@ -19,7 +19,7 @@ public IEnumerable<Node> FullScan(string? path)
1919
}
2020

2121
/// <summary>
22-
/// Scans the specified path for .csproj and solution files.
22+
/// Scans the specified path for .csproj and solution files recursively with a max depth of 1.
2323
/// </summary>
2424
/// <param name="path"></param>
2525
/// <returns></returns>

src/Dependify.Core/Serializers/MermaidSerializer.cs

+5
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ public static string ToString(DependencyGraph graph)
1919

2020
writer.Indent++;
2121

22+
foreach (var node in graph.Nodes.OfType<SolutionReferenceNode>())
23+
{
24+
writer.WriteLine($"{node.Id}");
25+
}
26+
2227
foreach (var node in graph.Nodes.OfType<ProjectReferenceNode>())
2328
{
2429
writer.WriteLine($"{node.Id}:::project");

src/Dependify.Core/SolutionRegistry.cs

+34-3
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ public Task LoadSolutionsAsync(MsBuildConfig msBuildConfig, CancellationToken ca
6868
if (solution == this.Solutions[^1])
6969
{
7070
this.subject.OnNext(
71-
new NodeEvent(NodeEventType.Other, string.Empty, string.Empty)
71+
new NodeEvent(NodeEventType.RegistryLoaded, string.Empty, string.Empty)
7272
{
7373
Message = "All solutions loaded"
7474
}
@@ -82,7 +82,7 @@ public Task LoadSolutionsAsync(MsBuildConfig msBuildConfig, CancellationToken ca
8282
return Task.CompletedTask;
8383
}
8484

85-
public NodeUsageStatistics GetDependencyCount(SolutionReferenceNode solution, Node node)
85+
public NodeUsage GetDependencyCount(SolutionReferenceNode solution, Node node)
8686
{
8787
var graph = this.GetGraph(solution);
8888

@@ -100,9 +100,40 @@ public NodeUsageStatistics GetDependencyCount(SolutionReferenceNode solution, No
100100
{
101101
return this.solutionGraphs.TryGetValue(solution, out var graph) ? graph : null;
102102
}
103+
104+
public DependencyGraph GetFullGraph()
105+
{
106+
var builder = new DependencyGraph.Builder(new SolutionReferenceNode());
107+
108+
foreach (var (solution, graph) in this.solutionGraphs)
109+
{
110+
var solutionNode = new SolutionReferenceNode(solution.Path);
111+
112+
builder.WithNode(solutionNode);
113+
114+
foreach (var node in graph.Nodes)
115+
{
116+
if (node.Type == NodeConstants.Solution)
117+
{
118+
continue;
119+
}
120+
121+
builder.WithNode(node);
122+
123+
builder.WithEdge(new Edge(solutionNode, node));
124+
125+
foreach (var edgeNode in graph.FindDescendants(node))
126+
{
127+
builder.WithEdge(new Edge(node, edgeNode));
128+
}
129+
}
130+
}
131+
132+
return builder.Build();
133+
}
103134
}
104135

105-
public record NodeUsageStatistics(
136+
public record NodeUsage(
106137
Node Node,
107138
IList<ProjectReferenceNode> DependsOnProjects,
108139
IList<PackageReferenceNode> DependsOnPackages,

src/Web/Components/App.razor

+14
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,20 @@
2626
});
2727
};
2828
</script>
29+
<script type="module">
30+
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs';
31+
32+
window.redrawMermaidDiagram = async (content) => {
33+
const drawDiagram = async function () {
34+
let element = document.querySelector('.mermaid');
35+
const graphDefinition = content;
36+
const { svg } = await mermaid.render('graphDiv', graphDefinition);
37+
element.innerHTML = svg;
38+
};
39+
40+
await drawDiagram();
41+
};
42+
</script>
2943
</body>
3044

3145
</html>

src/Web/Components/Layout/MainLayout.razor

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
</div>
3030

3131
@code {
32-
private bool _drawerOpen = false;
32+
private bool _drawerOpen = true;
3333
private bool _isDarkMode = false;
3434
private MudTheme? _theme = null;
3535

src/Web/Components/Layout/NavMenu.razor

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<MudNavMenu style="display: flex; flex-direction: column; height: 100%;">
22
<MudNavLink Href="" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.Home">Home</MudNavLink>
3+
<MudNavLink Href="dependency-explorer" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.Search">Dependency Explorer</MudNavLink>
34
<MudDivider />
45
<div style=" margin-top: auto;padding: 10px;">
56
<MudText>Commit: @GitCommit</MudText>

0 commit comments

Comments
 (0)