Skip to content

Add cross-platform netstandard2.0 test coverage#1235

Open
Copilot wants to merge 8 commits into
mainfrom
copilot/add-net8-test-project
Open

Add cross-platform netstandard2.0 test coverage#1235
Copilot wants to merge 8 commits into
mainfrom
copilot/add-net8-test-project

Conversation

Copilot AI commented Feb 2, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a dedicated test project that exercises the netstandard2.0 build of the MCP libraries on a modern .NET runtime, providing netstandard2.0 test coverage on non-Windows platforms (where net472 is not run to avoid mono). On Windows, the netstandard2.0 assets continue to be validated through the net472 target of ModelContextProtocol.Tests.

Changes

Added: tests/ModelContextProtocol.Netstandard2_0.Tests/ModelContextProtocol.Netstandard2_0.Tests.csproj

  • Targets net8.0 so it runs cross-platform without mono.
  • References ModelContextProtocol and ModelContextProtocol.Core with <SetTargetFramework>TargetFramework=netstandard2.0</SetTargetFramework> to force the product references to resolve to their netstandard2.0 build.
  • Compiles the same test sources as ModelContextProtocol.Tests (excluding the external-process/stdio integration tests, which launch a separate net8.0 server process).
  • Pins Microsoft.Bcl.AsyncInterfaces so the netstandard2.0 product loads the required 10.x assembly instead of a transitive 6.0.0.

Modified: ModelContextProtocol.slnx, Directory.Packages.props

  • Registered the new test project and added the Microsoft.Bcl.AsyncInterfaces package version entry.

Modified: tests/ModelContextProtocol.Tests/ModelContextProtocol.Tests.csproj

  • Reverted the earlier conditional SetTargetFramework experiment so the main test project once again uses matching TFMs per platform (net10.0, net9.0, net8.0, plus net472 on Windows).

Implementation

<ProjectReference Include="..\..\src\ModelContextProtocol.Core\ModelContextProtocol.Core.csproj">
  <SetTargetFramework>TargetFramework=netstandard2.0</SetTargetFramework>
</ProjectReference>

Behavior

Windows

  • ModelContextProtocol.Tests: net10.0, net9.0, net8.0, net472 (net472 validates netstandard2.0 on .NET Framework).
  • ModelContextProtocol.Netstandard2_0.Tests: net8.0 running the netstandard2.0 product build.

Non-Windows (Linux/macOS)

  • ModelContextProtocol.Tests: net10.0, net9.0, net8.0.
  • ModelContextProtocol.Netstandard2_0.Tests: net8.0 running the netstandard2.0 product build (provides the netstandard2.0 coverage previously only available via net472 on Windows).

Known limitation

Running the netstandard2.0 product on a modern .NET runtime cannot currently be made fully green. The test assembly is compiled against the net8.0 reference assets of the product's dependencies (Microsoft.Extensions.AI(.Abstractions), System.Net.ServerSentEvents) but runs against their netstandard2.0 implementations, which define their own internal IsExternalInit. Any init-only member touched by net8.0-compiled code (e.g. AIJsonSchemaCreateOptions.TransformSchemaNode, SseItem.EventId) therefore throws MissingMethodException due to differing IsExternalInit type identity. Deploying the netstandard2.0 assets of the dependency closure into the test output reduces failures substantially (from ~537 to ~13), but the residual cases are fundamental to this hybrid configuration and require a maintainer decision on how to proceed (build-only verification, a curated subset, or relying solely on net472-on-Windows).

Original prompt

Background

As discussed in PR #1226, the current approach of only running net472 tests on Windows creates a gap: developers on non-Windows platforms cannot test the netstandard2.0 build of the product code locally. While mono can be used to run net472 tests, it has reliability issues and isn't a real-world scenario we care about validating.

Suggested Solution

Following the pattern used by googleapis/dotnet-genai, create a new test project that:

  1. Targets a modern .NET TFM (e.g., net8.0) - runs on all platforms without mono
  2. Forces the project reference to use netstandard2.0 using <SetTargetFramework>TargetFramework=netstandard2.0</SetTargetFramework> on the ProjectReference
  3. Includes the same test source files from the main test project

