diff --git a/docs/mdsource/doc-index.include.md b/docs/mdsource/doc-index.include.md
index 34428ced8..6a7580623 100644
--- a/docs/mdsource/doc-index.include.md
+++ b/docs/mdsource/doc-index.include.md
@@ -11,6 +11,7 @@
* [Guids](/docs/guids.md)
* [Dates](/docs/dates.md)
* [Scrubbing](/docs/scrubbers.md)
+ * [Solution Discovery](/docs/solution-discovery.md)
* [Counter](/docs/counter.md)
* [Members that throw](/docs/members-throw.md)
* [Ordering](/docs/ordering.md)
diff --git a/docs/mdsource/scrubbers.source.md b/docs/mdsource/scrubbers.source.md
index ed8734807..342c5f493 100644
--- a/docs/mdsource/scrubbers.source.md
+++ b/docs/mdsource/scrubbers.source.md
@@ -191,3 +191,4 @@ snippet: Verify.Xunit.Tests/Scrubbers/ScrubberLevelsSample.Usage.verified.txt
* [Guid behavior](guids.md)
* [Date behavior](dates.md)
* [Numeric Ids](numeric-ids.md)
+ * [Solution Discovery](solution-discovery.md)
diff --git a/docs/mdsource/solution-discovery.source.md b/docs/mdsource/solution-discovery.source.md
new file mode 100644
index 000000000..262fe7b10
--- /dev/null
+++ b/docs/mdsource/solution-discovery.source.md
@@ -0,0 +1,84 @@
+# Solution Discovery
+
+
+Verify automatically discovers solution information at build time and embeds it into the test assembly as metadata. This metadata is used to determine where to store snapshot files.
+
+
+## How It Works
+
+During the build process, Verify searches for solution files (`.slnx` or `.sln`) in the following locations (relative to the project directory):
+
+1. Project directory
+2. Parent directory
+3. Parent's parent directory
+
+If a single solution file is found, Verify extracts and stores:
+
+- `SolutionDir` - The directory containing the solution file
+- `SolutionName` - The solution file name without extension
+
+This information is embedded in the assembly as `AssemblyMetadataAttribute` values with keys:
+
+- `Verify.SolutionDirectory`
+- `Verify.SolutionName`
+
+
+## Preference Rules
+
+- If both `.slnx` and `.sln` files exist in the same directory, `.slnx` is preferred
+- If multiple `.slnx` files exist, Verify cannot auto-discover and will show a warning
+- If no `.slnx` files exist but multiple `.sln` files exist, Verify cannot auto-discover and will show a warning
+
+
+## Explicit Override
+
+Explicitly set solution information via MSBuild command-line properties:
+
+```bash
+dotnet build /p:SolutionDir="C:\Path\To\Solution\" /p:SolutionName="MySolution"
+```
+
+When `SolutionDir` is explicitly provided but `SolutionName` is not, Verify will attempt to derive `SolutionName` from the solution file found in the specified `SolutionDir`.
+
+
+## Warning Messages
+
+If Verify cannot discover solution information, it will emit build warnings with guidance:
+
+
+### Multiple Solution Files Found
+
+```
+Multiple solution files found. Unable to auto-discover SolutionDir and SolutionName.
+Found: C:\Path\Solution1.slnx;C:\Path\Solution2.slnx;C:\Path\Solution.sln.
+Verify searches for .slnx and .sln files in the project directory, parent directory, and parent's parent directory.
+To resolve this, either ensure only one solution file exists in these locations, or explicitly set SolutionDir and SolutionName via command line:
+/p:SolutionDir="C:\Path\To\Solution\" /p:SolutionName="MySolution"
+```
+
+
+### No Solution Files Found
+
+```
+No solution files found. Unable to auto-discover SolutionDir and SolutionName.
+Verify searches for .slnx and .sln files in the project directory '(C:\Path\To\Project)', parent directory, and parent's parent directory.
+To resolve this, either add a solution file to one of these locations, or explicitly set SolutionDir and SolutionName via command line:
+/p:SolutionDir="C:\Path\To\Solution\" /p:SolutionName="MySolution"
+```
+
+
+### Multiple Solution Files in Explicit SolutionDir
+
+```
+Multiple solution files found in SolutionDir 'C:\Path\To\Solution\'. Unable to auto-discover SolutionName.
+Found: C:\Path\To\Solution\Solution1.slnx;C:\Path\To\Solution\Solution2.sln.
+To resolve this, explicitly set SolutionName via command line:
+/p:SolutionName="MySolution"
+```
+
+
+## Build Integration
+
+Solution discovery happens automatically as part of the build via the `Verify.props` MSBuild targets file, which is automatically imported when referencing the Verify NuGet package.
+
+The discovery runs in the `DiscoverSolutionInfo` target, which executes before compilation, and the discovered values are written to generated source files by the `WriteVerifyAttributes` target.
diff --git a/docs/readme.md b/docs/readme.md
index dbf211f4c..f3f6426d6 100644
--- a/docs/readme.md
+++ b/docs/readme.md
@@ -20,6 +20,7 @@ To change this file edit the source file and then run MarkdownSnippets.
* [Guids](/docs/guids.md)
* [Dates](/docs/dates.md)
* [Scrubbing](/docs/scrubbers.md)
+ * [Solution Discovery](/docs/solution-discovery.md)
* [Counter](/docs/counter.md)
* [Members that throw](/docs/members-throw.md)
* [Ordering](/docs/ordering.md)
diff --git a/docs/scrubbers.md b/docs/scrubbers.md
index 54d7cd36f..97087b4c5 100644
--- a/docs/scrubbers.md
+++ b/docs/scrubbers.md
@@ -930,3 +930,4 @@ A B C
* [Guid behavior](guids.md)
* [Date behavior](dates.md)
* [Numeric Ids](numeric-ids.md)
+ * [Solution Discovery](solution-discovery.md)
diff --git a/docs/solution-discovery.md b/docs/solution-discovery.md
new file mode 100644
index 000000000..0025a3672
--- /dev/null
+++ b/docs/solution-discovery.md
@@ -0,0 +1,91 @@
+
+
+# Solution Discovery
+
+
+Verify automatically discovers solution information at build time and embeds it into the test assembly as metadata. This metadata is used to determine where to store snapshot files.
+
+
+## How It Works
+
+During the build process, Verify searches for solution files (`.slnx` or `.sln`) in the following locations (relative to the project directory):
+
+1. Project directory
+2. Parent directory
+3. Parent's parent directory
+
+If a single solution file is found, Verify extracts and stores:
+
+- `SolutionDir` - The directory containing the solution file
+- `SolutionName` - The solution file name without extension
+
+This information is embedded in the assembly as `AssemblyMetadataAttribute` values with keys:
+
+- `Verify.SolutionDirectory`
+- `Verify.SolutionName`
+
+
+## Preference Rules
+
+- If both `.slnx` and `.sln` files exist in the same directory, `.slnx` is preferred
+- If multiple `.slnx` files exist, Verify cannot auto-discover and will show a warning
+- If no `.slnx` files exist but multiple `.sln` files exist, Verify cannot auto-discover and will show a warning
+
+
+## Explicit Override
+
+Explicitly set solution information via MSBuild command-line properties:
+
+```bash
+dotnet build /p:SolutionDir="C:\Path\To\Solution\" /p:SolutionName="MySolution"
+```
+
+When `SolutionDir` is explicitly provided but `SolutionName` is not, Verify will attempt to derive `SolutionName` from the solution file found in the specified `SolutionDir`.
+
+
+## Warning Messages
+
+If Verify cannot discover solution information, it will emit build warnings with guidance:
+
+
+### Multiple Solution Files Found
+
+```
+Multiple solution files found. Unable to auto-discover SolutionDir and SolutionName.
+Found: C:\Path\Solution1.slnx;C:\Path\Solution2.slnx;C:\Path\Solution.sln.
+Verify searches for .slnx and .sln files in the project directory, parent directory, and parent's parent directory.
+To resolve this, either ensure only one solution file exists in these locations, or explicitly set SolutionDir and SolutionName via command line:
+/p:SolutionDir="C:\Path\To\Solution\" /p:SolutionName="MySolution"
+```
+
+
+### No Solution Files Found
+
+```
+No solution files found. Unable to auto-discover SolutionDir and SolutionName.
+Verify searches for .slnx and .sln files in the project directory '(C:\Path\To\Project)', parent directory, and parent's parent directory.
+To resolve this, either add a solution file to one of these locations, or explicitly set SolutionDir and SolutionName via command line:
+/p:SolutionDir="C:\Path\To\Solution\" /p:SolutionName="MySolution"
+```
+
+
+### Multiple Solution Files in Explicit SolutionDir
+
+```
+Multiple solution files found in SolutionDir 'C:\Path\To\Solution\'. Unable to auto-discover SolutionName.
+Found: C:\Path\To\Solution\Solution1.slnx;C:\Path\To\Solution\Solution2.sln.
+To resolve this, explicitly set SolutionName via command line:
+/p:SolutionName="MySolution"
+```
+
+
+## Build Integration
+
+Solution discovery happens automatically as part of the build via the `Verify.props` MSBuild targets file, which is automatically imported when referencing the Verify NuGet package.
+
+The discovery runs in the `DiscoverSolutionInfo` target, which executes before compilation, and the discovered values are written to generated source files by the `WriteVerifyAttributes` target.
diff --git a/readme.md b/readme.md
index 90e516425..c0c75fe70 100644
--- a/readme.md
+++ b/readme.md
@@ -1083,6 +1083,7 @@ To opt out of this feature, include the following in the project file:
* [Guids](/docs/guids.md)
* [Dates](/docs/dates.md)
* [Scrubbing](/docs/scrubbers.md)
+ * [Solution Discovery](/docs/solution-discovery.md)
* [Counter](/docs/counter.md)
* [Members that throw](/docs/members-throw.md)
* [Ordering](/docs/ordering.md)
diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props
index 733bda9c2..7bc0f262a 100644
--- a/src/Directory.Packages.props
+++ b/src/Directory.Packages.props
@@ -24,6 +24,7 @@
+
diff --git a/src/Verify.NUnit/Extensions.cs b/src/Verify.NUnit/Extensions.cs
index c9840d6ab..0a45ca1b6 100644
--- a/src/Verify.NUnit/Extensions.cs
+++ b/src/Verify.NUnit/Extensions.cs
@@ -37,7 +37,7 @@ public static bool TryGetParent(this TestAdapter adapter, [NotNullWhen(true)] ou
return methodParameterNames;
}
- var names = GetConstructorParameterNames(method.TypeInfo.Type, parent.Arguments.Length);
+ var names = method.TypeInfo.Type.GetConstructorParameterNames(parent.Arguments.Length);
if (methodParameterNames == null)
{
return names.ToList();
@@ -72,4 +72,4 @@ public static IEnumerable GetConstructorParameterNames(this Type type, i
return names;
}
-}
\ No newline at end of file
+}
diff --git a/src/Verify.Tests/GlobalUsings.cs b/src/Verify.Tests/GlobalUsings.cs
index 43e2f3dca..43922434b 100644
--- a/src/Verify.Tests/GlobalUsings.cs
+++ b/src/Verify.Tests/GlobalUsings.cs
@@ -7,4 +7,6 @@
global using EmptyFiles;
global using Polyfills;
global using System.Collections.ObjectModel;
+global using System.Reflection.Metadata;
+global using System.Reflection.PortableExecutable;
global using System.Security.Claims;
\ No newline at end of file
diff --git a/src/Verify.Tests/SolutionDiscoveryTests.cs b/src/Verify.Tests/SolutionDiscoveryTests.cs
new file mode 100644
index 000000000..6d4a59e9e
--- /dev/null
+++ b/src/Verify.Tests/SolutionDiscoveryTests.cs
@@ -0,0 +1,466 @@
+#if NET10_0
+public class SolutionDiscoveryTests
+{
+ [Fact]
+ public async Task SingleSlnxFile()
+ {
+ using var directory = new TempDirectory();
+ var tempDir = directory.Path;
+
+ // Create directory structure
+ var projectDir = Path.Combine(tempDir, "TestProject");
+ Directory.CreateDirectory(projectDir);
+
+ // Create .slnx file in same directory as project
+ var slnxPath = Path.Combine(tempDir, "TestSolution.slnx");
+ await File.WriteAllTextAsync(slnxPath, CreateMinimalSlnxContent());
+
+ // Create .csproj file
+ var csprojPath = Path.Combine(projectDir, "TestProject.csproj");
+ await File.WriteAllTextAsync(csprojPath, CreateMinimalCsprojContent());
+
+ // Build project
+ var (success, output) = await BuildProject(csprojPath);
+ Assert.True(success, $"Build failed: {output}");
+
+ // Load assembly and verify metadata
+ var assemblyPath = GetAssemblyPath(projectDir);
+ var (solutionDir, solutionName) = LoadAssemblyAndGetMetadata(assemblyPath);
+
+ Assert.Equal(tempDir + Path.DirectorySeparatorChar, solutionDir);
+ Assert.Equal("TestSolution", solutionName);
+ }
+
+ [Fact]
+ public async Task SingleSlnFile()
+ {
+ using var directory = new TempDirectory();
+ var tempDir = directory.Path;
+
+ // Create directory structure
+ var projectDir = Path.Combine(tempDir, "TestProject");
+ Directory.CreateDirectory(projectDir);
+
+ // Create .sln file in same directory as project
+ var slnPath = Path.Combine(tempDir, "TestSolution.sln");
+ await File.WriteAllTextAsync(slnPath, CreateMinimalSlnContent());
+
+ // Create .csproj file
+ var csprojPath = Path.Combine(projectDir, "TestProject.csproj");
+ await File.WriteAllTextAsync(csprojPath, CreateMinimalCsprojContent());
+
+ // Build project
+ var (success, output) = await BuildProject(csprojPath);
+ Assert.True(success, $"Build failed: {output}");
+
+ // Load assembly and verify metadata
+ var assemblyPath = GetAssemblyPath(projectDir);
+ var (solutionDir, solutionName) = LoadAssemblyAndGetMetadata(assemblyPath);
+
+ Assert.Equal(tempDir + Path.DirectorySeparatorChar, solutionDir);
+ Assert.Equal("TestSolution", solutionName);
+ }
+
+ [Fact]
+ public async Task BothSlnxAndSln_PreferSlnx()
+ {
+ using var directory = new TempDirectory();
+ var tempDir = directory.Path;
+
+ // Create directory structure
+ var projectDir = Path.Combine(tempDir, "TestProject");
+ Directory.CreateDirectory(projectDir);
+
+ // Create both .slnx and .sln files
+ var slnxPath = Path.Combine(tempDir, "PreferredSolution.slnx");
+ await File.WriteAllTextAsync(slnxPath, CreateMinimalSlnxContent());
+
+ var slnPath = Path.Combine(tempDir, "OtherSolution.sln");
+ await File.WriteAllTextAsync(slnPath, CreateMinimalSlnContent());
+
+ // Create .csproj file
+ var csprojPath = Path.Combine(projectDir, "TestProject.csproj");
+ await File.WriteAllTextAsync(csprojPath, CreateMinimalCsprojContent());
+
+ // Build project
+ var (success, output) = await BuildProject(csprojPath);
+ Assert.True(success, $"Build failed: {output}");
+
+ // Load assembly and verify metadata - should use .slnx
+ var assemblyPath = GetAssemblyPath(projectDir);
+ var (_, solutionName) = LoadAssemblyAndGetMetadata(assemblyPath);
+
+ // Should prefer .slnx file
+ Assert.Equal("PreferredSolution", solutionName);
+ }
+
+ [Fact]
+ public async Task MultipleSlnxFiles_ShowsWarning()
+ {
+ using var directory = new TempDirectory();
+ var tempDir = directory.Path;
+
+ // Create directory structure
+ var projectDir = Path.Combine(tempDir, "TestProject");
+ Directory.CreateDirectory(projectDir);
+
+ // Create multiple .slnx files
+ await File.WriteAllTextAsync(Path.Combine(tempDir, "Solution1.slnx"), CreateMinimalSlnxContent());
+ await File.WriteAllTextAsync(Path.Combine(tempDir, "Solution2.slnx"), CreateMinimalSlnxContent());
+
+ // Create .csproj file
+ var csprojPath = Path.Combine(projectDir, "TestProject.csproj");
+ await File.WriteAllTextAsync(csprojPath, CreateMinimalCsprojContent());
+
+ // Build project
+ var (success, output) = await BuildProject(csprojPath);
+ Assert.True(success, $"Build failed: {output}");
+
+ // Should contain warning about multiple solution files with helpful guidance
+ Assert.Contains("Multiple solution files found", output);
+ Assert.Contains("Verify searches for .slnx and .sln files", output);
+ Assert.Contains("/p:SolutionDir=", output);
+ Assert.Contains("/p:SolutionName=", output);
+
+ // Load assembly - should NOT have solution metadata
+ var assemblyPath = GetAssemblyPath(projectDir);
+ var (solutionDir, solutionName) = LoadAssemblyAndGetMetadata(assemblyPath);
+
+ Assert.Null(solutionDir);
+ Assert.Null(solutionName);
+ }
+
+ [Fact]
+ public async Task MultipleSlnFiles_ShowsWarning()
+ {
+ using var directory = new TempDirectory();
+ var tempDir = directory.Path;
+
+ // Create directory structure
+ var projectDir = Path.Combine(tempDir, "TestProject");
+ Directory.CreateDirectory(projectDir);
+
+ // Create multiple .sln files
+ await File.WriteAllTextAsync(Path.Combine(tempDir, "Solution1.sln"), CreateMinimalSlnContent());
+ await File.WriteAllTextAsync(Path.Combine(tempDir, "Solution2.sln"), CreateMinimalSlnContent());
+
+ // Create .csproj file
+ var csprojPath = Path.Combine(projectDir, "TestProject.csproj");
+ await File.WriteAllTextAsync(csprojPath, CreateMinimalCsprojContent());
+
+ // Build project
+ var (success, output) = await BuildProject(csprojPath);
+ Assert.True(success, $"Build failed: {output}");
+
+ // Should contain warning about multiple solution files with helpful guidance
+ Assert.Contains("Multiple solution files found", output);
+ Assert.Contains("Verify searches for .slnx and .sln files", output);
+ Assert.Contains("/p:SolutionDir=", output);
+ Assert.Contains("/p:SolutionName=", output);
+
+ // Load assembly - should NOT have solution metadata
+ var assemblyPath = GetAssemblyPath(projectDir);
+ var (solutionDir, solutionName) = LoadAssemblyAndGetMetadata(assemblyPath);
+
+ Assert.Null(solutionDir);
+ Assert.Null(solutionName);
+ }
+
+ [Fact]
+ public async Task SolutionInParentDirectory()
+ {
+ using var directory = new TempDirectory();
+ var tempDir = directory.Path;
+
+ // Create directory structure: tempDir/src/TestProject
+ var srcDir = Path.Combine(tempDir, "src");
+ var projectDir = Path.Combine(srcDir, "TestProject");
+ Directory.CreateDirectory(projectDir);
+
+ // Create .slnx file in parent directory
+ var slnxPath = Path.Combine(srcDir, "TestSolution.slnx");
+ await File.WriteAllTextAsync(slnxPath, CreateMinimalSlnxContent());
+
+ // Create .csproj file
+ var csprojPath = Path.Combine(projectDir, "TestProject.csproj");
+ await File.WriteAllTextAsync(csprojPath, CreateMinimalCsprojContent());
+
+ // Build project
+ var (success, output) = await BuildProject(csprojPath);
+ Assert.True(success, $"Build failed: {output}");
+
+ // Load assembly and verify metadata
+ var assemblyPath = GetAssemblyPath(projectDir);
+ var (solutionDir, solutionName) = LoadAssemblyAndGetMetadata(assemblyPath);
+
+ Assert.Equal(srcDir + Path.DirectorySeparatorChar, solutionDir);
+ Assert.Equal("TestSolution", solutionName);
+ }
+
+ [Fact]
+ public async Task SolutionInParentsParentDirectory()
+ {
+ using var directory = new TempDirectory();
+ var tempDir = directory.Path;
+
+ // Create directory structure: tempDir/src/SubDir/TestProject
+ var srcDir = Path.Combine(tempDir, "src");
+ var subDir = Path.Combine(srcDir, "SubDir");
+ var projectDir = Path.Combine(subDir, "TestProject");
+ Directory.CreateDirectory(projectDir);
+
+ // Create .slnx file in parent's parent directory
+ var slnxPath = Path.Combine(srcDir, "TestSolution.slnx");
+ await File.WriteAllTextAsync(slnxPath, CreateMinimalSlnxContent());
+
+ // Create .csproj file
+ var csprojPath = Path.Combine(projectDir, "TestProject.csproj");
+ await File.WriteAllTextAsync(csprojPath, CreateMinimalCsprojContent());
+
+ // Build project
+ var (success, output) = await BuildProject(csprojPath);
+ Assert.True(success, $"Build failed: {output}");
+
+ // Load assembly and verify metadata
+ var assemblyPath = GetAssemblyPath(projectDir);
+ var (solutionDir, solutionName) = LoadAssemblyAndGetMetadata(assemblyPath);
+
+ Assert.Equal(srcDir + Path.DirectorySeparatorChar, solutionDir);
+ Assert.Equal("TestSolution", solutionName);
+ }
+
+ [Fact]
+ public async Task NoSolutionFile_NoMetadata()
+ {
+ using var directory = new TempDirectory();
+ var tempDir = directory.Path;
+
+ // Create directory structure
+ var projectDir = Path.Combine(tempDir, "TestProject");
+ Directory.CreateDirectory(projectDir);
+
+ // Create .csproj file (no solution file)
+ var csprojPath = Path.Combine(projectDir, "TestProject.csproj");
+ await File.WriteAllTextAsync(csprojPath, CreateMinimalCsprojContent());
+
+ // Build project
+ var (success, output) = await BuildProject(csprojPath);
+ Assert.True(success, $"Build failed: {output}");
+
+ // Should contain warning about no solution files found
+ Assert.Contains("No solution files found", output);
+ Assert.Contains("Verify searches for .slnx and .sln files", output);
+ Assert.Contains("/p:SolutionDir=", output);
+ Assert.Contains("/p:SolutionName=", output);
+
+ // Load assembly - should NOT have solution metadata
+ var assemblyPath = GetAssemblyPath(projectDir);
+ var (solutionDir, solutionName) = LoadAssemblyAndGetMetadata(assemblyPath);
+
+ Assert.Null(solutionDir);
+ Assert.Null(solutionName);
+ }
+
+ [Fact]
+ public async Task ExplicitSolutionName_OverridesDiscovery()
+ {
+ using var directory = new TempDirectory();
+ var tempDir = directory.Path;
+
+ // Create directory structure
+ var projectDir = Path.Combine(tempDir, "TestProject");
+ Directory.CreateDirectory(projectDir);
+
+ // Create .slnx file (would normally be discovered)
+ var slnxPath = Path.Combine(tempDir, "DiscoveredSolution.slnx");
+ await File.WriteAllTextAsync(slnxPath, CreateMinimalSlnxContent());
+
+ // Create .csproj file
+ var csprojPath = Path.Combine(projectDir, "TestProject.csproj");
+ await File.WriteAllTextAsync(csprojPath, CreateMinimalCsprojContent());
+
+ // Build project with explicit SolutionName (and let SolutionDir be discovered normally)
+ var explicitSolutionName = "ExplicitSolution";
+ var (success, output) = await BuildProject(csprojPath, solutionName: explicitSolutionName);
+ Assert.True(success, $"Build failed: {output}");
+
+ // Load assembly - should use explicit SolutionName, but discovered SolutionDir
+ var assemblyPath = GetAssemblyPath(projectDir);
+ var (solutionDir, solutionName) = LoadAssemblyAndGetMetadata(assemblyPath);
+
+ // SolutionDir should still be discovered
+ Assert.Equal(tempDir + Path.DirectorySeparatorChar, solutionDir);
+ // But SolutionName should be the explicitly provided value
+ Assert.Equal(explicitSolutionName, solutionName);
+ }
+
+ static string CreateMinimalCsprojContent()
+ {
+ // Get the path to Verify.csproj and Verify.props relative to test project
+ var verifyProjectPath = Path.Combine(ProjectFiles.SolutionDirectory, "Verify", "Verify.csproj");
+
+ var verifyPropsPath = Path.Combine(ProjectFiles.SolutionDirectory, "Verify", "buildTransitive", "Verify.props");
+
+ return $"""
+
+
+ net10.0
+ Library
+ TestProject
+
+
+
+
+
+
+ """;
+ }
+
+ static string CreateMinimalSlnxContent() =>
+ """
+ {
+ "solution": {
+ "path": "TestSolution.slnx",
+ "version": "1.0"
+ }
+ }
+ """;
+
+ static string CreateMinimalSlnContent() =>
+ """
+ Microsoft Visual Studio Solution File, Format Version 12.00
+ VisualStudioVersion = 17.0.31903.59
+ MinimumVisualStudioVersion = 10.0.40219.1
+ Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ EndGlobal
+ """;
+
+ static async Task<(bool success, string output)> BuildProject(string csprojPath, string? solutionDir = null, string? solutionName = null)
+ {
+ var args = $"build \"{csprojPath}\" --configuration Release --verbosity normal";
+
+ if (solutionDir != null)
+ {
+ args += $" \"/p:SolutionDir={solutionDir}\"";
+ }
+
+ if (solutionName != null)
+ {
+ args += $" \"/p:SolutionName={solutionName}\"";
+ }
+
+ var startInfo = new ProcessStartInfo
+ {
+ FileName = "dotnet",
+ Arguments = args,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ UseShellExecute = false,
+ CreateNoWindow = true
+ };
+
+ using var process = Process.Start(startInfo)!;
+
+ var outputTask = process.StandardOutput.ReadToEndAsync();
+ var errorTask = process.StandardError.ReadToEndAsync();
+
+ await process.WaitForExitAsync();
+
+ var output = await outputTask;
+ var error = await errorTask;
+ var fullOutput = output + Environment.NewLine + error;
+
+ return (process.ExitCode == 0, fullOutput);
+ }
+
+ static string GetAssemblyPath(string projectDir)
+ {
+ var binPath = Path.Combine(projectDir, "bin", "Release", "net10.0");
+ var dllPath = Path.Combine(binPath, "TestProject.dll");
+
+ if (File.Exists(dllPath))
+ {
+ return dllPath;
+ }
+
+ throw new FileNotFoundException($"Assembly not found at {dllPath}");
+ }
+
+ static (string? solutionDir, string? solutionName) LoadAssemblyAndGetMetadata(string assemblyPath)
+ {
+ // Use MetadataReader to read assembly attributes without loading the assembly
+ using var fileStream = File.OpenRead(assemblyPath);
+ using var peReader = new PEReader(fileStream);
+ var metadataReader = peReader.GetMetadataReader();
+
+ string? solutionDir = null;
+ string? solutionName = null;
+
+ foreach (var attributeHandle in metadataReader.GetAssemblyDefinition().GetCustomAttributes())
+ {
+ var attribute = metadataReader.GetCustomAttribute(attributeHandle);
+
+ // Check if this is AssemblyMetadataAttribute
+ if (attribute.Constructor.Kind != HandleKind.MemberReference)
+ {
+ continue;
+ }
+
+ var constructor = metadataReader.GetMemberReference((MemberReferenceHandle)attribute.Constructor);
+ var attributeType = constructor.Parent;
+
+ if (attributeType.Kind != HandleKind.TypeReference)
+ {
+ continue;
+ }
+
+ var typeRef = metadataReader.GetTypeReference((TypeReferenceHandle)attributeType);
+ var typeName = metadataReader.GetString(typeRef.Name);
+ var typeNamespace = metadataReader.GetString(typeRef.Namespace);
+
+ if (typeName != "AssemblyMetadataAttribute" ||
+ typeNamespace != "System.Reflection")
+ {
+ continue;
+ }
+
+ var value = attribute.DecodeValue(new CustomAttributeTypeProvider());
+ if (value.FixedArguments.Length != 2)
+ {
+ continue;
+ }
+
+ var key = value.FixedArguments[0].Value as string;
+ var val = value.FixedArguments[1].Value as string;
+
+ if (key == "Verify.SolutionDirectory")
+ {
+ solutionDir = val;
+ }
+ else if (key == "Verify.SolutionName")
+ {
+ solutionName = val;
+ }
+ }
+
+ return (solutionDir, solutionName);
+ }
+
+ class CustomAttributeTypeProvider : ICustomAttributeTypeProvider