Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,7 @@ dotnet run --project src/ProjGraph.Mcp
projgraph visualize ./MySolution.sln

# Mermaid format for documentation
projgraph visualize ./MySolution.slnx --format mermaid > docs/dependencies.mmd
```
projgraph visualize ./MySolution.slnx --format mermaid > docs/dependencies.mmd```

### Generate Database Diagrams

Expand Down
2 changes: 1 addition & 1 deletion samples/visualize/simple-dependencies/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ projgraph visualize A/A.csproj

**Output:**

```
```bash
Projects
├── 📦 A
│ └── → B
Expand Down
1 change: 1 addition & 0 deletions specs/001-cli-graph-rendering/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ As a modern .NET developer, I want the tool to support the new XML-based `.slnx`
- **Description**: Analyzes a .NET solution or project file and returns the dependency graph as a set of nodes and edges.
- **Parameters**:
- `path`: (string) The absolute path to the .sln, .slnx, or .csproj file.
- `show_title`: (boolean, optional) Whether to include the title in the diagram (default: true).
- `includePackages`: (boolean) (Out of scope for initial version) Whether to include NuGet package dependencies in the graph.

## Success Criteria *(mandatory)*
Expand Down
1 change: 1 addition & 0 deletions specs/002-dbcontext-erd/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ As an AI Assistant, I want to retrieve a Mermaid ERD of the project's data model
- **Parameters**:
- `path`: (string) Absolute path to the DbContext .cs file.
- `contextName`: (string, optional) Specific DbContext class name to use if multiple are present.
- `show_title`: (boolean, optional) Whether to include the title in the diagram (default: true).

## Success Criteria *(mandatory)*

Expand Down
1 change: 1 addition & 0 deletions specs/003-mermaid-class-diagram/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,4 @@ As a developer, I want to control the depth of the discovery so the diagram does
- `includeInheritance`: (boolean) Whether to search the workspace for base classes and interfaces.
- `includeDependencies`: (boolean) Whether to search for and include classes used by the target types.
- `depth`: (integer) How many levels of relationships to follow (default: 1).
- `show_title`: (boolean, optional) Whether to include the title in the diagram (default: true).
12 changes: 10 additions & 2 deletions src/ProjGraph.Cli/Commands/ClassDiagramCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,14 @@ public sealed class Settings : CommandSettings
[DefaultValue(1)]
public int Depth { get; init; }

/// <summary>
/// Gets or sets a value indicating whether to include the title in the rendered output.
/// </summary>
[CommandOption("--show-title <true|false>")]
[Description("Include the diagram title (default true)")]
[DefaultValue(true)]
public bool ShowTitle { get; init; } = true;

/// <summary>
/// Validates the settings provided by the user.
/// Ensures the file path is valid, exists, and points to a .cs file.
Expand Down Expand Up @@ -108,8 +116,8 @@ public override async Task<int> ExecuteAsync(
settings.IncludeDependencies,
settings.Depth);

var mermaid = mermaidRenderer.Render(model);
console.WriteLine(mermaid);
var mermaidOutput = mermaidRenderer.Render(model, new DiagramOptions(settings.ShowTitle));
console.WriteLine(mermaidOutput);

return 0;
}
Expand Down
12 changes: 10 additions & 2 deletions src/ProjGraph.Cli/Commands/ErdCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@ public sealed class Settings : CommandSettings
[Description("The name of the DbContext or ModelSnapshot to analyze (optional)")]
public string? ContextName { get; init; }

/// <summary>
/// Gets or sets a value indicating whether to include the title in the rendered output.
/// </summary>
[CommandOption("--show-title <true|false>")]
[Description("Include the diagram title (default true)")]
[DefaultValue(true)]
public bool ShowTitle { get; init; } = true;

