From 7db4e75130f25727deaabffb7d0680f4dcf0d787 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Fri, 3 Jul 2026 11:32:24 +0200 Subject: [PATCH 1/2] Avoid MSB4011 duplicate-import warnings when layering MSTest.Sdk on another base SDK (#9562) Guard the Microsoft.NET.Sdk import in MSTest.Sdk's Sdk.props/Sdk.targets behind a new _MSTestSdkImportsMicrosoftNETSdk property. MSTest.Sdk now only imports the base .NET SDK when no other SDK has already done so (detected via UsingMicrosoftNETSdk, which Microsoft.NET.Sdk sets very early). This lets MSTest.Sdk be layered on top of a different base SDK such as Microsoft.NET.Sdk.Web via manual SDK imports without emitting MSB4011 warnings. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/Package/MSTest.Sdk/README.md | 27 +++++++++ src/Package/MSTest.Sdk/Sdk/Sdk.props.template | 27 ++++++++- src/Package/MSTest.Sdk/Sdk/Sdk.targets | 11 +++- .../SdkTests.cs | 60 +++++++++++++++++++ 4 files changed, 122 insertions(+), 3 deletions(-) diff --git a/src/Package/MSTest.Sdk/README.md b/src/Package/MSTest.Sdk/README.md index 9047d20f80..acbf88ec09 100644 --- a/src/Package/MSTest.Sdk/README.md +++ b/src/Package/MSTest.Sdk/README.md @@ -18,3 +18,30 @@ Instead, the `.targets` files should split the version specification depending o matching `PackageVersion` item carrying the `Version` metadata so CPM resolves the version. This approach works regardless of `CentralPackageVersionOverrideEnabled` and does not produce `NU1009` warnings. + +## Layering on top of a different base SDK + +`MSTest.Sdk` implicitly imports `Microsoft.NET.Sdk` as its base SDK, so `` +is enough for the common case. To combine it with a different base SDK (for example +`Microsoft.NET.Sdk.Web` for an ASP.NET Core integration test project), import both SDKs manually and +list `Microsoft.NET.Sdk.Web` first so it owns the base `Microsoft.NET.Sdk` import: + +```xml + + + + + + net10.0 + + + + + +``` + +`Sdk.props`/`Sdk.targets` guard their `Microsoft.NET.Sdk` import behind the +`_MSTestSdkImportsMicrosoftNETSdk` property: `MSTest.Sdk` only imports the base SDK when no other SDK +has already done so (detected via the `UsingMicrosoftNETSdk` property that `Microsoft.NET.Sdk` sets +very early). This keeps the manual/mixed layering scenario free of `MSB4011` duplicate-import +warnings. diff --git a/src/Package/MSTest.Sdk/Sdk/Sdk.props.template b/src/Package/MSTest.Sdk/Sdk/Sdk.props.template index 004df17238..12cc054cfa 100644 --- a/src/Package/MSTest.Sdk/Sdk/Sdk.props.template +++ b/src/Package/MSTest.Sdk/Sdk/Sdk.props.template @@ -8,8 +8,33 @@ true + + + <_MSTestSdkImportsMicrosoftNETSdk Condition=" '$(_MSTestSdkImportsMicrosoftNETSdk)' == '' AND '$(UsingMicrosoftNETSdk)' != 'true' ">true + <_MSTestSdkImportsMicrosoftNETSdk Condition=" '$(_MSTestSdkImportsMicrosoftNETSdk)' == '' ">false + + - + false diff --git a/src/Package/MSTest.Sdk/Sdk/Sdk.targets b/src/Package/MSTest.Sdk/Sdk/Sdk.targets index eb0409a9af..f71aae3f4b 100644 --- a/src/Package/MSTest.Sdk/Sdk/Sdk.targets +++ b/src/Package/MSTest.Sdk/Sdk/Sdk.targets @@ -13,6 +13,13 @@ - - + + diff --git a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/SdkTests.cs b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/SdkTests.cs index 4bbac6557f..fcf8005fe7 100644 --- a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/SdkTests.cs +++ b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/SdkTests.cs @@ -667,4 +667,64 @@ public void TestMethod1() testHostResult.AssertOutputContains("Test Parallelization enabled"); testHostResult.AssertOutputContains("(Workers: 3, Scope: MethodLevel)"); } + + // Verifies that MSTest.Sdk can be layered on top of a different base SDK (here Microsoft.NET.Sdk.Web) + // using manual SDK imports without emitting MSB4011 duplicate-import warnings. See https://github.com/microsoft/testfx/issues/9562. + [TestMethod] + public async Task MSTestSdk_LayeredOnTopOfWebSdk_BuildsWithoutDuplicateImportWarnings() + { + const string MixedSdkSource = """ +#file MSTestWeb.csproj + + + + + + + true + $TargetFramework$ + x64 + $(NoWarn);NU1507 + + + + + + + +#file UnitTest1.cs +using Microsoft.AspNetCore.Builder; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace MSTestWebTest +{ + [TestClass] + public class UnitTest1 + { + [TestMethod] + public void TestMethod1() + { + WebApplicationBuilder builder = WebApplication.CreateBuilder(); + Assert.IsNotNull(builder); + } + } +} +"""; + + const string MixedAssetName = "MSTestWeb"; + + using TestAsset testAsset = await TestAsset.GenerateAssetAsync( + MixedAssetName, + MixedSdkSource + .PatchCodeWithReplace("$MSTestVersion$", MSTestVersion) + .PatchCodeWithReplace("$TargetFramework$", TargetFrameworks.NetCurrent)); + + DotnetMuxerResult compilationResult = await DotnetCli.RunAsync($"build -c {BuildConfiguration.Release} {testAsset.TargetAssetPath}", cancellationToken: TestContext.CancellationToken); + compilationResult.AssertExitCodeIs(0); + compilationResult.AssertOutputDoesNotContain("MSB4011"); + + var testHost = TestHost.LocateFrom(testAsset.TargetAssetPath, MixedAssetName, TargetFrameworks.NetCurrent, buildConfiguration: BuildConfiguration.Release); + TestHostResult testHostResult = await testHost.ExecuteAsync(cancellationToken: TestContext.CancellationToken); + testHostResult.AssertOutputContainsSummary(0, 1, 0); + } } From daf433bacef246c3994c60b65cf5b76cad6b4f0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Fri, 3 Jul 2026 18:26:07 +0200 Subject: [PATCH 2/2] Fix acceptance test: pin MSTest.Sdk version via global.json for manual SDK imports The versioned SDK form (MSTest.Sdk/version) only resolves on the outer attribute, not on , which caused MSB4236 'SDK could not be found' in CI. Pin the version through global.json's msbuild-sdks instead, matching the canonical way to pin MSBuild project SDK versions for manual/mixed imports. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/Package/MSTest.Sdk/README.md | 11 +++++++++++ .../MSTest.Acceptance.IntegrationTests/SdkTests.cs | 11 +++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/Package/MSTest.Sdk/README.md b/src/Package/MSTest.Sdk/README.md index acbf88ec09..ea909a9213 100644 --- a/src/Package/MSTest.Sdk/README.md +++ b/src/Package/MSTest.Sdk/README.md @@ -40,6 +40,17 @@ list `Microsoft.NET.Sdk.Web` first so it owns the base `Microsoft.NET.Sdk` impor ``` +Because the version cannot be specified on an ``'s `Sdk` attribute the same way it can on +``, pin the `MSTest.Sdk` version through `global.json`: + +```json +{ + "msbuild-sdks": { + "MSTest.Sdk": "x.y.z" + } +} +``` + `Sdk.props`/`Sdk.targets` guard their `Microsoft.NET.Sdk` import behind the `_MSTestSdkImportsMicrosoftNETSdk` property: `MSTest.Sdk` only imports the base SDK when no other SDK has already done so (detected via the `UsingMicrosoftNETSdk` property that `Microsoft.NET.Sdk` sets diff --git a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/SdkTests.cs b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/SdkTests.cs index fcf8005fe7..00238c0018 100644 --- a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/SdkTests.cs +++ b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/SdkTests.cs @@ -678,7 +678,7 @@ public async Task MSTestSdk_LayeredOnTopOfWebSdk_BuildsWithoutDuplicateImportWar - + true @@ -687,11 +687,18 @@ public async Task MSTestSdk_LayeredOnTopOfWebSdk_BuildsWithoutDuplicateImportWar $(NoWarn);NU1507 - + +#file global.json +{ + "msbuild-sdks": { + "MSTest.Sdk": "$MSTestVersion$" + } +} + #file UnitTest1.cs using Microsoft.AspNetCore.Builder; using Microsoft.VisualStudio.TestTools.UnitTesting;