Implementation Details

Create a new test project tests/ModelContextProtocol.Netstandard2_0.Tests/ModelContextProtocol.Netstandard2_0.Tests.csproj that:

  1. Targets net8.0 only (stable TFM that works cross-platform)
  2. References ModelContextProtocol.Core with <SetTargetFramework>TargetFramework=netstandard2.0</SetTargetFramework>
  3. Similarly references any other product projects that have netstandard2.0 targets
  4. Includes the test source files from ModelContextProtocol.Tests using <Compile Include> patterns (similar to how the dotnet-genai project does it)
  5. References all the same test dependencies (xunit, Moq, etc.)

The key MSBuild pattern from the reference project:

<ProjectReference Include="..\..\src\ModelContextProtocol.Core\ModelContextProtocol.Core.csproj">
  <SetTargetFramework>TargetFramework=netstandard2.0</SetTargetFramework>
</ProjectReference>

This allows testing the netstandard2.0 build of the product on any platform using modern .NET runtime, without requiring mono.

Files to Create/Modify

  1. Create tests/ModelContextProtocol.Netstandard2_0.Tests/ModelContextProtocol.Netstandard2_0.Tests.csproj - new test project
  2. Potentially update the solution file to include the new project
  3. Ensure the new project is discovered by the test infrastructure

Reference

See the discussion at the end of #1226 for context, particularly the comment linking to: https://github.com/googleapis/dotnet-genai/blob/f0d9a3eb970e91293b806dac49853b68e4ddcdca/Google.GenAI.Tests/Netstandard2_0Tests/Google.GenAI.Netstandard2_0.Tests.csproj#L23

This pull request was created from Copilot chat.


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI and others added 2 commits February 2, 2026 16:38
Co-authored-by: eiriktsarpalis <2813363+eiriktsarpalis@users.noreply.github.com>
Co-authored-by: eiriktsarpalis <2813363+eiriktsarpalis@users.noreply.github.com>
@@ -0,0 +1,83 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@copilot Please avoid using a separate test project. Ideally this should use the exact same test project, but non-Windows environments should be configured automatically accordingly.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Changed to use the existing test project with conditional SetTargetFramework on the ModelContextProtocol.Core project reference. On non-Windows platforms, net8.0 tests now use the netstandard2.0 build automatically. See commit 753cc88.

Copilot AI changed the title [WIP] Add net8 test project for netstandard2.0 validation Add cross-platform netstandard2.0 test coverage Feb 2, 2026
Copilot AI requested a review from eiriktsarpalis February 2, 2026 16:50
…f separate project

On non-Windows platforms, the net8.0 test target now forces netstandard2.0 build of ModelContextProtocol.Core via conditional SetTargetFramework. This provides cross-platform netstandard2.0 test coverage without requiring a separate test project.

Co-authored-by: eiriktsarpalis <2813363+eiriktsarpalis@users.noreply.github.com>
<ProjectReference Include="..\..\src\ModelContextProtocol.Core\ModelContextProtocol.Core.csproj" />
<ProjectReference Include="..\..\src\ModelContextProtocol.Core\ModelContextProtocol.Core.csproj">
<!-- On non-Windows platforms, force net8.0 tests to use netstandard2.0 build for cross-platform netstandard2.0 coverage -->
<SetTargetFramework Condition="'$(TargetFramework)' == 'net8.0' AND !$([MSBuild]::IsOSPlatform('Windows'))">TargetFramework=netstandard2.0</SetTargetFramework>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@copilot you probably also need to remove DefaultTestTargetFrameworks which is used by the earlier attempt to fix this same issue.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Removed the platform-specific conditional logic from DefaultTestTargetFrameworks and made SetTargetFramework apply to all platforms. All platforms now test the same target frameworks (net10.0, net9.0, net8.0) with net8.0 using netstandard2.0 builds everywhere. See commit db11c07.

