Skip to content

Commit

Permalink
Merge pull request #4 from NikiforovAll/feature/dependency-explorer
Browse files Browse the repository at this point in the history
feat: add dependency explorer
  • Loading branch information
NikiforovAll authored Aug 3, 2024
2 parents 83eebc8 + c6339d2 commit 24bbc34
Show file tree
Hide file tree
Showing 18 changed files with 383 additions and 35 deletions.
84 changes: 73 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,33 +6,68 @@
[![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg)](https://conventionalcommits.org)
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/nikiforovall/dependify/blob/main/LICENSE.md)

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.

| Package | Version | Description |
| ---------------- | -------------------------------------------------------------------------------------------------------- | ------------------------------------ |
| `Dependify.Cli` | [![Nuget](https://img.shields.io/nuget/v/Dependify.Cli.svg)](https://nuget.org/packages/Dependify.Cli) | CLI |
| `Dependify.Core` | [![Nuget](https://img.shields.io/nuget/v/Dependify.Core.svg)](https://nuget.org/packages/Dependify.Core) | Core library |
| ` Dependify.Aspire.Hosting` | [![Nuget](https://img.shields.io/nuget/v/Dependify.Aspire.Hosting.svg)](https://nuget.org/packages/Dependify.Aspire.Hosting) | Aspire support |

## Install

```bash
dotnet tool install -g Dependify.Cli
```

## Usage
```bash
❯ dependify -h
USAGE:
Dependify.Cli.dll [OPTIONS] <COMMAND>

EXAMPLES:
Dependify.Cli.dll graph scan ./path/to/folder --framework net8
Dependify.Cli.dll graph show ./path/to/project --framework net8

You can start dependify in `serve mode` and open the browser to navigate the generated graph.
OPTIONS:
-h, --help Prints help information

COMMANDS:
graph
serve <path>
```

## Usage

```bash
dependify serve $dev/keycloak-authorization-services-dotnet/
dependify serve $dev/path-to-folder/
```

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

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

![serve-main-window](./assets/serve-main-window.png)
### Features

- Workbench ⚙️
- Dependency Explorer 🔎

Workbench gives you high level overview of the dependencies in the solution.

<video src="https://github.com/user-attachments/assets/e3eecf59-864d-4a7b-9411-60ee7a364c57" controls="controls">
</video>

You can open the mermaid diagram right in the browser.

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

Dependency Explorer allows you to select the dependencies you want to see.

<video src="https://github.com/user-attachments/assets/555df3ef-b0c3-4354-911f-81d4dfd07607" controls="controls">
</video>

### Aspire support

You can add `Dependify.Web` as resource to your Aspire project.

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

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


```bash
dependify graph --help
```

```text
USAGE:
dependify graph [OPTIONS] <COMMAND>
Expand All @@ -84,19 +118,20 @@ COMMANDS:
show <path> Shows the dependencies of a project or solution located in the specified path
```

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.

```bash
dependify graph scan \
$dev/keycloak-authorization-services-dotnet/ \
--framework net8
dependify graph scan $dev/keycloak-authorization-services-dotnet/
```

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

Here is how to change the output format to `mermaid`.

```bash
dependify graph scan \
$dev/keycloak-authorization-services-dotnet/ \
--exclude-sln \
--framework net8 \
--format mermaid \
--output ./graph.md
```
Expand Down Expand Up @@ -173,7 +208,34 @@ graph LR
Keycloak.AuthServices.IntegrationTests.csproj --> TestWebApiWithControllers.csproj
classDef project fill:#74200154;
classDef package fill:#22aaee;
```

### API

You can use the API to build your own tools.

```bash
dotnet add package Dependify.Core
```

```csharp
var services = new ServiceCollection()
.AddLogging()
.AddSingleton<ProjectLocator>()
.AddSingleton<MsBuildService>();

var provider = services.BuildServiceProvider();

var locator = provider.GetRequiredService<ProjectLocator>();
var msBuildService = provider.GetRequiredService<MsBuildService>();

var nodes = locator.FullScan("C:\\Users\\joel\\source\\repos\\Dependify");

var solution = nodes.OfType<SolutionReferenceNode>().FirstOrDefault();

var graph = msBuildService.AnalyzeReferences(solution, MsBuildConfig.Default);

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

## Build and Development
Expand Down
Binary file added assets/dependency-explorer-demo.mp4
Binary file not shown.
Binary file added assets/workbench-demo.mp4
Binary file not shown.
21 changes: 21 additions & 0 deletions src/Dependify.Core/Graph/DependencyGraph.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,27 @@ public DependencyGraph SubGraph(Node node, Predicate<Node>? filter = default)
return new DependencyGraph(node, nodes, edges);
}

public DependencyGraph SubGraph(Predicate<Node>? filter = default)
{
var nodes = this
.Nodes.SelectMany(n =>
{
if (filter is not null && !filter(n))
{
return [];
}

return this.FindAllDescendants(n, filter).Concat([n]);
})
.ToList();

// TODO: bug - fix, filter?.Invoke(edge.End) == true

var edges = this.Edges.Where(edge => nodes.Contains(edge.Start) && filter?.Invoke(edge.End) == true).ToList();

return new DependencyGraph(new SolutionReferenceNode(), nodes, edges);
}

private IEnumerable<Node> FindAllDescendants(Node node, Predicate<Node>? filter = default)

Check warning on line 62 in src/Dependify.Core/Graph/DependencyGraph.cs

View workflow job for this annotation

GitHub Actions / Build-windows-latest

Change return type of method 'FindAllDescendants' from 'System.Collections.Generic.IEnumerable<Dependify.Core.Graph.Node>' to 'System.Collections.Generic.List<Dependify.Core.Graph.Node>' for improved performance (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1859)

Check warning on line 62 in src/Dependify.Core/Graph/DependencyGraph.cs

View workflow job for this annotation

GitHub Actions / Build-ubuntu-latest

Change return type of method 'FindAllDescendants' from 'System.Collections.Generic.IEnumerable<Dependify.Core.Graph.Node>' to 'System.Collections.Generic.List<Dependify.Core.Graph.Node>' for improved performance (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1859)

Check warning on line 62 in src/Dependify.Core/Graph/DependencyGraph.cs

View workflow job for this annotation

GitHub Actions / Build-windows-latest

Change return type of method 'FindAllDescendants' from 'System.Collections.Generic.IEnumerable<Dependify.Core.Graph.Node>' to 'System.Collections.Generic.List<Dependify.Core.Graph.Node>' for improved performance (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1859)

Check warning on line 62 in src/Dependify.Core/Graph/DependencyGraph.cs

View workflow job for this annotation

GitHub Actions / Build-windows-latest

Change return type of method 'FindAllDescendants' from 'System.Collections.Generic.IEnumerable<Dependify.Core.Graph.Node>' to 'System.Collections.Generic.List<Dependify.Core.Graph.Node>' for improved performance (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1859)
{
var nodes = new List<Node>();
Expand Down
3 changes: 3 additions & 0 deletions src/Dependify.Core/MsBuildService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ public enum NodeEventType
ProjectLoaded,
SolutionLoading,
SolutionLoaded,
RegistryLoaded,
Other
}

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

public static MsBuildConfig Default => new(includePackages: true, fullScan: true, framework: null);

public bool IncludePackages { get; set; }
public bool FullScan { get; set; }
public string? Framework { get; set; }
Expand Down
4 changes: 2 additions & 2 deletions src/Dependify.Core/ProjectLocator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ public class ProjectLocator(ILogger<ProjectLocator> logger)
private const string ProjectFileExtension = ".csproj";

/// <summary>
/// Scans the specified path for .csproj and solution files.
/// Scans the specified path for .csproj and solution files recursively.
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
Expand All @@ -19,7 +19,7 @@ public IEnumerable<Node> FullScan(string? path)
}

/// <summary>
/// Scans the specified path for .csproj and solution files.
/// Scans the specified path for .csproj and solution files recursively with a max depth of 1.
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
Expand Down
5 changes: 5 additions & 0 deletions src/Dependify.Core/Serializers/MermaidSerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ public static string ToString(DependencyGraph graph)

writer.Indent++;

foreach (var node in graph.Nodes.OfType<SolutionReferenceNode>())
{
writer.WriteLine($"{node.Id}");
}

foreach (var node in graph.Nodes.OfType<ProjectReferenceNode>())
{
writer.WriteLine($"{node.Id}:::project");
Expand Down
37 changes: 34 additions & 3 deletions src/Dependify.Core/SolutionRegistry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ public Task LoadSolutionsAsync(MsBuildConfig msBuildConfig, CancellationToken ca
if (solution == this.Solutions[^1])
{
this.subject.OnNext(
new NodeEvent(NodeEventType.Other, string.Empty, string.Empty)
new NodeEvent(NodeEventType.RegistryLoaded, string.Empty, string.Empty)
{
Message = "All solutions loaded"
}
Expand All @@ -82,7 +82,7 @@ public Task LoadSolutionsAsync(MsBuildConfig msBuildConfig, CancellationToken ca
return Task.CompletedTask;
}

public NodeUsageStatistics GetDependencyCount(SolutionReferenceNode solution, Node node)
public NodeUsage GetDependencyCount(SolutionReferenceNode solution, Node node)
{
var graph = this.GetGraph(solution);

Expand All @@ -100,9 +100,40 @@ public NodeUsageStatistics GetDependencyCount(SolutionReferenceNode solution, No
{
return this.solutionGraphs.TryGetValue(solution, out var graph) ? graph : null;
}

public DependencyGraph GetFullGraph()
{
var builder = new DependencyGraph.Builder(new SolutionReferenceNode());

foreach (var (solution, graph) in this.solutionGraphs)
{
var solutionNode = new SolutionReferenceNode(solution.Path);

builder.WithNode(solutionNode);

foreach (var node in graph.Nodes)
{
if (node.Type == NodeConstants.Solution)
{
continue;
}

builder.WithNode(node);

builder.WithEdge(new Edge(solutionNode, node));

foreach (var edgeNode in graph.FindDescendants(node))
{
builder.WithEdge(new Edge(node, edgeNode));
}
}
}

return builder.Build();
}
}

public record NodeUsageStatistics(
public record NodeUsage(
Node Node,
IList<ProjectReferenceNode> DependsOnProjects,
IList<PackageReferenceNode> DependsOnPackages,
Expand Down
14 changes: 14 additions & 0 deletions src/Web/Components/App.razor
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,20 @@
});
};
</script>
<script type="module">
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs';
window.redrawMermaidDiagram = async (content) => {
const drawDiagram = async function () {
let element = document.querySelector('.mermaid');
const graphDefinition = content;
const { svg } = await mermaid.render('graphDiv', graphDefinition);
element.innerHTML = svg;
};
await drawDiagram();
};
</script>
</body>

</html>
2 changes: 1 addition & 1 deletion src/Web/Components/Layout/MainLayout.razor
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
</div>

@code {
private bool _drawerOpen = false;
private bool _drawerOpen = true;
private bool _isDarkMode = false;
private MudTheme? _theme = null;

Expand Down
1 change: 1 addition & 0 deletions src/Web/Components/Layout/NavMenu.razor
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<MudNavMenu style="display: flex; flex-direction: column; height: 100%;">
<MudNavLink Href="" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.Home">Home</MudNavLink>
<MudNavLink Href="dependency-explorer" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.Search">Dependency Explorer</MudNavLink>
<MudDivider />
<div style=" margin-top: auto;padding: 10px;">
<MudText>Commit: @GitCommit</MudText>
Expand Down
Loading

0 comments on commit 24bbc34

Please sign in to comment.