/// <summary>
/// Validates the settings provided for the command.
/// Ensures that the specified path exists, is a .cs file, or is left empty to search the current directory.
Expand Down Expand Up @@ -107,8 +115,8 @@ public override async Task<int> ExecuteAsync(

var model = await AnalyzeModelAsync(targetPath, settings.ContextName, cancellationToken);

var mermaid = mermaidRenderer.Render(model);
console.WriteLine(mermaid);
var mermaidOutput = mermaidRenderer.Render(model, new DiagramOptions(settings.ShowTitle));
console.WriteLine(mermaidOutput);

return 0;
}
Expand Down
26 changes: 19 additions & 7 deletions src/ProjGraph.Cli/Commands/VisualizeCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ public sealed class VisualizeCommand(
IOutputConsole console)
: AsyncCommand<VisualizeCommand.Settings>
{
private const string FormatMermaid = "mermaid";
private const string FormatTree = "tree";
private const string FormatFlat = "flat";

/// <summary>
/// Represents the settings for the `VisualizeCommand`.
/// </summary>
Expand All @@ -49,6 +53,14 @@ public sealed class Settings : CommandSettings
[DefaultValue("mermaid")]
public string Format { get; private set; } = "mermaid";

/// <summary>
/// Gets or sets a value indicating whether to include the title in the rendered output.
/// </summary>
[CommandOption("--show-title <true|false>")]
[Description("Include the diagram title (default true)")]
[DefaultValue(true)]
public bool ShowTitle { get; init; } = true;

/// <summary>
/// Validates the settings provided for the command.
/// Ensures that the specified path exists, is valid, and that the format is "flat", "tree" or "mermaid".
Expand All @@ -69,7 +81,7 @@ public override ValidationResult Validate()
}