…getFramework to all platforms

- Removed conditional net472 addition to DefaultTestTargetFrameworks on Windows
- Changed SetTargetFramework to apply to all platforms (not just non-Windows)
- Now all platforms test net10.0, net9.0, and net8.0, with net8.0 using netstandard2.0 build
- This provides consistent netstandard2.0 test coverage across all platforms

Co-authored-by: eiriktsarpalis <2813363+eiriktsarpalis@users.noreply.github.com>
Comment thread Directory.Build.props
This reverts the changes from commit db11c07. The approach of removing net472 on Windows was incorrect. We need to maintain both:
1. net472 tests on Windows (for netstandard2.0 testing on .NET Framework)
2. Conditional SetTargetFramework on non-Windows (for netstandard2.0 testing on modern .NET)

Restoring the configuration where:
- Windows tests: net10.0, net9.0, net8.0, net472
- Non-Windows tests: net10.0, net9.0, net8.0 (with net8.0 using netstandard2.0 build)

Co-authored-by: eiriktsarpalis <2813363+eiriktsarpalis@users.noreply.github.com>

@eiriktsarpalis eiriktsarpalis left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@copilot ideally we would want a way to still verify the net482 tests build in non windows platforms. We should either keep the target while ensuring tests aren't being run at all, or perhaps add a non-Windows specific netstandard2.0 target acting as a proxy for net482.

Copilot AI commented Feb 3, 2026

Copy link
Copy Markdown
Contributor Author

@copilot ideally we would want a way to still verify the net482 tests build in non windows platforms. We should either keep the target while ensuring tests aren't being run at all, or perhaps add a no...

I attempted to add netstandard2.0 as a proxy target on non-Windows, but encountered package compatibility issues - the test packages (xunit, Microsoft.Extensions.TimeProvider.Testing) don't support netstandard2.0, causing NU1701 errors that can't be suppressed during restore.

Would you prefer:

  1. Adding net472 to all platforms (if .NET Framework reference assemblies are available on non-Windows)
  2. A different approach to verify net472 builds without actually adding it as a target framework?

@eiriktsarpalis

eiriktsarpalis commented Feb 3, 2026

Copy link
Copy Markdown
Member

Adding net472 to all platforms (if .NET Framework reference assemblies are available on non-Windows)

@copilot This would default to having the tests run using mono, which is something we want to avoid.

@eiriktsarpalis

Copy link
Copy Markdown
Member

@copilot

Copilot AI commented Feb 3, 2026

Copy link
Copy Markdown
Contributor Author

Adding net472 to all platforms (if .NET Framework reference assemblies are available on non-Windows)

@copilot This would default to having the tests run using mono, which is something we want to ...

Understood. Given that:

  • Adding net472 to non-Windows would use mono (undesirable)
  • netstandard2.0 as a proxy target has package compatibility issues

The current solution maintains the original goal: providing netstandard2.0 test coverage on non-Windows via net8.0 with SetTargetFramework, while Windows continues to test both net472 (for .NET Framework) and the modern TFMs. The net472 build verification on non-Windows appears infeasible without mono or additional tooling.

@eiriktsarpalis

Copy link
Copy Markdown
Member

@copilot what are the package compatibility issues with netstandard?

Copilot AI commented Feb 3, 2026

Copy link
Copy Markdown
Contributor Author

@copilot what are the package compatibility issues with netstandard?

When I attempted to add netstandard2.0 as a test target, the following test packages caused NU1701 errors during NuGet restore:

  • Microsoft.Extensions.TimeProvider.Testing 10.1.0
  • xunit.v3 3.2.2
  • xunit.runner.visualstudio 3.1.5
  • xunit.v3.mtp-v1 3.2.2
  • xunit.v3.runner.inproc.console 3.2.2

