From 8b7db38106e9fc8eead8f970b27113525b23e173 Mon Sep 17 00:00:00 2001 From: Veronika Ovsyannikova Date: Fri, 12 Jun 2026 17:27:08 +0200 Subject: [PATCH 1/4] Migrate FindAssembliesWithReferencesTo --- agency.toml | 2 ++ .../Tasks/FindAssembliesWithReferencesTo.cs | 8 +++-- src/RazorSdk/Tasks/ReferenceResolver.cs | 33 ++++++++++++++----- 3 files changed, 33 insertions(+), 10 deletions(-) create mode 100644 agency.toml diff --git a/agency.toml b/agency.toml new file mode 100644 index 000000000000..a2cc13fc1ab2 --- /dev/null +++ b/agency.toml @@ -0,0 +1,2 @@ +[mcps.servers] +ms-docs = { tools = ["*"], type = "http", url = "https://learn.microsoft.com/api/mcp" } 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..db72cfbe9722 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,18 +16,32 @@ 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(); - Lookup = new Dictionary(StringComparer.Ordinal); - foreach (var item in assemblyItems) - { - Lookup[item.AssemblyName] = item; - } + Lookup = assemblyItems.ToDictionary( + item => item.AssemblyName, + StringComparer.Ordinal); } protected Dictionary Lookup { get; } @@ -103,12 +118,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 From 579d6cbff89374011e137e86ded251893402377f Mon Sep 17 00:00:00 2001 From: Veronika Ovsyannikova Date: Fri, 12 Jun 2026 17:37:08 +0200 Subject: [PATCH 2/4] Fixes --- agency.toml | 2 -- src/RazorSdk/Tasks/ReferenceResolver.cs | 8 +++++--- 2 files changed, 5 insertions(+), 5 deletions(-) delete mode 100644 agency.toml diff --git a/agency.toml b/agency.toml deleted file mode 100644 index a2cc13fc1ab2..000000000000 --- a/agency.toml +++ /dev/null @@ -1,2 +0,0 @@ -[mcps.servers] -ms-docs = { tools = ["*"], type = "http", url = "https://learn.microsoft.com/api/mcp" } diff --git a/src/RazorSdk/Tasks/ReferenceResolver.cs b/src/RazorSdk/Tasks/ReferenceResolver.cs index db72cfbe9722..c0f0357578f3 100644 --- a/src/RazorSdk/Tasks/ReferenceResolver.cs +++ b/src/RazorSdk/Tasks/ReferenceResolver.cs @@ -39,9 +39,11 @@ public ReferenceResolver( _assemblyItems = assemblyItems; _classifications = new Dictionary(); - Lookup = assemblyItems.ToDictionary( - item => item.AssemblyName, - StringComparer.Ordinal); + Lookup = new Dictionary(StringComparer.Ordinal); + foreach (var item in assemblyItems) + { + Lookup[item.AssemblyName] = item; + } } protected Dictionary Lookup { get; } From 80951c48fc9b76a84704d6bc27037b085841a4d3 Mon Sep 17 00:00:00 2001 From: Veronika Ovsyannikova Date: Mon, 15 Jun 2026 11:58:13 +0200 Subject: [PATCH 3/4] add tests --- ...bliesWithReferencesToMultiThreadingTest.cs | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 test/Microsoft.NET.Sdk.Razor.Tests/FindAssembliesWithReferencesToMultiThreadingTest.cs 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..85e6fb5a24f0 --- /dev/null +++ b/test/Microsoft.NET.Sdk.Razor.Tests/FindAssembliesWithReferencesToMultiThreadingTest.cs @@ -0,0 +1,111 @@ +// 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 TaskEnvironment CreateMultiThreadedEnvironment() => + TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(Path.GetTempPath()); + + 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 */ } + } + } + } +} From b2dfec163c2ca36384e9f3458ebeb1d2e7ecb762 Mon Sep 17 00:00:00 2001 From: Veronika Ovsyannikova Date: Mon, 15 Jun 2026 12:13:07 +0200 Subject: [PATCH 4/4] clean up --- .../FindAssembliesWithReferencesToMultiThreadingTest.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/Microsoft.NET.Sdk.Razor.Tests/FindAssembliesWithReferencesToMultiThreadingTest.cs b/test/Microsoft.NET.Sdk.Razor.Tests/FindAssembliesWithReferencesToMultiThreadingTest.cs index 85e6fb5a24f0..cf8c78416c2c 100644 --- a/test/Microsoft.NET.Sdk.Razor.Tests/FindAssembliesWithReferencesToMultiThreadingTest.cs +++ b/test/Microsoft.NET.Sdk.Razor.Tests/FindAssembliesWithReferencesToMultiThreadingTest.cs @@ -70,9 +70,6 @@ public void Execute_PassesTaskEnvironmentToResolver_AndResolvesRelativeAssemblyI "the file is present under TaskEnvironment.ProjectDirectory; no not-found warning should be raised"); } - private static TaskEnvironment CreateMultiThreadedEnvironment() => - TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(Path.GetTempPath()); - private static ExposingReferenceResolver CreateExposingResolver(TaskEnvironment env) => new(Array.Empty(), Array.Empty(), env);