From bcdece97b64f0d65bb4fe63123b64f57bbc7c01d Mon Sep 17 00:00:00 2001 From: Alexander Zekelin <128831809+zekelinAlex@users.noreply.github.com> Date: Thu, 17 Sep 2026 14:34:31 +0200 Subject: [PATCH] feat: auto-detect the solution root instead of trusting the wire parameter --- .../Scaffolding/ComponentScaffold.cs | 3 + .../Helpers/SolutionRootLocator.cs | 78 +++++++++++++++++++ .../SolutionRootLocatorTests.cs | 76 ++++++++++++++++++ 3 files changed, 157 insertions(+) create mode 100644 src/TALXIS.Platform.Metadata.Serialization.Xml/Scaffolding/Helpers/SolutionRootLocator.cs create mode 100644 tests/TALXIS.Platform.Metadata.Tests/SolutionRootLocatorTests.cs diff --git a/src/TALXIS.Platform.Metadata.Serialization.Xml/Scaffolding/ComponentScaffold.cs b/src/TALXIS.Platform.Metadata.Serialization.Xml/Scaffolding/ComponentScaffold.cs index c97ba2d..6cf54e0 100644 --- a/src/TALXIS.Platform.Metadata.Serialization.Xml/Scaffolding/ComponentScaffold.cs +++ b/src/TALXIS.Platform.Metadata.Serialization.Xml/Scaffolding/ComponentScaffold.cs @@ -43,6 +43,9 @@ public static ScaffoldResult Apply(ComponentScaffoldRequest request) throw new NotSupportedException( $"Component type '{request.ComponentType}' has no scaffold applier. Supported types: {string.Join(", ", Appliers.Keys)}."); } + // Uses the supplied path only when it identifies a solution root; otherwise + // auto-detects the root from the current directory. + request.SolutionRootPath = SolutionRootLocator.Resolve(request.SolutionRootPath, Directory.GetCurrentDirectory()); return applier(request); } diff --git a/src/TALXIS.Platform.Metadata.Serialization.Xml/Scaffolding/Helpers/SolutionRootLocator.cs b/src/TALXIS.Platform.Metadata.Serialization.Xml/Scaffolding/Helpers/SolutionRootLocator.cs new file mode 100644 index 0000000..cb7ca83 --- /dev/null +++ b/src/TALXIS.Platform.Metadata.Serialization.Xml/Scaffolding/Helpers/SolutionRootLocator.cs @@ -0,0 +1,78 @@ +using System.Xml; + +namespace TALXIS.Platform.Metadata.Serialization.Xml.Scaffolding; + +/// +/// Resolves the unpacked solution root for a scaffold request. If the supplied +/// path does not identify an existing directory, detects the root from the base +/// directory: Other/Solution.xml in the base itself, the SolutionRootPath property +/// of the single .csproj, or a unique Other/Solution.xml below the base. +/// +public static class SolutionRootLocator +{ + public static string Resolve(string? requested, string baseDirectory) + { + if (!string.IsNullOrWhiteSpace(requested)) + { + var requestedPath = Path.IsPathRooted(requested) ? requested : Path.Combine(baseDirectory, requested); + if (Directory.Exists(requestedPath)) return requested; + } + + if (IsSolutionRoot(baseDirectory)) return baseDirectory; + + var fromProject = FromProjectProperty(baseDirectory); + if (fromProject != null) return Path.Combine(baseDirectory, fromProject); + + return FromDirectorySearch(baseDirectory); + } + + private static bool IsSolutionRoot(string directory) => + File.Exists(Path.Combine(directory, "Other", "Solution.xml")); + + private static string? FromProjectProperty(string baseDirectory) + { + var projects = Directory.Exists(baseDirectory) + ? Directory.GetFiles(baseDirectory, "*.csproj", SearchOption.TopDirectoryOnly) + : Array.Empty(); + if (projects.Length > 1) + { + throw new InvalidOperationException( + $"Multiple .csproj files found in '{baseDirectory}': {string.Join(", ", projects.Select(Path.GetFileName))}"); + } + if (projects.Length == 0) return null; + + var doc = new XmlDocument(); + try + { + doc.Load(projects[0]); + } + catch (XmlException) + { + return null; + } + + var node = doc.SelectSingleNode( + "/*[local-name()='Project']/*[local-name()='PropertyGroup']/*[local-name()='SolutionRootPath']"); + var value = node?.InnerText.Trim(); + return string.IsNullOrWhiteSpace(value) ? null : value; + } + + private static string FromDirectorySearch(string baseDirectory) + { + var candidates = Directory.Exists(baseDirectory) + ? Directory.GetFiles(baseDirectory, "Solution.xml", SearchOption.AllDirectories) + .Where(path => string.Equals(Path.GetFileName(Path.GetDirectoryName(path)), "Other", StringComparison.OrdinalIgnoreCase)) + .ToArray() + : Array.Empty(); + + if (candidates.Length > 1) + { + throw new InvalidOperationException( + $"Multiple Other/Solution.xml files found under '{baseDirectory}':{Environment.NewLine}{string.Join(Environment.NewLine, candidates)}"); + } + if (candidates.Length == 0) + throw new InvalidOperationException($"Failed to determine SolutionRootPath from '{baseDirectory}'."); + + return Path.GetDirectoryName(Path.GetDirectoryName(candidates[0]))!; + } +} diff --git a/tests/TALXIS.Platform.Metadata.Tests/SolutionRootLocatorTests.cs b/tests/TALXIS.Platform.Metadata.Tests/SolutionRootLocatorTests.cs new file mode 100644 index 0000000..e1eeea1 --- /dev/null +++ b/tests/TALXIS.Platform.Metadata.Tests/SolutionRootLocatorTests.cs @@ -0,0 +1,76 @@ +using TALXIS.Platform.Metadata.Serialization.Xml.Scaffolding; + +namespace TALXIS.Platform.Metadata.Tests; + +public class SolutionRootLocatorTests : IDisposable +{ + private readonly string _base = Directory.CreateTempSubdirectory("metadata-solution-root-locator").FullName; + + public void Dispose() => Directory.Delete(_base, recursive: true); + + private void CreateSolutionRoot(params string[] segments) + { + var other = Path.Combine(Path.Combine(new[] { _base }.Concat(segments).ToArray()), "Other"); + Directory.CreateDirectory(other); + File.WriteAllText(Path.Combine(other, "Solution.xml"), ""); + } + + [Fact] + public void Resolve_ValidRequestedRoot_IsHonored() + { + CreateSolutionRoot("Declarations"); + + var resolved = SolutionRootLocator.Resolve("Declarations", _base); + + Assert.Equal("Declarations", resolved); + } + + [Fact] + public void Resolve_PlaceholderRequested_DetectsBaseDirectoryRoot() + { + CreateSolutionRoot(); + + var resolved = SolutionRootLocator.Resolve("__solution-root-path__", _base); + + Assert.Equal(_base, resolved); + } + + [Fact] + public void Resolve_ReadsCsprojProperty() + { + CreateSolutionRoot("Declarations", "Source"); + Directory.CreateDirectory(Path.Combine(_base, "Declarations", "Decoy")); + File.WriteAllText(Path.Combine(_base, "Sandbox.csproj"), """ + Declarations/Source + """); + + var resolved = SolutionRootLocator.Resolve(null, _base); + + Assert.Equal(Path.Combine(_base, "Declarations/Source"), resolved); + } + + [Fact] + public void Resolve_FallsBackToUniqueNestedRoot() + { + CreateSolutionRoot("Declarations"); + + var resolved = SolutionRootLocator.Resolve(null, _base); + + Assert.Equal(Path.Combine(_base, "Declarations"), resolved); + } + + [Fact] + public void Resolve_MultipleNestedRoots_Throws() + { + CreateSolutionRoot("A"); + CreateSolutionRoot("B"); + + Assert.Throws(() => SolutionRootLocator.Resolve(null, _base)); + } + + [Fact] + public void Resolve_NothingFound_Throws() + { + Assert.Throws(() => SolutionRootLocator.Resolve(null, _base)); + } +}