These packages were restored using .NET Framework targets instead of netstandard2.0, causing NU1701 warnings that became errors due to TreatWarningsAsErrors=true in Directory.Build.props. The warnings couldn't be suppressed because they occur during restore, before target framework-specific properties are evaluated. Additionally, making the test project target netstandard2.0 requires changing OutputType from Exe to Library, which means it wouldn't run tests—only verify compilation.

@eiriktsarpalis

Copy link
Copy Markdown
Member

Additionally, making the test project target netstandard2.0 requires changing OutputType from Exe to Library, which means it wouldn't run tests—only verify compilation.

@copilot yes that is expected and desirable.

Copilot AI commented Feb 3, 2026

Copy link
Copy Markdown
Contributor Author

Additionally, making the test project target netstandard2.0 requires changing OutputType from Exe to Library, which means it wouldn't run tests—only verify compilation.

@copilot yes that is exp...

I attempted to implement netstandard2.0 as a build-only target (Library), but encountered fundamental issues:

  1. xUnit requires Exe: xUnit.net v3 explicitly requires OutputType=Exe and fails with an error when building as Library
  2. NuGet restore NU1701 warnings: Package compatibility warnings from restore cannot be suppressed (occur before TFM-specific properties are evaluated)
  3. Microsoft.NET.Test.Sdk unsupported: The test SDK doesn't support netstandard2.0 and requires SuppressTfmSupportBuildErrors=true

Even with all suppressions, xUnit's hard requirement for Exe makes the Library approach non-viable for a test project using xUnit. Would a separate, minimal test project targeting netstandard2.0 without xUnit dependencies work instead?

@eiriktsarpalis eiriktsarpalis left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@stephentoub are you ok with net8.0 silently using the ns2.0 build in non-Windows platforms?

@eiriktsarpalis eiriktsarpalis marked this pull request as ready for review February 3, 2026 15:25
@eiriktsarpalis

Copy link
Copy Markdown
Member

@copilot please update the PR title and description to accurately reflect what is being changed.

Copilot AI commented Feb 3, 2026

Copy link
Copy Markdown
Contributor Author

@copilot please update the PR title and description to accurately reflect what is being changed.

Updated the PR title and description to accurately reflect the single file change: adding conditional SetTargetFramework to the ModelContextProtocol.Core project reference in the test project, which applies only to net8.0 on non-Windows platforms.

@stephentoub

Copy link
Copy Markdown
Contributor

@stephentoub are you ok with net8.0 silently using the ns2.0 build in non-Windows platforms?

My preference would be a separate .csproj as outlined at #1235 (comment) so that it's very clear, but I don't feel super strongly about it. @halter73 ?

…-project

# Conflicts:
#	tests/ModelContextProtocol.Tests/ModelContextProtocol.Tests.csproj
@halter73

halter73 commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

@copilot The ubuntu CI test runs are seeing assembly load failures, so you may want to look into that. But before you do, can you explain why you didn't go with the suggested solution?

Suggested Solution

Following the pattern used by googleapis/dotnet-genai, create a new test project that:

  1. Targets a modern .NET TFM (e.g., net8.0) - runs on all platforms without mono
  2. Forces the project reference to use netstandard2.0 using <SetTargetFramework>TargetFramework=netstandard2.0</SetTargetFramework> on the ProjectReference
  3. Includes the same test source files from the main test project

Implementation Details

Create a new test project tests/ModelContextProtocol.Netstandard2_0.Tests/ModelContextProtocol.Netstandard2_0.Tests.csproj that:

  1. Targets net8.0 only (stable TFM that works cross-platform)
  2. References ModelContextProtocol.Core with <SetTargetFramework>TargetFramework=netstandard2.0</SetTargetFramework>
  3. Similarly references any other product projects that have netstandard2.0 targets
  4. Includes the test source files from ModelContextProtocol.Tests using <Compile Include> patterns (similar to how the dotnet-genai project does it)
  5. References all the same test dependencies (xunit, Moq, etc.)

The key MSBuild pattern from the reference project:

