Skip to content

Multithreading migration: Group 7 — 4 attribute-only + 2 Pattern B tasks#53118

Draft
SimaTian wants to merge 13 commits intodotnet:mainfrom
SimaTian:merge-group-7
Draft

Multithreading migration: Group 7 — 4 attribute-only + 2 Pattern B tasks#53118
SimaTian wants to merge 13 commits intodotnet:mainfrom
SimaTian:merge-group-7

Conversation

@SimaTian
Copy link
Member

Summary

Migrate 5 tasks to support multithreaded MSBuild execution. Three tasks are attribute-only (Pattern A), two require interface-based migration (Pattern B).

Tasks Migrated

Task Pattern Key Changes
SetGeneratedAppConfigMetadata A Attribute added; behavioral tests
ValidateExecutableReferences A Attribute added; behavioral tests
RemoveDuplicatePackageReferences A Attribute already on main; tests added
SelectRuntimeIdentifierSpecificItems B Added IMultiThreadableTask, TaskEnvironment; absolutized RuntimeIdentifierGraphPath
FilterResolvedFiles B Added IMultiThreadableTask, TaskEnvironment; absolutized AssetsFilePath

Pattern B Details

SelectRuntimeIdentifierSpecificItems: Calls RuntimeGraphCache.GetRuntimeGraph(RuntimeIdentifierGraphPath) which enforces Path.IsPathRooted() and performs file I/O via JsonRuntimeFormat.ReadRuntimeGraph(). RuntimeIdentifierGraphPath is absolutized via TaskEnvironment.GetAbsolutePath() before the call.

FilterResolvedFiles: Calls LockFileCache.GetLockFile(AssetsFilePath) which requires rooted paths. AssetsFilePath is absolutized before the call.

Tests Added

  • GivenAttributeOnlyTasksGroup7.cs — attribute-presence tests, behavioral tests, parity tests, concurrent execution tests
  • GivenAFilterResolvedFilesMultiThreading.cs — CWD-independence parity test, path resolution test

Files Changed

  • src/Tasks/Microsoft.NET.Build.Tasks/SelectRuntimeIdentifierSpecificItems.cs
  • src/Tasks/Microsoft.NET.Build.Tasks/SetGeneratedAppConfigMetadata.cs
  • src/Tasks/Microsoft.NET.Build.Tasks/ValidateExecutableReferences.cs
  • src/Tasks/Microsoft.NET.Build.Tasks/FilterResolvedFiles.cs
  • src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAttributeOnlyTasksGroup7.cs (new)
  • src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAFilterResolvedFilesMultiThreading.cs (new)

SimaTian and others added 5 commits February 22, 2026 16:29
Tasks: SelectRuntimeIdentifierSpecificItems, SetGeneratedAppConfigMetadata, ValidateExecutableReferences, FilterResolvedFiles, RemoveDuplicatePackageReferences.

FilterResolvedFiles includes absolutization of AssetsFilePath. Others are attribute-only with test additions.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… (Group 7)

Add [MSBuildMultiThreadableTask] attribute verification for 4 attribute-only
tasks plus FilterResolvedFiles. Replace reflection-based TaskEnvironment
assignment with direct property access. Add parity test for FilterResolvedFiles
verifying CWD-independent behavior.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…file

The test was misplaced (FilterResolvedFiles is Pattern B, not attribute-only)
and had a latent NullReferenceException bug due to missing TaskEnvironment
assignment. The correct version exists in GivenAFilterResolvedFilesMultiThreading.cs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Task calls RuntimeGraphCache.GetRuntimeGraph(RuntimeIdentifierGraphPath) which
does file I/O (JsonRuntimeFormat.ReadRuntimeGraph). The path must be absolutized
via TaskEnvironment for multithreading safety.

Changes:
- Add IMultiThreadableTask interface and TaskEnvironment property
- Absolutize RuntimeIdentifierGraphPath before passing to RuntimeGraphCache
- Update tests to provide TaskEnvironment via TaskEnvironmentHelper.CreateForTest

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings February 22, 2026 20:01
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Migrates a set of Microsoft.NET.Build.Tasks MSBuild tasks to support multithreaded execution by adding the multithreadable task attribute and, for Pattern B tasks, implementing IMultiThreadableTask and resolving file paths via TaskEnvironment.