Format = Format.ToLowerInvariant();
if (Format != "flat" && Format != "tree" && Format != "mermaid")
if (Format != FormatFlat && Format != FormatTree && Format != FormatMermaid)
{
return ValidationResult.Error("Format must be 'flat', 'tree' or 'mermaid'");
}
Expand Down Expand Up @@ -101,13 +113,13 @@ public override async Task<int> ExecuteAsync(
{
try
{
if (settings.Format.Equals("mermaid", StringComparison.OrdinalIgnoreCase))
if (settings.Format.Equals(FormatMermaid, StringComparison.OrdinalIgnoreCase))
{
// For mermaid, we want clean stdout, so all status goes to stderr
console.WriteInfo($"Analyzing {settings.Path}...");

var graph = await Task.Run(() => graphService.BuildGraph(settings.Path), cancellationToken);
console.WriteLine(GetRenderer(settings.Format).Render(graph));
console.WriteLine(GetRenderer(settings.Format).Render(graph, new DiagramOptions(settings.ShowTitle)));
}
else
{
Expand All @@ -126,7 +138,7 @@ await AnsiConsole.Status()
return 0;
}

console.WriteLine(GetRenderer(settings.Format).Render(graph));
console.WriteLine(GetRenderer(settings.Format).Render(graph, new DiagramOptions(settings.ShowTitle)));
}

return 0;
Expand All @@ -152,9 +164,9 @@ private IDiagramRenderer<SolutionGraph> GetRenderer(string format)
{
return format.ToLowerInvariant() switch
{
"mermaid" => renderers.OfType<MermaidGraphRenderer>().First(),
"tree" => renderers.OfType<TreeGraphRenderer>().First(),
"flat" => renderers.OfType<FlatGraphRenderer>().First(),
FormatMermaid => renderers.OfType<MermaidGraphRenderer>().First(),
FormatTree => renderers.OfType<TreeGraphRenderer>().First(),
FormatFlat => renderers.OfType<FlatGraphRenderer>().First(),
_ => throw new ArgumentException($"Unsupported format: {format}")
};
}
Expand Down
31 changes: 30 additions & 1 deletion src/ProjGraph.Cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,17 @@ projgraph visualize ./MySolution.sln

# Mermaid diagram
projgraph visualize ./MySolution.slnx --format mermaid > graph.mmd

# Mermaid diagram without title header
projgraph visualize ./MySolution.slnx --format mermaid --show-title false
```

**Settings**:

- `<PATH>`: Path to `.sln`, `.slnx`, or `.csproj` file.
- `-f|--format`: Output format (`flat`, `tree`, `mermaid`). Default: `mermaid`.
- `--show-title <true|false>`: Include diagram title. Default: `true`.

**Supports**: `.sln`, `.slnx`, `.csproj`

**Example output**:
Expand All @@ -46,8 +55,17 @@ projgraph erd ./Migrations/MyDbContextModelSnapshot.cs

# Save to file
projgraph erd ./Data/MyDbContext.cs > database-schema.md

# Generate without title header
projgraph erd ./Data/MyDbContext.cs --show-title false
```

**Settings**:

- `[path]`: Optional path to `.cs` file. Searches current directory if not specified.
- `-c|--context <NAME>`: Optional context/snapshot name.
- `--show-title <true|false>`: Include diagram title. Default: `true`.

**Features**:

- Detects entities, properties, and relationships from source or snapshots
Expand Down Expand Up @@ -83,10 +101,21 @@ Generate Mermaid Class Diagram for a specific class and its hierarchy.
# Analyze a specific class and discover its base types and dependencies
projgraph classdiagram ./Models/Admin.cs

# Specify depth of discovery (default: 3)
# Specify depth of discovery (default: 1)
projgraph classdiagram ./Models/Admin.cs --depth 5

# Generate without title header
projgraph classdiagram ./Models/Admin.cs --show-title false
```

**Settings**:

- `<path>`: Path to the `.cs` file.
- `-i|--inheritance`: Include base classes/interfaces. Default: `false`.
- `-d|--dependencies`: Include dependent types. Default: `false`.
- `--depth <INT>`: Max discovery depth. Default: `1`.
- `--show-title <true|false>`: Include diagram title. Default: `true`.

**Features**:

- Detects properties, fields, and inheritance (`<|--`)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,14 @@ public sealed class MermaidClassDiagramRenderer : IDiagramRenderer<ClassModel>
/// Renders a ClassModel as a Mermaid class diagram.
/// </summary>
/// <param name="model">The ClassModel containing the types, relationships, and an optional title to be rendered.</param>
/// <param name="options">The options for rendering the diagram.</param>
/// <returns>A string representation of the Mermaid class diagram.</returns>
public string Render(ClassModel model)
public string Render(ClassModel model, DiagramOptions? options = null)
{
var sb = new StringBuilder();
sb.AppendLine("```mermaid");

if (!string.IsNullOrWhiteSpace(model.Title))
if ((options?.ShowTitle ?? true) && !string.IsNullOrWhiteSpace(model.Title))
{
sb.AppendLine("---");
sb.AppendLine($"title: {model.Title}");
Expand Down
7 changes: 7 additions & 0 deletions src/ProjGraph.Lib.Core/Abstractions/DiagramOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace ProjGraph.Lib.Core.Abstractions;

/// <summary>
/// Represents options for rendering a diagram.
/// </summary>
/// <param name="ShowTitle">Whether to include the title in the rendered output.</param>
public record DiagramOptions(bool ShowTitle = true);
3 changes: 2 additions & 1 deletion src/ProjGraph.Lib.Core/Abstractions/IDiagramRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public interface IDiagramRenderer<in TModel>
/// Renders the specified model into a string representation.
/// </summary>
/// <param name="model">The model to render.</param>
/// <param name="options">The options for rendering the diagram.</param>
/// <returns>A string representation of the diagram.</returns>
string Render(TModel model);
string Render(TModel model, DiagramOptions? options = null);
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,16 @@ public sealed class MermaidErdRenderer : IDiagramRenderer<EfModel>
/// Renders a Mermaid ERD diagram from the given Entity Framework model.
/// </summary>
/// <param name="model">The EF model containing entities and relationships to be rendered.</param>
/// <param name="options">The options for rendering the diagram.</param>
/// <returns>
/// A string representing the ERD in Mermaid syntax, which can be used to visualize the model.
/// </returns>
public string Render(EfModel model)
public string Render(EfModel model, DiagramOptions? options = null)
{
var sb = new StringBuilder();
sb.AppendLine("```mermaid");

if (!string.IsNullOrWhiteSpace(model.ContextName))
if ((options?.ShowTitle ?? true) && !string.IsNullOrWhiteSpace(model.ContextName))
{
sb.AppendLine("---");
sb.AppendLine($"title: {model.ContextName}");
Expand Down
6 changes: 4 additions & 2 deletions src/ProjGraph.Lib.ProjectGraph/Rendering/FlatGraphRenderer.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using ProjGraph.Core.Models;
using ProjGraph.Lib.Core.Abstractions;
using Spectre.Console;

namespace ProjGraph.Lib.ProjectGraph.Rendering;
Expand All @@ -12,17 +13,18 @@ public sealed class FlatGraphRenderer : SolutionGraphRendererBase
/// Renders a <see cref="SolutionGraph"/> as a flat list of projects and their direct dependencies.
/// </summary>
/// <param name="graph">The <see cref="SolutionGraph"/> to render.</param>
/// <param name="options">The options for rendering the diagram.</param>
/// <returns>A string representation of the solution graph rendered as a flat list.</returns>
/// <remarks>
/// This method renders each project in sorted order (by type and name) followed by its direct dependencies.
/// Projects involved in cyclic dependencies are highlighted in red. Cycle detection is performed using the
/// <see cref="SolutionGraphRendererBase.GetCyclicProjectIds"/> method.
/// </remarks>
public override string Render(SolutionGraph graph)
public override string Render(SolutionGraph graph, DiagramOptions? options = null)
{
_writer.GetStringBuilder().Clear();

RenderHeader(graph);
RenderHeader(graph, options);

var cyclicProjectIds = GetCyclicProjectIds(graph);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,14 @@ public sealed class MermaidGraphRenderer : IDiagramRenderer<SolutionGraph>
/// Renders a solution graph into a Mermaid.js graph definition.
/// </summary>
/// <param name="graph">The solution graph to render.</param>
/// <param name="options">The options for rendering the diagram.</param>
/// <returns>A string containing the Mermaid.js graph definition.</returns>
public string Render(SolutionGraph graph)
public string Render(SolutionGraph graph, DiagramOptions? options = null)
{
var sb = new StringBuilder();
sb.AppendLine("```mermaid");

if (!string.IsNullOrWhiteSpace(graph.Name))
if ((options?.ShowTitle ?? true) && !string.IsNullOrWhiteSpace(graph.Name))
{
sb.AppendLine("---");
sb.AppendLine($"title: {graph.Name}");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,23 @@ protected SolutionGraphRendererBase()
/// Renders the specified <see cref="SolutionGraph"/> to a string representation.
/// </summary>
/// <param name="graph">The solution graph to render.</param>
/// <param name="options">The options for rendering the diagram.</param>
/// <returns>A string representation of the rendered graph.</returns>
public abstract string Render(SolutionGraph graph);
public abstract string Render(SolutionGraph graph, DiagramOptions? options = null);

/// <summary>
/// Renders the header section of the solution graph visualization.
/// </summary>
/// <param name="graph">The solution graph to render the header for.</param>
protected void RenderHeader(SolutionGraph graph)
/// <param name="options">The options for rendering the diagram.</param>
protected void RenderHeader(SolutionGraph graph, DiagramOptions? options = null)
{
var graphName = Markup.Escape(graph.Name.Trim());
_console.Write(new Rule($"[yellow]Dependency Graph: {graphName}[/]") { Justification = Justify.Left });
if (options?.ShowTitle ?? true)
{
var graphName = Markup.Escape(graph.Name.Trim());
_console.Write(new Rule($"[yellow]Dependency Graph: {graphName}[/]") { Justification = Justify.Left });
}

_console.MarkupLine("[bold blue]Projects[/]");
}

Expand Down Expand Up @@ -86,9 +92,11 @@ protected void RenderCycleWarning(HashSet<Guid> cyclicProjectIds)
protected static HashSet<Guid> GetCyclicProjectIds(SolutionGraph graph)
{
var cycles = TarjanSccAlgorithm.FindStronglyConnectedComponents(graph);
return cycles
return
[
.. cycles
.Where(c => c.Count > 1)
.SelectMany(c => c)
.ToHashSet();
];
}
}
6 changes: 4 additions & 2 deletions src/ProjGraph.Lib.ProjectGraph/Rendering/TreeGraphRenderer.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using ProjGraph.Core.Models;
using ProjGraph.Lib.Core.Abstractions;
using Spectre.Console;

namespace ProjGraph.Lib.ProjectGraph.Rendering;
Expand All @@ -12,17 +13,18 @@ public sealed class TreeGraphRenderer : SolutionGraphRendererBase
/// Renders a <see cref="SolutionGraph"/> as a tree structure, displaying project dependencies hierarchically.
/// </summary>
/// <param name="graph">The <see cref="SolutionGraph"/> to render.</param>
/// <param name="options">The options for rendering the diagram.</param>
/// <returns>A string representation of the solution graph rendered as a tree.</returns>
/// <remarks>
/// This method identifies root projects (those with no incoming dependencies) and renders each as a separate tree branch.
/// Projects involved in cyclic dependencies are highlighted in red, and cycle detection is performed using the
/// <see cref="SolutionGraphRendererBase.GetCyclicProjectIds"/> method.
/// </remarks>
public override string Render(SolutionGraph graph)
public override string Render(SolutionGraph graph, DiagramOptions? options = null)
{
_writer.GetStringBuilder().Clear();

RenderHeader(graph);
RenderHeader(graph, options);

// Identify incoming dependency counts to find roots
var incomingCounts = graph.Projects.ToDictionary(p => p.Id, _ => 0);
Expand Down
Loading