diff --git a/src/Framework.UnitTests/AbsolutePath_Tests.cs b/src/Framework.UnitTests/AbsolutePath_Tests.cs
index 40a93551385..ef2ac95a9d1 100644
--- a/src/Framework.UnitTests/AbsolutePath_Tests.cs
+++ b/src/Framework.UnitTests/AbsolutePath_Tests.cs
@@ -219,6 +219,50 @@ public void AbsolutePath_UnixPathValidation_ShouldAcceptOnlyTrueAbsolutePaths(st
ValidatePathAcceptance(path, shouldBeAccepted);
}
+ [Fact]
+ public void GetCanonicalForm_NullPath_ShouldReturnSameInstance()
+ {
+ var absolutePath = new AbsolutePath(null!, null!, ignoreRootedCheck: true);
+ var result = absolutePath.GetCanonicalForm();
+
+ // Should return the same struct values when no normalization is needed
+ result.ShouldBe(absolutePath);
+ }
+
+
+ [WindowsOnlyTheory]
+ [InlineData("C:\\foo\\.\\bar")] // Current directory reference
+ [InlineData("C:\\foo\\..\\bar")] // Parent directory reference
+ [InlineData("C:\\foo/bar")] // Forward slash to backslash
+ [InlineData("C:\\foo\\bar")] // Simple Windows path (no normalization needed)
+ public void GetCanonicalForm_WindowsPathNormalization_ShouldMatchPathGetFullPath(string inputPath)
+ {
+ ValidateGetCanonicalFormMatchesSystem(inputPath);
+ }
+
+ [UnixOnlyTheory]
+ [InlineData("/foo/./bar")] // Current directory reference
+ [InlineData("/foo/../bar")] // Parent directory reference
+ [InlineData("/foo/bar")] // Simple Unix path (no normalization needed)
+ [InlineData("/foo/bar\\baz")] // Simple Unix path with backslash that is not a path separator (no normalization needed)
+ public void GetCanonicalForm_UnixPathNormalization_ShouldMatchPathGetFullPath(string inputPath)
+ {
+ ValidateGetCanonicalFormMatchesSystem(inputPath);
+ }
+
+ private static void ValidateGetCanonicalFormMatchesSystem(string inputPath)
+ {
+ var absolutePath = new AbsolutePath(inputPath, ignoreRootedCheck: true);
+ var result = absolutePath.GetCanonicalForm();
+ var systemResult = Path.GetFullPath(inputPath);
+
+ // Should match Path.GetFullPath behavior exactly
+ result.Value.ShouldBe(systemResult);
+
+ // Should preserve original value
+ result.OriginalValue.ShouldBe(absolutePath.OriginalValue);
+ }
+
[WindowsOnlyFact]
[UseInvariantCulture]
public void AbsolutePath_NotRooted_ShouldThrowWithLocalizedMessage()
diff --git a/src/Framework/PathHelpers/AbsolutePath.cs b/src/Framework/PathHelpers/AbsolutePath.cs
index ca7d91598af..b8fe5a57813 100644
--- a/src/Framework/PathHelpers/AbsolutePath.cs
+++ b/src/Framework/PathHelpers/AbsolutePath.cs
@@ -125,6 +125,59 @@ public AbsolutePath(string path, AbsolutePath basePath)
/// The path to convert.
public static implicit operator string(AbsolutePath path) => path.Value;
+ ///
+ /// Returns the canonical form of this path.
+ ///
+ ///
+ /// An representing the canonical form of the path.
+ ///
+ ///
+ ///
+ /// The canonical form of a path is exactly what would produce,
+ /// with the following properties:
+ ///
+ /// - All relative path segments ("." and "..") are resolved.
+ /// - Directory separators are normalized to the platform convention (backslash on Windows).
+ ///
+ ///
+ ///
+ /// If the path is already in canonical form, returns the current instance to avoid unnecessary allocations.
+ /// Preserves the OriginalValue of the current instance.
+ ///
+ ///
+ internal AbsolutePath GetCanonicalForm()
+ {
+ if (string.IsNullOrEmpty(Value))
+ {
+ return this;
+ }
+
+
+ // Note: this is a quick check to avoid calling Path.GetFullPath when it's not necessary, since it can be expensive.
+ // It should cover the most common cases and avoid the overhead of Path.GetFullPath in those cases.
+
+ // Check for relative path segments "." and ".."
+ // In absolute path those segments can not appear in the beginning of the path, only after a path separator.
+ // This is not a precise full detection of relative segments. There is no false negatives as this might affect correctenes, but it may have false positives:
+ // like when there is a hidden file or directory starting with a dot, or on linux the backslash and dot can be part of the file name.
+ // In case of false positives we would call Path.GetFullPath and the result would still be correct.
+
+ bool hasRelativeSegment = Value.Contains("/.") || Value.Contains("\\.");
+
+ // Check if directory separator normalization is required (only on Windows: "/" to "\")
+ // On unix "\" is not a valid path separator, but is a part of the file/directory name, so no normalization is needed.
+ bool needsSeparatorNormalization = NativeMethods.IsWindows && Value.IndexOf(Path.AltDirectorySeparatorChar) >= 0;
+
+ if (!hasRelativeSegment && !needsSeparatorNormalization)
+ {
+ return this;
+ }
+
+ // Use Path.GetFullPath to resolve relative segments and normalize separators.
+ // Skip validation since Path.GetFullPath already ensures the result is absolute.
+ return new AbsolutePath(Path.GetFullPath(Value), OriginalValue, ignoreRootedCheck: true);
+ }
+
///
/// Determines whether two instances are equal.
///