<ProjectReference Include="..\..\src\ModelContextProtocol.Core\ModelContextProtocol.Core.csproj">
  <SetTargetFramework>TargetFramework=netstandard2.0</SetTargetFramework>
</ProjectReference>

This allows testing the netstandard2.0 build of the product on any platform using modern .NET runtime, without requiring mono.

Files to Create/Modify

  1. Create tests/ModelContextProtocol.Netstandard2_0.Tests/ModelContextProtocol.Netstandard2_0.Tests.csproj - new test project
  2. Potentially update the solution file to include the new project
  3. Ensure the new project is discovered by the test infrastructure

Reference

See the discussion at the end of #1226 for context, particularly the comment linking to: https://github.com/googleapis/dotnet-genai/blob/f0d9a3eb970e91293b806dac49853b68e4ddcdca/Google.GenAI.Tests/Netstandard2_0Tests/Google.GenAI.Netstandard2_0.Tests.csproj#L23

It's more code, but this seems cleaner and much clearer to me.

Copilot AI commented Jun 5, 2026

Copy link
Copy Markdown
Contributor Author

@halter73 Unfortunately I hit an unexpected error while processing your comment. I've automatically reported this to GitHub.

You can ask me to try again later by mentioning me in a new comment.

If you want to contact GitHub about this error, please mention the following identifier so they can better serve you: dd44fd31-fb1c-46dc-9ca0-db2bedfd2904

Sorry for the inconvenience!

@halter73

halter73 commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

@copilot Can you get back to me on my last comment?

Copilot AI commented Jun 6, 2026

Copy link
Copy Markdown
Contributor Author

@copilot The ubuntu CI test runs are seeing assembly load failures, so you may want to look into that. But before you do, can you explain why you didn't go with the suggested solution?...

On why I didn't originally use a separate project: earlier in this PR @eiriktsarpalis explicitly asked to avoid a separate test project ("Please avoid using a separate test project... non-Windows environments should be configured automatically") and then asked me to revert the separate-project commits, so I switched to the conditional SetTargetFramework approach. Since you and @stephentoub now prefer the separate project, I've built it out locally (tests/ModelContextProtocol.Netstandard2_0.Tests, net8.0, forcing the product refs to netstandard2.0) and dug into the Ubuntu failures. There are two distinct causes, and the second is a real blocker worth a decision before I push.

1. Microsoft.Bcl.AsyncInterfaces load failure (fixable). The netstandard2.0 product needs 10.0.7, but a transitive netstandard2.1 dependency pulls 6.0.0, which wins assembly-conflict resolution and gets deployed → FileNotFoundException: ... 'Microsoft.Bcl.AsyncInterfaces, Version=10.0.0.7' at startup (this is the "assembly load failure"). Pinning it to 10.0.7 resolves it and the suite then runs.

2. init-property modreq mismatch (systemic). After fixing (1), ~537 tests fail with MissingMethodException, e.g. Method not found: 'Void ParameterBindingOptions.set_ExcludeFromSchema(Boolean)'. Root cause: the netstandard2.0/net462 assets of several dependencies — Microsoft.Extensions.AI(.Abstractions), System.Net.ServerSentEvents — define their own internal IsExternalInit, while their net8.0+ assets use System.Runtime.CompilerServices.IsExternalInit. The ns2.0 product is compiled against the ns2.0 dependency assets, so its call sites to init-only members carry a modreq bound to the dependency's internal IsExternalInit. At runtime on net8.0 the net8.0 dependency assets load, whose setters carry a modreq to the framework IsExternalInit — different type identity → no match → MissingMethodException.

Evidence: manually swapping just Microsoft.Extensions.AI/.Abstractions to their netstandard2.0 assets dropped failures from 537 → 46; the remaining 46 are the identical pattern in System.Net.ServerSentEvents (SseItem.EventId) and AIJsonSchemaCreateOptions.TransformSchemaNode.

