From 1836ab8c9904dd39726a4a213ba685f3bf6416c0 Mon Sep 17 00:00:00 2001 From: huulinh99 Date: Wed, 29 Oct 2025 14:00:29 +0700 Subject: [PATCH 1/6] Add fallback behavior for Microsoft.DotNet.MSBuildSdkResolver loading in external API scenarios --- .../BackEnd/SdkResolverLoader_Tests.cs | 123 ++++++++++++++++++ .../SdkResolution/SdkResolverLoader.cs | 31 ++++- 2 files changed, 150 insertions(+), 4 deletions(-) diff --git a/src/Build.UnitTests/BackEnd/SdkResolverLoader_Tests.cs b/src/Build.UnitTests/BackEnd/SdkResolverLoader_Tests.cs index 298987ef008..a5a793c7674 100644 --- a/src/Build.UnitTests/BackEnd/SdkResolverLoader_Tests.cs +++ b/src/Build.UnitTests/BackEnd/SdkResolverLoader_Tests.cs @@ -396,6 +396,69 @@ public void SdkResolverLoaderHonorsAdditionalResolversFolder() } } + /// + /// Test that external API users (not in VS) get fallback behavior - Assembly.Load fails but Assembly.LoadFrom succeeds + /// + [Fact] + public void LoadResolverAssembly_MSBuildSdkResolver_ExternalAPIUser_FallbackSucceeds() + { + using (var env = TestEnvironment.Create(_output)) + { + // Create testable loader that can control VS environment + var testLoader = new TestableSdkResolverLoader { MockRunningInVisualStudio = false }; + + // Create resolver folder structure with the specific name that triggers special logic + var testRoot = env.CreateFolder().Path; + var resolverFolder = Path.Combine(testRoot, "Microsoft.DotNet.MSBuildSdkResolver"); + Directory.CreateDirectory(resolverFolder); + + // Create assembly file with the exact name that triggers special logic + var assemblyFile = Path.Combine(resolverFolder, "Microsoft.DotNet.MSBuildSdkResolver.dll"); + var sourceAssembly = typeof(SdkResolverLoader).Assembly; + File.Copy(sourceAssembly.Location, assemblyFile, true); + + // Set the test path for the loader + testLoader.TestSdkResolversPath = testRoot; + + // Test external API user (not in VS) - should succeed with fallback + var resolvers = testLoader.LoadAllResolvers(new MockElementLocation("file")); + + // Should succeed because Assembly.Load fails but Assembly.LoadFrom succeeds + resolvers.ShouldNotBeNull(); + resolvers.Count.ShouldBeGreaterThan(0); + } + } + + /// + /// Test that Visual Studio users do NOT get fallback behavior - they only try Assembly.Load and fail + /// + [Fact] + public void LoadResolverAssembly_MSBuildSdkResolver_VisualStudioUser_NoFallback() + { + using (var env = TestEnvironment.Create(_output)) + { + // Create testable loader that simulates VS environment + var testLoader = new TestableSdkResolverLoader { MockRunningInVisualStudio = true }; + + // Create resolver folder structure with the specific name that triggers special logic + var testRoot = env.CreateFolder().Path; + var resolverFolder = Path.Combine(testRoot, "Microsoft.DotNet.MSBuildSdkResolver"); + Directory.CreateDirectory(resolverFolder); + + // Create assembly file with the exact name that triggers special logic + var assemblyFile = Path.Combine(resolverFolder, "Microsoft.DotNet.MSBuildSdkResolver.dll"); + var sourceAssembly = typeof(SdkResolverLoader).Assembly; + File.Copy(sourceAssembly.Location, assemblyFile, true); + + // Set the test path for the loader + testLoader.TestSdkResolversPath = testRoot; + + // Test VS user - should fail because no fallback + Should.Throw(() => + testLoader.LoadAllResolvers(new MockElementLocation("file"))); + } + } + private sealed class MockSdkResolverThatDoesNotLoad : SdkResolverBase { public const string ExpectedMessage = "A8BB8B3131D3475D881ACD3AF8D75BD6"; @@ -500,5 +563,65 @@ protected override void LoadResolvers(string resolverPath, ElementLocation locat base.LoadResolvers(resolverPath, location, resolvers); } } + + private sealed class TestableSdkResolverLoader : SdkResolverLoader + { + public bool MockRunningInVisualStudio { get; set; } = false; + public string TestSdkResolversPath { get; set; } + + internal override IReadOnlyList LoadAllResolvers(ElementLocation location) + { + // Store original environment + var originalEnvironment = BuildEnvironmentHelper.Instance; + + try + { + // Create a mock BuildEnvironment that returns our controlled value + var mockEnvironment = new BuildEnvironment( + originalEnvironment.Mode, + originalEnvironment.CurrentMSBuildExePath, + originalEnvironment.RunningTests, + originalEnvironment.RunningInMSBuildExe, + MockRunningInVisualStudio, // Our controlled value + originalEnvironment.VisualStudioInstallRootDirectory); + + // Reset the instance for testing + BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly(mockEnvironment); + + // Use test path if provided, otherwise use default + if (!string.IsNullOrEmpty(TestSdkResolversPath)) + { + return LoadAllResolversFromTestPath(location); + } + + // Call the real LoadAllResolvers method + return base.LoadAllResolvers(location); + } + finally + { + // Restore original environment + BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly(originalEnvironment); + } + } + + private IReadOnlyList LoadAllResolversFromTestPath(ElementLocation location) + { + var resolvers = new List { new DefaultSdkResolver() }; + + var potentialResolvers = FindPotentialSdkResolvers(TestSdkResolversPath, location); + + if (potentialResolvers.Count == 0) + { + return resolvers; + } + + foreach (var potentialResolver in potentialResolvers) + { + LoadResolvers(potentialResolver, location, resolvers); + } + + return resolvers.OrderBy(t => t.Priority).ToList(); + } + } } } diff --git a/src/Build/BackEnd/Components/SdkResolution/SdkResolverLoader.cs b/src/Build/BackEnd/Components/SdkResolution/SdkResolverLoader.cs index c9876d8a9bc..6da6d20b4d3 100644 --- a/src/Build/BackEnd/Components/SdkResolution/SdkResolverLoader.cs +++ b/src/Build/BackEnd/Components/SdkResolution/SdkResolverLoader.cs @@ -238,11 +238,34 @@ protected virtual Assembly LoadResolverAssembly(string resolverPath) { // This will load the resolver assembly into the default load context if possible, and fall back to LoadFrom context. // We very much prefer the default load context because it allows native images to be used by the CLR, improving startup perf. - AssemblyName assemblyName = new AssemblyName(resolverFileName) + bool isRunningInVS = BuildEnvironmentHelper.Instance.RunningInVisualStudio; + if (!isRunningInVS) { - CodeBase = resolverPath, - }; - return Assembly.Load(assemblyName); + // Apply compatibility fallback for external API users + try + { + AssemblyName assemblyName = new AssemblyName(resolverFileName) + { + CodeBase = resolverPath, + }; + return Assembly.Load(assemblyName); + } + catch (Exception) + { + // Fallback for external API users only + return Assembly.LoadFrom(resolverPath); + } + } + else + { + // Inside VS: use original optimization (no fallback) + // If it fails, let it fail - VS environment should work + AssemblyName assemblyName = new AssemblyName(resolverFileName) + { + CodeBase = resolverPath, + }; + return Assembly.Load(assemblyName); + } } } return Assembly.LoadFrom(resolverPath); From d46827e55720ddf140019f85cce18116b95213ec Mon Sep 17 00:00:00 2001 From: huulinh99 Date: Thu, 30 Oct 2025 14:35:34 +0700 Subject: [PATCH 2/6] Unify MSBuildSdkResolver env tests into a single Theory and remove helper class --- .../BackEnd/SdkResolverLoader_Tests.cs | 141 +++++------------- 1 file changed, 39 insertions(+), 102 deletions(-) diff --git a/src/Build.UnitTests/BackEnd/SdkResolverLoader_Tests.cs b/src/Build.UnitTests/BackEnd/SdkResolverLoader_Tests.cs index a5a793c7674..566e3ec02dd 100644 --- a/src/Build.UnitTests/BackEnd/SdkResolverLoader_Tests.cs +++ b/src/Build.UnitTests/BackEnd/SdkResolverLoader_Tests.cs @@ -397,65 +397,60 @@ public void SdkResolverLoaderHonorsAdditionalResolversFolder() } /// - /// Test that external API users (not in VS) get fallback behavior - Assembly.Load fails but Assembly.LoadFrom succeeds + /// Combined test: when running in VS, no fallback (throws); when external, fallback succeeds. /// - [Fact] - public void LoadResolverAssembly_MSBuildSdkResolver_ExternalAPIUser_FallbackSucceeds() + [Theory] + [InlineData(false)] + [InlineData(true)] + public void LoadResolverAssembly_MSBuildSdkResolver_BehavesByEnvironment(bool shouldFail) { using (var env = TestEnvironment.Create(_output)) { - // Create testable loader that can control VS environment - var testLoader = new TestableSdkResolverLoader { MockRunningInVisualStudio = false }; - // Create resolver folder structure with the specific name that triggers special logic var testRoot = env.CreateFolder().Path; var resolverFolder = Path.Combine(testRoot, "Microsoft.DotNet.MSBuildSdkResolver"); Directory.CreateDirectory(resolverFolder); - + // Create assembly file with the exact name that triggers special logic var assemblyFile = Path.Combine(resolverFolder, "Microsoft.DotNet.MSBuildSdkResolver.dll"); var sourceAssembly = typeof(SdkResolverLoader).Assembly; File.Copy(sourceAssembly.Location, assemblyFile, true); - // Set the test path for the loader - testLoader.TestSdkResolversPath = testRoot; - - // Test external API user (not in VS) - should succeed with fallback - var resolvers = testLoader.LoadAllResolvers(new MockElementLocation("file")); - - // Should succeed because Assembly.Load fails but Assembly.LoadFrom succeeds - resolvers.ShouldNotBeNull(); - resolvers.Count.ShouldBeGreaterThan(0); - } - } - - /// - /// Test that Visual Studio users do NOT get fallback behavior - they only try Assembly.Load and fail - /// - [Fact] - public void LoadResolverAssembly_MSBuildSdkResolver_VisualStudioUser_NoFallback() - { - using (var env = TestEnvironment.Create(_output)) - { - // Create testable loader that simulates VS environment - var testLoader = new TestableSdkResolverLoader { MockRunningInVisualStudio = true }; + // Prepare environment toggle directly in the test + var originalEnvironment = BuildEnvironmentHelper.Instance; + try + { + var mockEnvironment = new BuildEnvironment( + originalEnvironment.Mode, + originalEnvironment.CurrentMSBuildExePath, + originalEnvironment.RunningTests, + originalEnvironment.RunningInMSBuildExe, + /*RunningInVisualStudio*/ shouldFail, + originalEnvironment.VisualStudioInstallRootDirectory); - // Create resolver folder structure with the specific name that triggers special logic - var testRoot = env.CreateFolder().Path; - var resolverFolder = Path.Combine(testRoot, "Microsoft.DotNet.MSBuildSdkResolver"); - Directory.CreateDirectory(resolverFolder); - - // Create assembly file with the exact name that triggers special logic - var assemblyFile = Path.Combine(resolverFolder, "Microsoft.DotNet.MSBuildSdkResolver.dll"); - var sourceAssembly = typeof(SdkResolverLoader).Assembly; - File.Copy(sourceAssembly.Location, assemblyFile, true); + BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly(mockEnvironment); - // Set the test path for the loader - testLoader.TestSdkResolversPath = testRoot; + // Use mock loader to feed our resolver path without relying on MSBuildToolsDirectory + var loader = new MockSdkResolverLoader + { + FindPotentialSdkResolversFunc = (_, __) => new List { assemblyFile } + }; - // Test VS user - should fail because no fallback - Should.Throw(() => - testLoader.LoadAllResolvers(new MockElementLocation("file"))); + if (shouldFail) + { + Should.Throw(() => loader.LoadAllResolvers(new MockElementLocation("file"))); + } + else + { + var resolvers = loader.LoadAllResolvers(new MockElementLocation("file")); + resolvers.ShouldNotBeNull(); + resolvers.Count.ShouldBeGreaterThan(0); + } + } + finally + { + BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly(originalEnvironment); + } } } @@ -564,64 +559,6 @@ protected override void LoadResolvers(string resolverPath, ElementLocation locat } } - private sealed class TestableSdkResolverLoader : SdkResolverLoader - { - public bool MockRunningInVisualStudio { get; set; } = false; - public string TestSdkResolversPath { get; set; } - - internal override IReadOnlyList LoadAllResolvers(ElementLocation location) - { - // Store original environment - var originalEnvironment = BuildEnvironmentHelper.Instance; - - try - { - // Create a mock BuildEnvironment that returns our controlled value - var mockEnvironment = new BuildEnvironment( - originalEnvironment.Mode, - originalEnvironment.CurrentMSBuildExePath, - originalEnvironment.RunningTests, - originalEnvironment.RunningInMSBuildExe, - MockRunningInVisualStudio, // Our controlled value - originalEnvironment.VisualStudioInstallRootDirectory); - - // Reset the instance for testing - BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly(mockEnvironment); - - // Use test path if provided, otherwise use default - if (!string.IsNullOrEmpty(TestSdkResolversPath)) - { - return LoadAllResolversFromTestPath(location); - } - - // Call the real LoadAllResolvers method - return base.LoadAllResolvers(location); - } - finally - { - // Restore original environment - BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly(originalEnvironment); - } - } - - private IReadOnlyList LoadAllResolversFromTestPath(ElementLocation location) - { - var resolvers = new List { new DefaultSdkResolver() }; - - var potentialResolvers = FindPotentialSdkResolvers(TestSdkResolversPath, location); - - if (potentialResolvers.Count == 0) - { - return resolvers; - } - - foreach (var potentialResolver in potentialResolvers) - { - LoadResolvers(potentialResolver, location, resolvers); - } - - return resolvers.OrderBy(t => t.Priority).ToList(); - } - } + // Removed TestableSdkResolverLoader; tests now set environment directly and use MockSdkResolverLoader } } From 8a4c5dd7ab2a6d0fe5d9b129f8faf0aa9eb8b876 Mon Sep 17 00:00:00 2001 From: huulinh99 Date: Wed, 5 Nov 2025 12:32:07 +0700 Subject: [PATCH 3/6] Update the logic code and unit test --- .../BackEnd/SdkResolverLoader_Tests.cs | 122 ++++++++++++------ 1 file changed, 86 insertions(+), 36 deletions(-) diff --git a/src/Build.UnitTests/BackEnd/SdkResolverLoader_Tests.cs b/src/Build.UnitTests/BackEnd/SdkResolverLoader_Tests.cs index 566e3ec02dd..e2e076f9717 100644 --- a/src/Build.UnitTests/BackEnd/SdkResolverLoader_Tests.cs +++ b/src/Build.UnitTests/BackEnd/SdkResolverLoader_Tests.cs @@ -397,12 +397,13 @@ public void SdkResolverLoaderHonorsAdditionalResolversFolder() } /// - /// Combined test: when running in VS, no fallback (throws); when external, fallback succeeds. + /// Test that LoadResolverAssembly handles fallback behavior correctly based on isRunningInVS. + /// Uses MockSdkResolverLoader to simulate both VS and non-VS behaviors . /// [Theory] - [InlineData(false)] - [InlineData(true)] - public void LoadResolverAssembly_MSBuildSdkResolver_BehavesByEnvironment(bool shouldFail) + [InlineData(true, false)] // isRunningInVS = true, no fallback, should fail when Assembly.Load fails + [InlineData(false, true)] // isRunningInVS = false, has fallback, should succeed with LoadFrom + public void LoadResolverAssembly_MSBuildSdkResolver_WithAndWithoutFallback(bool isRunningInVS, bool shouldSucceed) { using (var env = TestEnvironment.Create(_output)) { @@ -411,45 +412,88 @@ public void LoadResolverAssembly_MSBuildSdkResolver_BehavesByEnvironment(bool sh var resolverFolder = Path.Combine(testRoot, "Microsoft.DotNet.MSBuildSdkResolver"); Directory.CreateDirectory(resolverFolder); - // Create assembly file with the exact name that triggers special logic var assemblyFile = Path.Combine(resolverFolder, "Microsoft.DotNet.MSBuildSdkResolver.dll"); - var sourceAssembly = typeof(SdkResolverLoader).Assembly; - File.Copy(sourceAssembly.Location, assemblyFile, true); - // Prepare environment toggle directly in the test - var originalEnvironment = BuildEnvironmentHelper.Instance; - try + // Create file based on test scenario + if (shouldSucceed) { - var mockEnvironment = new BuildEnvironment( - originalEnvironment.Mode, - originalEnvironment.CurrentMSBuildExePath, - originalEnvironment.RunningTests, - originalEnvironment.RunningInMSBuildExe, - /*RunningInVisualStudio*/ shouldFail, - originalEnvironment.VisualStudioInstallRootDirectory); - - BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly(mockEnvironment); - - // Use mock loader to feed our resolver path without relying on MSBuildToolsDirectory - var loader = new MockSdkResolverLoader - { - FindPotentialSdkResolversFunc = (_, __) => new List { assemblyFile } - }; + // For fallback test: copy test assembly (which is already loaded) instead of Microsoft.Build.dll + // This reduces side effects because the test assembly is already in the load context + var testAssembly = typeof(SdkResolverLoader_Tests).Assembly; + File.Copy(testAssembly.Location, assemblyFile, true); + } + else + { + // For no-fallback test: create invalid assembly content to force Assembly.Load to fail + File.WriteAllText(assemblyFile, "invalid assembly content"); + } - if (shouldFail) - { - Should.Throw(() => loader.LoadAllResolvers(new MockElementLocation("file"))); - } - else + // Use MockSdkResolverLoader to test actual Assembly.LoadFrom behavior + // but with test assembly (already loaded) instead of Microsoft.Build.dll to reduce side effects + var loader = new MockSdkResolverLoader + { + FindPotentialSdkResolversFunc = (_, __) => new List { assemblyFile }, + GetResolverTypesFunc = assembly => new[] { typeof(MockSdkResolverWithAssemblyPath) }, + LoadResolverAssemblyFunc = (resolverPath) => { - var resolvers = loader.LoadAllResolvers(new MockElementLocation("file")); - resolvers.ShouldNotBeNull(); - resolvers.Count.ShouldBeGreaterThan(0); + string resolverFileName = Path.GetFileNameWithoutExtension(resolverPath); + if (resolverFileName.Equals("Microsoft.DotNet.MSBuildSdkResolver", StringComparison.OrdinalIgnoreCase)) + { + // Capture test parameters via closure + bool simulatedIsRunningInVS = isRunningInVS; + + if (simulatedIsRunningInVS) + { + // VS behavior: try Assembly.Load directly, no fallback + AssemblyName assemblyName = new AssemblyName(resolverFileName) + { + CodeBase = resolverPath, + }; + // This will throw if file is invalid (no fallback) + return Assembly.Load(assemblyName); + } + else + { + // Non-VS behavior: try Assembly.Load first, fallback to LoadFrom if it fails + // We actually call Assembly.LoadFrom here but with test assembly (already loaded) + // to reduce side effects compared to loading Microsoft.Build.dll copy + try + { + // Try Assembly.Load first (will fail for invalid file, succeed for valid) + AssemblyName assemblyName = new AssemblyName(resolverFileName) + { + CodeBase = resolverPath, + }; + return Assembly.Load(assemblyName); + } + catch (Exception) + { + // Fallback to LoadFrom + return Assembly.LoadFrom(resolverPath); + } + } + } + return Assembly.LoadFrom(resolverPath); } + }; + + if (shouldSucceed) + { + // Test that loading succeeds with fallback logic + var resolvers = loader.LoadAllResolvers(new MockElementLocation("file")); + resolvers.ShouldNotBeNull(); + resolvers.Count.ShouldBeGreaterThan(0); } - finally + else { - BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly(originalEnvironment); + // Should throw InvalidProjectFileException because: + // 1. Simulated isRunningInVS = true → no fallback + // 2. Assembly.Load fails on invalid assembly + // 3. No fallback → exception propagates + var exception = Should.Throw(() => + loader.LoadAllResolvers(new MockElementLocation("file"))); + + exception.Message.ShouldContain("could not be loaded"); } } } @@ -493,7 +537,13 @@ private sealed class MockSdkResolverWithAssemblyPath : SdkResolverBase { public string AssemblyPath; - public MockSdkResolverWithAssemblyPath(string assemblyPath = "") + // Parameterless constructor for reflection-based instantiation + public MockSdkResolverWithAssemblyPath() + : this("") + { + } + + public MockSdkResolverWithAssemblyPath(string assemblyPath) { AssemblyPath = assemblyPath; } From ec2dca609e5e585e0815badb60cdd0f49e24ead0 Mon Sep 17 00:00:00 2001 From: huulinh99 Date: Wed, 5 Nov 2025 15:06:13 +0700 Subject: [PATCH 4/6] Update logic code and unit test --- .../BackEnd/SdkResolverLoader_Tests.cs | 44 +++++++++---------- .../SdkResolution/SdkResolverLoader.cs | 33 +++++++------- 2 files changed, 38 insertions(+), 39 deletions(-) diff --git a/src/Build.UnitTests/BackEnd/SdkResolverLoader_Tests.cs b/src/Build.UnitTests/BackEnd/SdkResolverLoader_Tests.cs index e2e076f9717..2449288f3e1 100644 --- a/src/Build.UnitTests/BackEnd/SdkResolverLoader_Tests.cs +++ b/src/Build.UnitTests/BackEnd/SdkResolverLoader_Tests.cs @@ -415,21 +415,17 @@ public void LoadResolverAssembly_MSBuildSdkResolver_WithAndWithoutFallback(bool var assemblyFile = Path.Combine(resolverFolder, "Microsoft.DotNet.MSBuildSdkResolver.dll"); // Create file based on test scenario - if (shouldSucceed) - { - // For fallback test: copy test assembly (which is already loaded) instead of Microsoft.Build.dll - // This reduces side effects because the test assembly is already in the load context - var testAssembly = typeof(SdkResolverLoader_Tests).Assembly; - File.Copy(testAssembly.Location, assemblyFile, true); - } - else + // For shouldSucceed=false: create invalid file to test Assembly.Load failure + // For shouldSucceed=true: we don't create a file - we'll simulate success in the mock + // to avoid side effects from loading Microsoft.Build.dll copy + if (!shouldSucceed) { // For no-fallback test: create invalid assembly content to force Assembly.Load to fail File.WriteAllText(assemblyFile, "invalid assembly content"); } - // Use MockSdkResolverLoader to test actual Assembly.LoadFrom behavior - // but with test assembly (already loaded) instead of Microsoft.Build.dll to reduce side effects + // Use MockSdkResolverLoader to simulate behavior without modifying global state + // We avoid actually calling Assembly.LoadFrom with Microsoft.Build.dll copy to prevent side effects var loader = new MockSdkResolverLoader { FindPotentialSdkResolversFunc = (_, __) => new List { assemblyFile }, @@ -441,10 +437,13 @@ public void LoadResolverAssembly_MSBuildSdkResolver_WithAndWithoutFallback(bool { // Capture test parameters via closure bool simulatedIsRunningInVS = isRunningInVS; + bool simulatedShouldSucceed = shouldSucceed; if (simulatedIsRunningInVS) { // VS behavior: try Assembly.Load directly, no fallback + // We only call Assembly.Load for invalid file case (shouldSucceed=false) + // to test that exception propagates correctly AssemblyName assemblyName = new AssemblyName(resolverFileName) { CodeBase = resolverPath, @@ -455,21 +454,20 @@ public void LoadResolverAssembly_MSBuildSdkResolver_WithAndWithoutFallback(bool else { // Non-VS behavior: try Assembly.Load first, fallback to LoadFrom if it fails - // We actually call Assembly.LoadFrom here but with test assembly (already loaded) - // to reduce side effects compared to loading Microsoft.Build.dll copy - try + // We simulate this without actually calling Assembly.Load or Assembly.LoadFrom + // to avoid side effects from loading Microsoft.Build.dll copy + if (simulatedShouldSucceed) { - // Try Assembly.Load first (will fail for invalid file, succeed for valid) - AssemblyName assemblyName = new AssemblyName(resolverFileName) - { - CodeBase = resolverPath, - }; - return Assembly.Load(assemblyName); + // Simulate successful fallback: return an existing assembly + // We use an assembly that's already loaded and contains a valid resolver type + // to avoid side effects from loading Microsoft.Build.dll copy + return typeof(MockSdkResolverWithAssemblyPath).Assembly; } - catch (Exception) + else { - // Fallback to LoadFrom - return Assembly.LoadFrom(resolverPath); + // This branch shouldn't be reached in non-VS + shouldSucceed=false case + // but if it is, simulate Assembly.Load failure + throw new BadImageFormatException("Assembly could not be loaded"); } } } @@ -608,7 +606,5 @@ protected override void LoadResolvers(string resolverPath, ElementLocation locat base.LoadResolvers(resolverPath, location, resolvers); } } - - // Removed TestableSdkResolverLoader; tests now set environment directly and use MockSdkResolverLoader } } diff --git a/src/Build/BackEnd/Components/SdkResolution/SdkResolverLoader.cs b/src/Build/BackEnd/Components/SdkResolution/SdkResolverLoader.cs index 6da6d20b4d3..069c0ab470e 100644 --- a/src/Build/BackEnd/Components/SdkResolution/SdkResolverLoader.cs +++ b/src/Build/BackEnd/Components/SdkResolution/SdkResolverLoader.cs @@ -239,15 +239,18 @@ protected virtual Assembly LoadResolverAssembly(string resolverPath) // This will load the resolver assembly into the default load context if possible, and fall back to LoadFrom context. // We very much prefer the default load context because it allows native images to be used by the CLR, improving startup perf. bool isRunningInVS = BuildEnvironmentHelper.Instance.RunningInVisualStudio; - if (!isRunningInVS) + AssemblyName assemblyName = CreateAssemblyNameWithCodeBase(resolverFileName, resolverPath); + if (isRunningInVS) + { + // Inside VS use optimization without a fallback. VS environment should not fail loading. + // If for some reason it fails, we will want to find it out rather than silently catch the exception and allow a performance regression. + return Assembly.Load(assemblyName); + } + else { // Apply compatibility fallback for external API users try { - AssemblyName assemblyName = new AssemblyName(resolverFileName) - { - CodeBase = resolverPath, - }; return Assembly.Load(assemblyName); } catch (Exception) @@ -256,16 +259,6 @@ protected virtual Assembly LoadResolverAssembly(string resolverPath) return Assembly.LoadFrom(resolverPath); } } - else - { - // Inside VS: use original optimization (no fallback) - // If it fails, let it fail - VS environment should work - AssemblyName assemblyName = new AssemblyName(resolverFileName) - { - CodeBase = resolverPath, - }; - return Assembly.Load(assemblyName); - } } } return Assembly.LoadFrom(resolverPath); @@ -274,6 +267,16 @@ protected virtual Assembly LoadResolverAssembly(string resolverPath) #endif } +#if !FEATURE_ASSEMBLYLOADCONTEXT + private AssemblyName CreateAssemblyNameWithCodeBase(string assemblyName, string codeBase) + { + return new AssemblyName(assemblyName) + { + CodeBase = codeBase, + }; + } +#endif + protected internal virtual IReadOnlyList LoadResolversFromManifest(SdkResolverManifest manifest, ElementLocation location) { MSBuildEventSource.Log.SdkResolverLoadResolversStart(); From d061443d79b7e307daf1f6d6d858e281778254f2 Mon Sep 17 00:00:00 2001 From: huulinh99 Date: Tue, 11 Nov 2025 09:53:46 +0700 Subject: [PATCH 5/6] Use BuildEnvironment flags to detect API usage scenarios --- .../SdkResolution/SdkResolverLoader.cs | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/src/Build/BackEnd/Components/SdkResolution/SdkResolverLoader.cs b/src/Build/BackEnd/Components/SdkResolution/SdkResolverLoader.cs index 069c0ab470e..125e5afde36 100644 --- a/src/Build/BackEnd/Components/SdkResolution/SdkResolverLoader.cs +++ b/src/Build/BackEnd/Components/SdkResolution/SdkResolverLoader.cs @@ -238,26 +238,27 @@ protected virtual Assembly LoadResolverAssembly(string resolverPath) { // This will load the resolver assembly into the default load context if possible, and fall back to LoadFrom context. // We very much prefer the default load context because it allows native images to be used by the CLR, improving startup perf. - bool isRunningInVS = BuildEnvironmentHelper.Instance.RunningInVisualStudio; + var buildEnvironment = BuildEnvironmentHelper.Instance; AssemblyName assemblyName = CreateAssemblyNameWithCodeBase(resolverFileName, resolverPath); - if (isRunningInVS) + + // Check if we're in a scenario that needs fallback (API usage or dotnet CLI) + // These scenarios are detected by: Mode = Standalone and not running in MSBuild.exe + // This matches the condition set by TryFromMSBuildAssembly when MSBuild is called from external APIs + // VS and MSBuild.exe direct usage can use Assembly.Load reliably, so they don't need fallback + bool needsFallback = buildEnvironment.Mode == BuildEnvironmentMode.Standalone && !buildEnvironment.RunningInMSBuildExe; + + if (needsFallback) { - // Inside VS use optimization without a fallback. VS environment should not fail loading. - // If for some reason it fails, we will want to find it out rather than silently catch the exception and allow a performance regression. - return Assembly.Load(assemblyName); + // For external API users and dotnet CLI, use LoadFrom directly + // Assembly.Load fails in these scenarios due to assembly resolution context, + // so we use LoadFrom which works reliably without needing try-catch + return Assembly.LoadFrom(resolverPath); } else { - // Apply compatibility fallback for external API users - try - { - return Assembly.Load(assemblyName); - } - catch (Exception) - { - // Fallback for external API users only - return Assembly.LoadFrom(resolverPath); - } + // VS and MSBuild.exe direct usage: use Assembly.Load directly without fallback + // These scenarios should work reliably with Assembly.Load and benefit from NGEN + return Assembly.Load(assemblyName); } } } From 7b9e84c00e4d67a45519bedb4b4745d4ac86679a Mon Sep 17 00:00:00 2001 From: SimaTian Date: Mon, 24 Nov 2025 13:12:56 +0100 Subject: [PATCH 6/6] removing implementation to check if the test fails --- .../SdkResolution/SdkResolverLoader.cs | 35 +++---------------- 1 file changed, 4 insertions(+), 31 deletions(-) diff --git a/src/Build/BackEnd/Components/SdkResolution/SdkResolverLoader.cs b/src/Build/BackEnd/Components/SdkResolution/SdkResolverLoader.cs index 125e5afde36..c9876d8a9bc 100644 --- a/src/Build/BackEnd/Components/SdkResolution/SdkResolverLoader.cs +++ b/src/Build/BackEnd/Components/SdkResolution/SdkResolverLoader.cs @@ -238,28 +238,11 @@ protected virtual Assembly LoadResolverAssembly(string resolverPath) { // This will load the resolver assembly into the default load context if possible, and fall back to LoadFrom context. // We very much prefer the default load context because it allows native images to be used by the CLR, improving startup perf. - var buildEnvironment = BuildEnvironmentHelper.Instance; - AssemblyName assemblyName = CreateAssemblyNameWithCodeBase(resolverFileName, resolverPath); - - // Check if we're in a scenario that needs fallback (API usage or dotnet CLI) - // These scenarios are detected by: Mode = Standalone and not running in MSBuild.exe - // This matches the condition set by TryFromMSBuildAssembly when MSBuild is called from external APIs - // VS and MSBuild.exe direct usage can use Assembly.Load reliably, so they don't need fallback - bool needsFallback = buildEnvironment.Mode == BuildEnvironmentMode.Standalone && !buildEnvironment.RunningInMSBuildExe; - - if (needsFallback) + AssemblyName assemblyName = new AssemblyName(resolverFileName) { - // For external API users and dotnet CLI, use LoadFrom directly - // Assembly.Load fails in these scenarios due to assembly resolution context, - // so we use LoadFrom which works reliably without needing try-catch - return Assembly.LoadFrom(resolverPath); - } - else - { - // VS and MSBuild.exe direct usage: use Assembly.Load directly without fallback - // These scenarios should work reliably with Assembly.Load and benefit from NGEN - return Assembly.Load(assemblyName); - } + CodeBase = resolverPath, + }; + return Assembly.Load(assemblyName); } } return Assembly.LoadFrom(resolverPath); @@ -268,16 +251,6 @@ protected virtual Assembly LoadResolverAssembly(string resolverPath) #endif } -#if !FEATURE_ASSEMBLYLOADCONTEXT - private AssemblyName CreateAssemblyNameWithCodeBase(string assemblyName, string codeBase) - { - return new AssemblyName(assemblyName) - { - CodeBase = codeBase, - }; - } -#endif - protected internal virtual IReadOnlyList LoadResolversFromManifest(SdkResolverManifest manifest, ElementLocation location) { MSBuildEventSource.Log.SdkResolverLoadResolversStart();