Changes:

  • Added [MSBuildMultiThreadableTask] to attribute-only tasks (SetGeneratedAppConfigMetadata, ValidateExecutableReferences).
  • Updated Pattern B tasks (SelectRuntimeIdentifierSpecificItems, FilterResolvedFiles) to implement IMultiThreadableTask and resolve input file paths using TaskEnvironment.GetAbsolutePath.
  • Added new unit tests validating attribute presence, CWD-independence/parity, and concurrent execution behavior.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/Tasks/Microsoft.NET.Build.Tasks/ValidateExecutableReferences.cs Marks the task as multithreadable (attribute-only).
src/Tasks/Microsoft.NET.Build.Tasks/SetGeneratedAppConfigMetadata.cs Marks the task as multithreadable (attribute-only).
src/Tasks/Microsoft.NET.Build.Tasks/SelectRuntimeIdentifierSpecificItems.cs Implements IMultiThreadableTask and resolves RuntimeIdentifierGraphPath via TaskEnvironment.
src/Tasks/Microsoft.NET.Build.Tasks/FilterResolvedFiles.cs Implements IMultiThreadableTask and resolves AssetsFilePath via TaskEnvironment.
src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAttributeOnlyTasksGroup7.cs Adds attribute presence, behavioral/parity, and concurrency tests for the migrated tasks.
src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAFilterResolvedFilesMultiThreading.cs Adds tests for FilterResolvedFiles attribute presence, path resolution, parity, and concurrency.

Comment on lines +49 to +56
{
BuildEngine = new MockBuildEngine(),
AssetsFilePath = "obj\\project.assets.json",
ResolvedFiles = Array.Empty<ITaskItem>(),
PackagesToPrune = Array.Empty<ITaskItem>(),
TargetFramework = ".NETCoreApp,Version=v8.0",
TaskEnvironment = TaskEnvironmentHelper.CreateForTest(projectDir),
};
Copy link

Copilot AI Feb 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AssetsFilePath is set to a Windows-specific relative string ("obj\project.assets.json"). On non-Windows this becomes a filename containing a backslash, so TaskEnvironment.GetAbsolutePath will resolve to a path that doesn’t exist and the test will fail. Use Path.Combine("obj", "project.assets.json") (or "obj/project.assets.json") so the test is OS-agnostic.

Copilot uses AI. Check for mistakes.
TaskEnvironment = TaskEnvironmentHelper.CreateForTest(projectDir),
};
barrier.SignalAndWait();
task.Execute();
Copy link

Copilot AI Feb 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The concurrent execution test ignores the return value from task.Execute(). If the task starts logging errors (and returns false) without throwing, this test would still pass. Capture/assert the result (and optionally assert no logged errors) so the test actually validates successful concurrent execution.

Suggested change
task.Execute();
var result = task.Execute();
if (!result)
{
errors.Add($"Thread {i}: task.Execute returned false");
}

Copilot uses AI. Check for mistakes.
Comment on lines +371 to +380
TargetRuntimeIdentifier = "linux-x64",
Items = new ITaskItem[]
{
CreateItemWithRid($"Item{i}", "linux-x64")
},
RuntimeIdentifierGraphPath = ""
};
barrier.SignalAndWait();
task.Execute();
}
Copy link

Copilot AI Feb 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RuntimeIdentifierGraphPath is set to an empty string in this test. TaskEnvironmentHelper ultimately constructs an AbsolutePath and will throw for null/empty paths, so this concurrent execution test will consistently fail. Create/write a real runtime graph file (like the earlier parity test) and pass its path here.

Copilot uses AI. Check for mistakes.
Comment on lines +389 to +462
public void SetGeneratedAppConfigMetadata_ConcurrentExecution(int parallelism)
{
var errors = new ConcurrentBag<string>();
var barrier = new Barrier(parallelism);
Parallel.For(0, parallelism, new ParallelOptions { MaxDegreeOfParallelism = parallelism }, i =>
{
try
{
var task = new SetGeneratedAppConfigMetadata
{
BuildEngine = new MockBuildEngine(),
GeneratedAppConfigFile = $"obj/app{i}.exe.config",
TargetName = $"app{i}.exe.config"
};
barrier.SignalAndWait();
task.Execute();
}
catch (Exception ex) { errors.Add($"Thread {i}: {ex.Message}"); }
});
errors.Should().BeEmpty();
}

