From 73fc8eb1b05fef3a5070372da52e2145a8a6ad9a Mon Sep 17 00:00:00 2001 From: HandyS11 Date: Tue, 24 Feb 2026 10:24:02 +0100 Subject: [PATCH 1/2] refactor: replace file system operations with DiagramOutputWriter in CLI commands --- .../Commands/ClassDiagramCommand.cs | 23 ++------- src/ProjGraph.Cli/Commands/ErdCommand.cs | 23 ++------- .../Commands/VisualizeCommand.cs | 23 ++------- .../Infrastructure/DiagramOutputWriter.cs | 50 +++++++++++++++++++ src/ProjGraph.Cli/Program.cs | 32 +++++++----- 5 files changed, 85 insertions(+), 66 deletions(-) create mode 100644 src/ProjGraph.Cli/Infrastructure/DiagramOutputWriter.cs diff --git a/src/ProjGraph.Cli/Commands/ClassDiagramCommand.cs b/src/ProjGraph.Cli/Commands/ClassDiagramCommand.cs index 0475be7..2bdc039 100644 --- a/src/ProjGraph.Cli/Commands/ClassDiagramCommand.cs +++ b/src/ProjGraph.Cli/Commands/ClassDiagramCommand.cs @@ -1,3 +1,4 @@ +using ProjGraph.Cli.Infrastructure; using ProjGraph.Core.Models; using ProjGraph.Lib.ClassDiagram.Application; using ProjGraph.Lib.Core.Abstractions; @@ -17,12 +18,12 @@ namespace ProjGraph.Cli.Commands; /// The class analysis service used to analyze C# files. /// The diagram renderer for producing Mermaid class diagram output. /// The output console for writing results and errors. -/// The file system abstraction for disk operations. +/// The helper for writing rendered output to file or console. internal sealed class ClassDiagramCommand( IClassAnalysisService analysisService, IDiagramRenderer mermaidRenderer, IOutputConsole console, - IFileSystem fileSystem) + DiagramOutputWriter outputWriter) : AsyncCommand { /// @@ -149,26 +150,12 @@ public override async Task ExecuteAsync( var model = await analysisService.AnalyzeFileAsync(settings.Path, options); - var wrapInMarkdownFence = settings.Output?.EndsWith(".mmd", StringComparison.OrdinalIgnoreCase) is false; + var wrapInMarkdownFence = DiagramOutputWriter.ShouldWrapInMarkdownFence(settings.Output); var mermaidOutput = mermaidRenderer.Render(model, new DiagramOptions(settings.ShowTitle, wrapInMarkdownFence)); - if (settings.Output is not null) - { - var directory = fileSystem.GetDirectoryName(settings.Output); - if (!string.IsNullOrEmpty(directory)) - { - fileSystem.CreateDirectory(directory); - } - - await fileSystem.WriteAllTextAsync(settings.Output, mermaidOutput, cancellationToken); - console.WriteInfo($"Saved to {settings.Output}"); - } - else - { - console.WriteLine(mermaidOutput); - } + await outputWriter.WriteAsync(mermaidOutput, settings.Output, cancellationToken); return 0; } diff --git a/src/ProjGraph.Cli/Commands/ErdCommand.cs b/src/ProjGraph.Cli/Commands/ErdCommand.cs index 2ea75f5..961b041 100644 --- a/src/ProjGraph.Cli/Commands/ErdCommand.cs +++ b/src/ProjGraph.Cli/Commands/ErdCommand.cs @@ -1,3 +1,4 @@ +using ProjGraph.Cli.Infrastructure; using ProjGraph.Core.Exceptions; using ProjGraph.Core.Models; using ProjGraph.Lib.Core.Abstractions; @@ -22,12 +23,12 @@ namespace ProjGraph.Cli.Commands; /// The Entity Framework analysis service for discovering and analyzing contexts and snapshots. /// The diagram renderer for producing Mermaid ERD output. /// The output console for writing results and errors. -/// The file system abstraction for disk operations. +/// The helper for writing rendered output to file or console. internal sealed class ErdCommand( IEfAnalysisService efService, IDiagramRenderer mermaidRenderer, IOutputConsole console, - IFileSystem fileSystem) + DiagramOutputWriter outputWriter) : AsyncCommand { /// @@ -130,26 +131,12 @@ public override async Task ExecuteAsync( var model = await AnalyzeModelAsync(targetPath, settings.ContextName, cancellationToken); - var wrapInMarkdownFence = settings.Output?.EndsWith(".mmd", StringComparison.OrdinalIgnoreCase) is false; + var wrapInMarkdownFence = DiagramOutputWriter.ShouldWrapInMarkdownFence(settings.Output); var mermaidOutput = mermaidRenderer.Render(model, new DiagramOptions(settings.ShowTitle, wrapInMarkdownFence)); - if (settings.Output is not null) - { - var directory = fileSystem.GetDirectoryName(settings.Output); - if (!string.IsNullOrEmpty(directory)) - { - fileSystem.CreateDirectory(directory); - } - - await fileSystem.WriteAllTextAsync(settings.Output, mermaidOutput, cancellationToken); - console.WriteInfo($"Saved to {settings.Output}"); - } - else - { - console.WriteLine(mermaidOutput); - } + await outputWriter.WriteAsync(mermaidOutput, settings.Output, cancellationToken); return 0; } diff --git a/src/ProjGraph.Cli/Commands/VisualizeCommand.cs b/src/ProjGraph.Cli/Commands/VisualizeCommand.cs index d172ac1..314581d 100644 --- a/src/ProjGraph.Cli/Commands/VisualizeCommand.cs +++ b/src/ProjGraph.Cli/Commands/VisualizeCommand.cs @@ -1,3 +1,4 @@ +using ProjGraph.Cli.Infrastructure; using ProjGraph.Core.Models; using ProjGraph.Lib.Core.Abstractions; using ProjGraph.Lib.ProjectGraph.Application; @@ -21,12 +22,12 @@ namespace ProjGraph.Cli.Commands; /// The graph service used to build the dependency graph. /// The collection of diagram renderers for different output formats. /// The output console for writing results and errors. -/// The file system abstraction for disk operations. +/// The helper for writing rendered output to file or console. internal sealed class VisualizeCommand( IGraphService graphService, IEnumerable> renderers, IOutputConsole console, - IFileSystem fileSystem) + DiagramOutputWriter outputWriter) : AsyncCommand { private const string FormatMermaid = "mermaid"; @@ -155,26 +156,12 @@ await Task.Run(() => graphService.BuildGraph(settings.Path), cancellationToken), graph = result; } - var wrapInMarkdownFence = settings.Output?.EndsWith(".mmd", StringComparison.OrdinalIgnoreCase) is false; + var wrapInMarkdownFence = DiagramOutputWriter.ShouldWrapInMarkdownFence(settings.Output); var rendered = GetRenderer(settings.NormalizedFormat) .Render(graph, new DiagramOptions(settings.ShowTitle, wrapInMarkdownFence)); - if (settings.Output is not null) - { - var directory = fileSystem.GetDirectoryName(settings.Output); - if (!string.IsNullOrEmpty(directory)) - { - fileSystem.CreateDirectory(directory); - } - - await fileSystem.WriteAllTextAsync(settings.Output, rendered, cancellationToken); - console.WriteInfo($"Saved to {settings.Output}"); - } - else - { - console.WriteLine(rendered); - } + await outputWriter.WriteAsync(rendered, settings.Output, cancellationToken); return 0; } diff --git a/src/ProjGraph.Cli/Infrastructure/DiagramOutputWriter.cs b/src/ProjGraph.Cli/Infrastructure/DiagramOutputWriter.cs new file mode 100644 index 0000000..9bc472e --- /dev/null +++ b/src/ProjGraph.Cli/Infrastructure/DiagramOutputWriter.cs @@ -0,0 +1,50 @@ +using ProjGraph.Lib.Core.Abstractions; + +namespace ProjGraph.Cli.Infrastructure; + +/// +/// Provides a reusable helper for writing rendered diagram output either to a file or to the console. +/// +/// The output console for writing results. +/// The file system abstraction for disk operations. +internal sealed class DiagramOutputWriter(IOutputConsole console, IFileSystem fileSystem) +{ + /// + /// Writes the rendered diagram to the specified output file, or to the console if no file path is given. + /// + /// The rendered diagram string to write. + /// + /// The optional file path to write to. When , the output is written to the console. + /// + /// A token to monitor for cancellation requests. + public async Task WriteAsync(string rendered, string? outputPath, CancellationToken cancellationToken) + { + if (outputPath is not null) + { + var directory = fileSystem.GetDirectoryName(outputPath); + if (!string.IsNullOrEmpty(directory)) + { + fileSystem.CreateDirectory(directory); + } + + await fileSystem.WriteAllTextAsync(outputPath, rendered, cancellationToken); + console.WriteInfo($"Saved to {outputPath}"); + } + else + { + console.WriteLine(rendered); + } + } + + /// + /// Determines whether the rendered output should be wrapped in a Markdown fence, + /// based on the output file extension. Files ending in .mmd are not wrapped. + /// When writing to the console (no output path), the output is wrapped. + /// + /// The optional output file path. + /// if the output should be wrapped in a Markdown fence; otherwise, . + public static bool ShouldWrapInMarkdownFence(string? outputPath) + { + return outputPath?.EndsWith(".mmd", StringComparison.OrdinalIgnoreCase) is not true; + } +} diff --git a/src/ProjGraph.Cli/Program.cs b/src/ProjGraph.Cli/Program.cs index 4823517..071432c 100644 --- a/src/ProjGraph.Cli/Program.cs +++ b/src/ProjGraph.Cli/Program.cs @@ -10,11 +10,18 @@ internal static class Program { public static int Main(string[] args) { + const string packageDiagramCommandName = "visualize"; + const string erdCommandName = "erd"; + const string classDiagramCommandName = "classdiagram"; + var services = new ServiceCollection(); // Register Library services services.AddProjGraphLib(); + // Register CLI-specific services + services.AddSingleton(); + var registrar = new TypeRegistrar(services); var app = new CommandApp(registrar); @@ -22,23 +29,24 @@ public static int Main(string[] args) { config.SetApplicationName("projgraph"); - config.AddCommand("visualize") + config.AddCommand(packageDiagramCommandName) .WithDescription("Visualize the dependency graph of a solution or project") - .WithExample("visualize", "MySolution.sln") - .WithExample("visualize", "MySolution.sln", "--format", "tree") - .WithExample("visualize", "MySolution.slnx", "--output", "graph.mmd"); + .WithExample(packageDiagramCommandName, "MySolution.sln") + .WithExample(packageDiagramCommandName, "MySolution.sln", "--format", "tree") + .WithExample(packageDiagramCommandName, "MySolution.slnx", "--output", "graph.mmd"); - config.AddCommand("erd") + config.AddCommand(erdCommandName) .WithDescription("Generate a Mermaid ERD for an Entity Framework Core DbContext") - .WithExample("erd", "Data/AppDbContext.cs") - .WithExample("erd", "Data/AppDbContext.cs", "--context", "BlogContext") - .WithExample("erd", "Data/AppDbContext.cs", "--output", "docs/erd.md"); + .WithExample(erdCommandName, "Data/AppDbContext.cs") + .WithExample(erdCommandName, "Data/AppDbContext.cs", "--context", "BlogContext") + .WithExample(erdCommandName, "Data/AppDbContext.cs", "--output", "docs/erd.md"); - config.AddCommand("classdiagram") + config.AddCommand(classDiagramCommandName) .WithDescription("Generate a Mermaid Class Diagram for a C# file") - .WithExample("classdiagram", "Services/UserService.cs") - .WithExample("classdiagram", "Models/User.cs", "--inheritance", "--dependencies", "--depth", "10") - .WithExample("classdiagram", "Models/User.cs", "--output", "user-hierarchy.mmd"); + .WithExample(classDiagramCommandName, "Services/UserService.cs") + .WithExample(classDiagramCommandName, "Models/User.cs", "--inheritance", "--dependencies", "--depth", + "10") + .WithExample(classDiagramCommandName, "Models/User.cs", "--output", "user-hierarchy.mmd"); }); return app.Run(args); From d22de3c4effeb8aab1b07da7edf66aeabea3d1d9 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Feb 2026 10:59:09 +0100 Subject: [PATCH 2/2] fix: register DiagramOutputWriter in CLI integration test helper (#39) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: HandyS11 <62420910+HandyS11@users.noreply.github.com> --- src/ProjGraph.Cli/Infrastructure/DiagramOutputWriter.cs | 2 +- tests/ProjGraph.Tests.Integration.Cli/Helpers/CliTestHelpers.cs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ProjGraph.Cli/Infrastructure/DiagramOutputWriter.cs b/src/ProjGraph.Cli/Infrastructure/DiagramOutputWriter.cs index 9bc472e..af82b48 100644 --- a/src/ProjGraph.Cli/Infrastructure/DiagramOutputWriter.cs +++ b/src/ProjGraph.Cli/Infrastructure/DiagramOutputWriter.cs @@ -1,4 +1,4 @@ -using ProjGraph.Lib.Core.Abstractions; +using ProjGraph.Lib.Core.Abstractions; namespace ProjGraph.Cli.Infrastructure; diff --git a/tests/ProjGraph.Tests.Integration.Cli/Helpers/CliTestHelpers.cs b/tests/ProjGraph.Tests.Integration.Cli/Helpers/CliTestHelpers.cs index 4a18b9e..929ae89 100644 --- a/tests/ProjGraph.Tests.Integration.Cli/Helpers/CliTestHelpers.cs +++ b/tests/ProjGraph.Tests.Integration.Cli/Helpers/CliTestHelpers.cs @@ -25,6 +25,7 @@ public static CommandApp CreateApp() { var services = new ServiceCollection(); services.AddProjGraphLib(); + services.AddSingleton(); var registrar = new TypeRegistrar(services); var app = new CommandApp(registrar);