diff --git a/src/RazorSdk/Tasks/FindAssembliesWithReferencesTo.cs b/src/RazorSdk/Tasks/FindAssembliesWithReferencesTo.cs
index 1eba7068e821..0c08fd266380 100644
--- a/src/RazorSdk/Tasks/FindAssembliesWithReferencesTo.cs
+++ b/src/RazorSdk/Tasks/FindAssembliesWithReferencesTo.cs
@@ -9,8 +9,12 @@
namespace Microsoft.AspNetCore.Razor.Tasks
{
- public class FindAssembliesWithReferencesTo : Task
+ [MSBuildMultiThreadableTask]
+ public class FindAssembliesWithReferencesTo : Task, IMultiThreadableTask
{
+ ///
+ public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback;
+
[Required]
public ITaskItem[] TargetAssemblyNames { get; set; }
@@ -44,7 +48,7 @@ public override bool Execute()
var targetAssemblyNames = TargetAssemblyNames.Select(s => s.ItemSpec).ToList();
- var provider = new ReferenceResolver((IReadOnlyList)targetAssemblyNames, referenceItems);
+ var provider = new ReferenceResolver((IReadOnlyList)targetAssemblyNames, referenceItems, TaskEnvironment);
try
{
var assemblyNames = provider.ResolveAssemblies();
diff --git a/src/RazorSdk/Tasks/ReferenceResolver.cs b/src/RazorSdk/Tasks/ReferenceResolver.cs
index 597c43931f68..c0f0357578f3 100644
--- a/src/RazorSdk/Tasks/ReferenceResolver.cs
+++ b/src/RazorSdk/Tasks/ReferenceResolver.cs
@@ -4,6 +4,7 @@
using System.Diagnostics;
using System.Reflection.Metadata;
using System.Reflection.PortableExecutable;
+using Microsoft.Build.Framework;
namespace Microsoft.AspNetCore.Razor.Tasks
{
@@ -15,9 +16,25 @@ public class ReferenceResolver
private readonly HashSet _mvcAssemblies;
private readonly IReadOnlyList _assemblyItems;
private readonly Dictionary _classifications;
+ private readonly TaskEnvironment _taskEnvironment = TaskEnvironment.Fallback;
- public ReferenceResolver(IReadOnlyList targetAssemblies, IReadOnlyList assemblyItems)
+ public ReferenceResolver(
+ IReadOnlyList targetAssemblies,
+ IReadOnlyList assemblyItems)
+ : this(targetAssemblies, assemblyItems, null)
{
+ }
+
+ public ReferenceResolver(
+ IReadOnlyList targetAssemblies,
+ IReadOnlyList assemblyItems,
+ TaskEnvironment? taskEnvironment)
+ {
+ if (taskEnvironment != null)
+ {
+ _taskEnvironment = taskEnvironment;
+ }
+
_mvcAssemblies = new HashSet(targetAssemblies, StringComparer.Ordinal);
_assemblyItems = assemblyItems;
_classifications = new Dictionary();
@@ -103,12 +120,14 @@ protected virtual IReadOnlyList GetReferences(string file)
{
try
{
- if (!File.Exists(file))
+ var absoluteFilePath = !String.IsNullOrEmpty(file) ? _taskEnvironment.GetAbsolutePath(file) : file;
+
+ if (!File.Exists(absoluteFilePath))
{
throw new ReferenceAssemblyNotFoundException(file);
}
- using var peReader = new PEReader(File.OpenRead(file));
+ using var peReader = new PEReader(File.OpenRead(absoluteFilePath));
if (!peReader.HasMetadata)
{
return Array.Empty(); // not a managed assembly
diff --git a/test/Microsoft.NET.Sdk.Razor.Tests/FindAssembliesWithReferencesToMultiThreadingTest.cs b/test/Microsoft.NET.Sdk.Razor.Tests/FindAssembliesWithReferencesToMultiThreadingTest.cs
new file mode 100644
index 000000000000..cf8c78416c2c
--- /dev/null
+++ b/test/Microsoft.NET.Sdk.Razor.Tests/FindAssembliesWithReferencesToMultiThreadingTest.cs
@@ -0,0 +1,108 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#nullable disable
+
+using System.Reflection;
+using Microsoft.AspNetCore.Razor.Tasks;
+using Microsoft.Build.Framework;
+using Moq;
+using TaskItem = Microsoft.Build.Utilities.TaskItem;
+
+namespace Microsoft.NET.Sdk.Razor.Test
+{
+ public class FindAssembliesWithReferencesToMultiThreadingTest
+ {
+ [Fact]
+ public void GetReferences_RelativePath_ResolvesAgainstTaskEnvironmentProjectDirectory()
+ {
+ // The point of the migration: relative paths must be absolutized against
+ // TaskEnvironment.ProjectDirectory instead of the process CWD. The file is placed
+ // in a temp directory distinct from CWD so pre-migration code would not find it.
+ using var temp = new TempDirectory();
+ const string fileName = "stub.dll";
+ File.WriteAllBytes(Path.Combine(temp.Path, fileName), new byte[] { 0 });
+ Directory.GetCurrentDirectory().Should().NotBe(temp.Path,
+ "test must run with CWD distinct from the temp dir so the migration is actually exercised");
+
+ var resolver = CreateExposingResolver(TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(temp.Path));
+
+ resolver.CallGetReferences(fileName).Should().BeEmpty(
+ "the file exists at TaskEnvironment.ProjectDirectory; PEReader's BadImageFormatException is swallowed and an empty list returned");
+ }
+
+
+ [Fact]
+ public void Execute_PassesTaskEnvironmentToResolver_AndResolvesRelativeAssemblyItemSpecs()
+ {
+ // End-to-end wiring test: FindAssembliesWithReferencesTo must forward its
+ // TaskEnvironment to the ReferenceResolver so relative ItemSpecs on Assemblies
+ // are resolved against the project directory, not the process CWD.
+ using var temp = new TempDirectory();
+ const string fileName = "candidate.dll";
+ File.WriteAllBytes(Path.Combine(temp.Path, fileName), new byte[] { 0 });
+
+ var warnings = new List();
+ var errors = new List();
+ var buildEngine = new Mock();
+ buildEngine.Setup(e => e.LogWarningEvent(It.IsAny()))
+ .Callback(warnings.Add);
+ buildEngine.Setup(e => e.LogErrorEvent(It.IsAny()))
+ .Callback(errors.Add);
+
+ var task = new FindAssembliesWithReferencesTo
+ {
+ BuildEngine = buildEngine.Object,
+ TaskEnvironment = TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(temp.Path),
+ TargetAssemblyNames = new ITaskItem[] { new TaskItem("Microsoft.AspNetCore.Mvc") },
+ Assemblies = new ITaskItem[]
+ {
+ new TaskItem(fileName, new Dictionary
+ {
+ ["FusionName"] = "Candidate, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
+ }),
+ },
+ };
+
+ task.Execute().Should().BeTrue();
+ errors.Should().BeEmpty();
+ warnings.Should().NotContain(w => w.Code == "RAZORSDK1007",
+ "the file is present under TaskEnvironment.ProjectDirectory; no not-found warning should be raised");
+ }
+
+ private static ExposingReferenceResolver CreateExposingResolver(TaskEnvironment env) =>
+ new(Array.Empty(), Array.Empty(), env);
+
+ private sealed class ExposingReferenceResolver : ReferenceResolver
+ {
+ public ExposingReferenceResolver(
+ IReadOnlyList targetAssemblies,
+ IReadOnlyList assemblyItems,
+ TaskEnvironment taskEnvironment)
+ : base(targetAssemblies, assemblyItems, taskEnvironment)
+ {
+ }
+
+ public IReadOnlyList CallGetReferences(string file) => GetReferences(file);
+ }
+
+ private sealed class TempDirectory : IDisposable
+ {
+ public TempDirectory()
+ {
+ Path = System.IO.Path.Combine(
+ System.IO.Path.GetTempPath(),
+ nameof(FindAssembliesWithReferencesToMultiThreadingTest),
+ Guid.NewGuid().ToString("N"));
+ Directory.CreateDirectory(Path);
+ }
+
+ public string Path { get; }
+
+ public void Dispose()
+ {
+ try { Directory.Delete(Path, recursive: true); } catch { /* best effort */ }
+ }
+ }
+ }
+}