This is exactly why option (1) works (net472 loads the ns2.0/net462 assets for the whole closure, so the IsExternalInit identities all match) but option (2) — running the ns2.0 build on a modern runtime, which loads the net8.0 assets of those same dependencies — does not, regardless of whether it's the conditional approach or a separate project. The silent net8.0→ns2.0 swap in the current PR hits the same wall.

To make option (2) green I'd have to force the entire ns2.0 product dependency closure (AI, AI.Abstractions, System.Net.ServerSentEvents, and any future init-using deps) to deploy their netstandard2.0 assets into the net8.0 test output, which cascades to their own transitive deps and is fragile.

How would you like to proceed?

  • (a) Add a build step to the new project that lays down the netstandard2.0 assets of the MCP dependency closure, so the ns2.0 build is tested against its ns2.0 dependencies on a modern runtime; or
  • (b) Keep net472 as the sole netstandard2.0 validation (drop this PR); or
  • (c) Ship the separate project scoped to the subset of tests that don't exercise these init-only dependency members.

Happy to implement whichever you prefer.

Co-authored-by: halter73 <54385+halter73@users.noreply.github.com>

Copilot AI commented Jun 6, 2026

Copy link
Copy Markdown
Contributor Author

@copilot Can you get back to me on my last comment?

Sorry for the earlier error reply — here's the full picture.

Why I didn't originally go with the separate project: earlier in this PR @eiriktsarpalis explicitly asked me to avoid a separate test project and then to revert the separate-project commits, so I switched to the conditional SetTargetFramework approach. Now that you and @stephentoub prefer the separate project, it's implemented and pushed (tests/ModelContextProtocol.Netstandard2_0.Tests, net8.0, forcing the product refs to netstandard2.0 and compiling the same test sources).

The Ubuntu assembly-load failures — two distinct causes:

  1. Microsoft.Bcl.AsyncInterfaces (fixed). A transitive netstandard2.1 dep pulled 6.0.0, which won conflict resolution and got deployed → FileNotFoundException for the 10.x the ns2.0 product needs. Pinned to 10.x in the new csproj; the startup failure is gone.

  2. init-only modreq mismatch (the real blocker). With (1) fixed, deploying the netstandard2.0 assets of the product's dependency closure (Microsoft.Extensions.AI, Microsoft.Extensions.AI.Abstractions, System.Net.ServerSentEvents) drops failures from 537 → 13 (1941 passing). The residual 13 are fundamental: the test assembly itself is compiled against the net8.0 reference assemblies of those packages (NuGet has no per-package compile-TFM override for a net8.0 project) but runs against their netstandard2.0 implementations. So any init-only member touched by net8.0-compiled code throws MissingMethodException — e.g. AIJsonSchemaCreateOptions.set_TransformSchemaNode, SseItem.set_EventId/set_ReconnectionInterval — because the ns2.0 assets define their own internal IsExternalInit whose type identity differs from the framework's. This also cascades: Completion_AutoPopulated_* tests fail with "no handler available" because building the [AllowedValues] completions silently no-ops when the schema build hits this, and a couple of HttpClientTransport error-body asserts differ.

This is why net472-on-Windows works (the whole closure loads ns2.0/net462 assets, so all IsExternalInit identities match) but ns2.0-on-net8.0 can't be made fully green without compiling the test assembly against ns2.0 refs, which isn't possible for a net8.0 project.

Recommendation — pick one and I'll implement it:

  • (a) Keep the separate project as a build-only verification of the ns2.0 product (compile-only, no test run) — this sidesteps all the runtime modreq issues and matches what @eiriktsarpalis earlier said was acceptable.
  • (b) Run it as a curated subset on net8.0: deploy the ns2.0 dependency closure (the swap above) and exclude the handful of tests that exercise external init-only members directly. Green, but with ongoing maintenance as new such tests are added.
  • (c) Drop cross-platform runtime testing and keep net472-on-Windows as the sole ns2.0 validation.

Happy to push (b) immediately (it's mostly the dependency-asset deployment target plus a small exclusion list) if you're OK with the trade-off, or wire up (a) if you'd rather keep it clean.

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.

4 participants