
Code coverage tooling is easy to treat as a test-only detail until a repository starts targeting more than one framework. At that point the coverage package is no longer just a command-line convenience. It participates in restore, build, instrumentation, test-host startup, and report generation for every target framework moniker (TFM) in the solution.
That is why Codebelt.Coverlet.MTP exists.
It is a deliberately narrow fork of upstream Coverlet, focused on one problem: using Microsoft Testing Platform coverage in mixed-target .NET test estates without forcing every consumer onto one latest-generation transitive graph. The package keeps the Coverlet engine and Microsoft Testing Platform extension model recognizable, but it publishes a different compatibility contract.
That distinction matters more than it first appears.
The official baseline is already good
Microsoft describes Microsoft.Testing.Platform as a lightweight, portable test platform with compile-time extension registration. Extensions are discovered through MSBuild integration and builder hooks rather than late-bound probing tricks. That is a strong baseline for deterministic test execution.
Upstream coverlet.MTP builds on that model. Its job is to bring Coverlet's coverage collection into the Microsoft Testing Platform world. It auto-registers through TestingPlatformBuilderHook, exposes the familiar --coverlet command-line surface, and keeps the implementation assembly name coverlet.MTP.dll so the platform discovers the extension the usual way.
That is also why this fork is intentionally restrained.
Codebelt.Coverlet.MTP is not a new coverage engine. It does not attempt to replace Coverlet's core instrumentation logic. It keeps the same extension identity where that identity matters to Microsoft Testing Platform, and it keeps full credit with the upstream Coverlet project where the hard work already lives.
The change is elsewhere: package identity, target frameworks, dependency groups, and the promise those groups make to consumers.
A TFM is part of the package contract
Microsoft's cross-platform targeting guidance for .NET libraries is blunt about the difference between broad compatibility and modern runtime capabilities. A package should target a framework because that target represents a meaningful compatibility or implementation boundary, not merely because another runtime version exists.
NuGet then turns that target choice into behavior. The NuGet package compatibility rules and multi-targeting documentation both make the same underlying point:
- NuGet picks the best matching asset for the active target framework.
- It resolves dependencies per target framework group.
- Compatible frameworks in one package must still expose compatible assemblies.
- Dependency groups are part of the public contract, not internal implementation trivia.
That means a package can keep the same public API surface and still change what a consumer restores, compiles against, and carries transitively.
For a coverage package, that is not a theoretical concern. Coverage packages sit close to the test host. They pull in Microsoft.Extensions.*, configuration pieces, instrumentation dependencies, and runtime-specific helpers. If those dependencies are flattened to the newest generation everywhere, a package can become technically installable while still expressing a compatibility story that is broader or newer than some consumers want to accept.
What Codebelt.Coverlet.MTP changes
The current package project is explicit:
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net9.0;net10.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
<PackageReference Include="Microsoft.Extensions.Configuration" VersionOverride="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" VersionOverride="8.0.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" VersionOverride="8.0.1" />
</ItemGroup>
The rest of the dependency alignment comes from the repository's central package management. Directory.Packages.props resolves the runtime generation per TFM for shipping projects: .NET 10 uses 10.0.12, .NET 9 uses 9.0.20, and the netstandard2.0 group keeps explicit 8.0.x overrides in the project file.
That is the core idea.
This is not cargo-cult multi-targeting. The package does not target net8.0 merely because .NET 8 exists. A .NET 8 consumer can resolve the netstandard2.0 asset, which is the broad compatibility floor. The explicit runtime-specific groups start at net9.0 and net10.0, where the dependency-generation boundary is treated as meaningful enough to deserve its own contract.
Just as important, the fork preserves Microsoft Testing Platform discovery:
- the implementation assembly remains
coverlet.MTP.dll; - the builder hook type remains
Coverlet.MTP.TestingPlatformBuilderHook; - the build-transitive props still register the extension assembly under
lib\$(TargetFramework).
So the package shape changes, but the integration contract that the test platform expects does not.
Why the fork exists at all
Upstream coverlet.MTP 10.1.0 already multi-targets, and that point deserves to be stated fairly. Its NuGet catalog shows dependency groups for netstandard2.0, net8.0, and net10.0.
The difference is that those groups still depend on Microsoft.Extensions.Configuration, Microsoft.Extensions.Configuration.Json, and Microsoft.Extensions.DependencyInjection version 10.0.12 across all three groups.
That can be a perfectly acceptable trade-off for some repositories. If your test estate is already aligned to that dependency generation, the upstream package may fit your needs well.
But that is exactly the trade-off Codebelt declines to impose universally.
Codebelt.Coverlet.MTP narrows the contract so the package can say something more precise:
- broad compatibility floor:
netstandard2.0; - explicit runtime-aligned dependency group for
.NET 9; - explicit runtime-aligned dependency group for
.NET 10; - preserved Coverlet and MTP discovery behavior.
In other words, the fork is not about changing how coverage works. It is about changing how honestly the package describes who gets which dependency graph.
Two dependency-graph examples explain the problem
The general principle is not unique to Coverlet.
Microsoft.Data.SqlClient 5.2.3 is a useful counterexample. It did not drop .NETStandard2.0. Its NuGet metadata still carried separate reference and dependency groups for .NETFramework 4.6.2, net6.0, net8.0, .NETStandard2.0, and .NETStandard2.1. That is what a truthful package contract looks like when different consumers legitimately need different dependency shapes.
Azure.Identity 1.18.0 illustrates the opposite pressure. That release added experimental Microsoft.Extensions.Configuration and dependency injection integration, and its package metadata added Microsoft.Extensions.Configuration.Abstractions and Microsoft.Extensions.Hosting.Abstractions version 10.0.3 to the net8.0, net10.0, and .NETStandard2.0 groups. The public API story was additive, but the transitive contract widened for every consumer. Azure.Identity 1.19.0 retained that broader dependency shape.
Neither example proves that one package is universally right and the other universally wrong. They do show why TFM and dependency-group decisions deserve first-class attention:
- a package's public API is only part of its contract;
- dependency groups change what the consumer must restore and load;
- multi-targeting is most valuable when it publishes the narrowest truthful contract per consumer;
- "latest generation everywhere" is a policy choice, not a neutral default.
That is the engineering space Codebelt.Coverlet.MTP is designed for.
Basic usage
The consumer story stays intentionally familiar. An MTP-enabled test project still references one coverage package and runs coverage the normal way:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net9.0;net10.0</TargetFrameworks>
<OutputType>Exe</OutputType>
<UseMicrosoftTestingPlatformRunner>true</UseMicrosoftTestingPlatformRunner>
<TestingPlatformDotnetTestSupport>true</TestingPlatformDotnetTestSupport>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="xunit.v3.mtp-v2" Version="4.0.1" />
<PackageReference Include="Microsoft.Testing.Platform" Version="2.4.1" />
<PackageReference Include="Codebelt.Coverlet.MTP" Version="10.1.0" />
</ItemGroup>
</Project>
Then run:
dotnet test --coverlet
That is an important part of the design. The fork changes package identity and dependency boundaries, not the everyday developer workflow for collecting coverage.
Recommended usage
Choose Codebelt.Coverlet.MTP when these conditions are true:
- your tests already run on Microsoft Testing Platform;
- the repository targets more than one current .NET framework, or needs a compatibility floor that should not inherit the newest runtime generation by default;
- your team treats transitive dependency shape as part of the package contract;
- you want to keep the familiar Coverlet MTP experience while being more deliberate about framework-aligned restore graphs.
Stay with upstream coverlet.MTP when the canonical upstream package and its current dependency shape already fit the estate you are maintaining.
Use the broader upstream Coverlet packages when you need drivers outside this fork's stated scope, such as the console tool, VSTest collector, or MSBuild integration.
There is one more caveat worth stating plainly. Multi-targeting is not permission to let compatible frameworks drift apart semantically. Microsoft's package guidance is explicit that compatible TFM assets must remain compatible with one another. If a package publishes multiple framework groups, it also inherits the responsibility to validate that those groups stay consistent. The TFM contract should get narrower and more truthful, not more surprising.
This package is narrow on purpose: keep Coverlet's Microsoft Testing Platform driver recognizable, keep the coverage workflow familiar, and publish a dependency contract that does not ask every consumer to pretend .NET 8, .NET 9, and .NET 10 are the same thing.
Timeline
The package's current shape makes more sense when read as Codebelt's first stable MTP release preserving the upstream extension contract while narrowing the public package boundary and compatibility policy.
v10.0.12026-09-20Codebelt first released the fork and published its narrower package contract.
Codebelt.Coverlet.MTPfirst shipped as a Codebelt package in 10.0.1. That release kept the inheritedCoverlet.MTP.TestingPlatformBuilderHookdiscovery model and thecoverlet.MTP.dllextension assembly identity, but changed the NuGet-facing contract toCodebelt.Coverlet.MTPand narrowed the supported TFMs tonetstandard2.0,net9.0, andnet10.0.It also aligned centrally managed runtime package versions by target framework generation, so the package could carry framework-appropriate dependency groups instead of projecting one newest-generation graph across every consumer.
v10.1.02026-09-27(current)The current release synchronizes upstream fixes without changing the Codebelt package boundary.
The active source of truth still lives in
src/coverlet.MTP, withCodebelt.Coverlet.MTPas the package ID,coverlet.MTP.dllas the extension assembly, and build-transitive registration pointing Microsoft Testing Platform at the TFM-specific library output. The 10.1.0 release adds the upstream--config-filefix and related MTP corrections while explicitly preserving the fork's multi-targeted package layout.
Sources
- Codebelt coverlet family page
- Codebelt.Coverlet.MTP package page
- Codebelt.Coverlet.MTP README
- Codebelt.Coverlet.MTP package project
- Codebelt coverlet central package versions
- Codebelt coverlet changelog
- Coverlet Microsoft Testing Platform integration documentation
- Microsoft.Testing.Platform overview
- Testing with
dotnet testand Microsoft Testing Platform - Build extensions for Microsoft.Testing.Platform
- Cross-platform targeting for .NET libraries
- NuGet package compatibility rules
- Multi-targeting for NuGet packages
- .NET Package Validation
- Codebelt.Coverlet.MTP 10.1.0 NuGet catalog entry
- coverlet.MTP 10.1.0 NuGet catalog entry
- Microsoft.Data.SqlClient 5.2.3 release notes
- Microsoft.Data.SqlClient 5.2.3 catalog entry
- Azure.Identity changelog
- Azure.Identity 1.17.2 catalog entry
- Azure.Identity 1.18.0 catalog entry
- Azure.Identity 1.19.0 catalog entry
