diff --git a/src/Build/Instance/TaskRegistry.cs b/src/Build/Instance/TaskRegistry.cs index 89d96753b1d..8b9db196161 100644 --- a/src/Build/Instance/TaskRegistry.cs +++ b/src/Build/Instance/TaskRegistry.cs @@ -950,18 +950,16 @@ private static bool IdentityParametersMatch(in TaskHostParameters x, in TaskHost return false; } - // For exact match, all properties must be equal (case-insensitive) + // For exact match, only identity properties must be equal + // Paths are NOT part of identity - they're just resolved locations return string.Equals(x.Runtime, y.Runtime, StringComparison.OrdinalIgnoreCase) && string.Equals(x.Architecture, y.Architecture, StringComparison.OrdinalIgnoreCase) && - string.Equals(x.DotnetHostPath, y.DotnetHostPath, StringComparison.OrdinalIgnoreCase) && - string.Equals(x.MSBuildAssemblyPath, y.MSBuildAssemblyPath, StringComparison.OrdinalIgnoreCase) && x.TaskHostFactoryExplicitlyRequested == y.TaskHostFactoryExplicitlyRequested; } else { // Fuzzy match: null is treated as "don't care" // Only check runtime and architecture for fuzzy matching - string runtimeX = x.Runtime; string runtimeY = y.Runtime; string architectureX = x.Architecture; diff --git a/src/Framework/TaskHostParameters.cs b/src/Framework/TaskHostParameters.cs index 73b78e38182..97ee9223a96 100644 --- a/src/Framework/TaskHostParameters.cs +++ b/src/Framework/TaskHostParameters.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.Collections.Generic; namespace Microsoft.Build.Framework @@ -8,7 +9,7 @@ namespace Microsoft.Build.Framework /// /// A readonly struct that represents task host parameters used to determine which host process to launch. /// - public readonly struct TaskHostParameters + public readonly struct TaskHostParameters : IEquatable { /// /// A static empty instance to avoid allocations when default parameters are needed. @@ -72,6 +73,29 @@ internal TaskHostParameters( /// public bool? TaskHostFactoryExplicitlyRequested => _taskHostFactoryExplicitlyRequested; + public override bool Equals(object? obj) => obj is TaskHostParameters other && Equals(other); + + public bool Equals(TaskHostParameters other) => + StringComparer.OrdinalIgnoreCase.Equals(Runtime ?? string.Empty, other.Runtime ?? string.Empty) + && StringComparer.OrdinalIgnoreCase.Equals(Architecture ?? string.Empty, other.Architecture ?? string.Empty) + && TaskHostFactoryExplicitlyRequested == other.TaskHostFactoryExplicitlyRequested; + + public override int GetHashCode() + { + // Manual hash code implementation for compatibility with .NET Framework 4.7.2 + var comparer = StringComparer.OrdinalIgnoreCase; + + unchecked + { + int hash = 17; + hash = hash * 31 + comparer.GetHashCode(Runtime ?? string.Empty); + hash = hash * 31 + comparer.GetHashCode(Architecture ?? string.Empty); + hash = hash * 31 + (TaskHostFactoryExplicitlyRequested?.GetHashCode() ?? 0); + + return hash; + } + } + /// /// Gets a value indicating whether returns true if parameters were unset. /// @@ -134,7 +158,7 @@ internal TaskHostParameters WithTaskHostFactoryExplicitlyRequested(bool taskHost /// /// The method was added to sustain compatibility with ITaskFactory2 factoryIdentityParameters parameters dictionary. /// - internal Dictionary ToDictionary() => new(3) + internal Dictionary ToDictionary() => new(3, StringComparer.OrdinalIgnoreCase) { { nameof(Runtime), Runtime ?? string.Empty }, { nameof(Architecture), Architecture ?? string.Empty },