[Theory]
[InlineData(4)]
[InlineData(16)]
public void ValidateExecutableReferences_ConcurrentExecution(int parallelism)
{
var errors = new ConcurrentBag<string>();
var barrier = new Barrier(parallelism);
Parallel.For(0, parallelism, new ParallelOptions { MaxDegreeOfParallelism = parallelism }, i =>
{
try
{
var task = new ValidateExecutableReferences
{
BuildEngine = new MockBuildEngine(),
IsExecutable = false,
SelfContained = false,
ReferencedProjects = Array.Empty<ITaskItem>()
};
barrier.SignalAndWait();
task.Execute();
}
catch (Exception ex) { errors.Add($"Thread {i}: {ex.Message}"); }
});
errors.Should().BeEmpty();
}

[Theory]
[InlineData(4)]
[InlineData(16)]
public void RemoveDuplicatePackageReferences_ConcurrentExecution(int parallelism)
{
var errors = new ConcurrentBag<string>();
var barrier = new Barrier(parallelism);
Parallel.For(0, parallelism, new ParallelOptions { MaxDegreeOfParallelism = parallelism }, i =>
{
try
{
var task = new RemoveDuplicatePackageReferences
{
BuildEngine = new MockBuildEngine(),
InputPackageReferences = new ITaskItem[]
{
new MockTaskItem($"Package{i}", new Dictionary<string, string> { { "Version", "1.0.0" } })
}
};
barrier.SignalAndWait();
task.Execute();
}
catch (Exception ex) { errors.Add($"Thread {i}: {ex.Message}"); }
});
errors.Should().BeEmpty();
}
Copy link

Copilot AI Feb 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These concurrent execution tests call task.Execute() but don’t assert the returned value. If a task logs errors and returns false (without throwing), the test would still pass and mask failures. Assert Execute() returns true (and/or validate the mock build engine has no errors) inside the loop.

Copilot uses AI. Check for mistakes.
Comment on lines 55 to 59
protected override void ExecuteCore()
{
var lockFileCache = new LockFileCache(this);
LockFile lockFile = lockFileCache.GetLockFile(AssetsFilePath);
LockFile lockFile = lockFileCache.GetLockFile(TaskEnvironment.GetAbsolutePath(AssetsFilePath));

Copy link

Copilot AI Feb 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ExecuteCore unconditionally dereferences TaskEnvironment and calls GetAbsolutePath. If TaskEnvironment isn’t injected (e.g., task instantiated outside MSBuild, or an older host that doesn’t set it), this becomes a NullReferenceException that rethrows from TaskBase.Execute() rather than a logged build error. Consider guarding against null (and falling back to the original AssetsFilePath / logging a clear error) to avoid hard crashes.

Copilot uses AI. Check for mistakes.
… fix

- Replace Barrier+Parallel.For with ManualResetEventSlim+Task.Run
- Use async test methods with Task.WhenAll
- Replace Windows path separator with Path.Combine
- Fix empty RuntimeIdentifierGraphPath in concurrent test
- Assert Execute() return value in concurrent tests

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@SimaTian SimaTian self-assigned this Feb 23, 2026
SimaTian and others added 2 commits February 23, 2026 17:12
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add TaskEnvironmentDefaults.cs for NETFRAMEWORK lazy-init fallback
- Apply lazy-init pattern to FilterResolvedFiles

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The combining constructor AbsolutePath(string, AbsolutePath) used
Path.Combine without Path.GetFullPath, leaving '..' segments
unresolved. This caused output paths like 'dir\..\ClassLib\...'
instead of 'ClassLib\...', breaking string-based path comparisons
in downstream MSBuild targets and tests.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@SimaTian SimaTian marked this pull request as draft February 24, 2026 17:54
SimaTian and others added 4 commits February 25, 2026 13:20
On .NET Framework, ProcessStartInfo defaults to UseShellExecute=true,
which prevents EnvironmentVariables from being applied.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Normalization is caller's responsibility, matching real MSBuild polyfill.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Use assembly-relative path instead of bare filename to avoid FileNotFound
when parallel tests change the process CWD via TaskTestEnvironment.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…y-init and set TaskEnvironment in tests

- Replace '= null!' with #if NETFRAMEWORK lazy-init pattern for
  TaskEnvironment property, matching all other Pattern B tasks
- Set TaskEnvironment in all 5 test instantiations to prevent NRE
  on .NET Core where there is no lazy-init fallback

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants