Codebelt
Codebelt.Coverlet.MTP icon with a coverage shield across three target framework lanes.

Codebelt.Coverlet.MTP and TFM Contracts

Codebelt.Coverlet.MTP packages Coverlet's Microsoft Testing Platform driver with explicit netstandard2.0, net9.0, and net10.0 dependency groups, so mixed-target .NET test estates can collect coverage without flattening every consumer onto one latest-generation transitive graph.

LIBRARIES
  • .NET
  • Code Coverage
  • Coverlet
  • Microsoft Testing Platform
  • NuGet
  • Multi-Targeting
  • TFM
  • Dependency Graph
Coverlet coverage pipeline spanning netstandard2.0, .NET 9, and .NET 10 dependency lanes while preserving one Microsoft Testing Platform entry point.Coverlet coverage pipeline spanning netstandard2.0, .NET 9, and .NET 10 dependency lanes while preserving one Microsoft Testing Platform entry point.

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.

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.

  1. v10.0.1 2026-09-20

    Codebelt first released the fork and published its narrower package contract.

    Codebelt.Coverlet.MTP first shipped as a Codebelt package in 10.0.1. That release kept the inherited Coverlet.MTP.TestingPlatformBuilderHook discovery model and the coverlet.MTP.dll extension assembly identity, but changed the NuGet-facing contract to Codebelt.Coverlet.MTP and narrowed the supported TFMs to netstandard2.0, net9.0, and net10.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.

  2. v10.1.0 2026-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, with Codebelt.Coverlet.MTP as the package ID, coverlet.MTP.dll as 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-file fix and related MTP corrections while explicitly preserving the fork's multi-targeted package layout.

Sources