From a0c7ca974acece17374a50946a28bff00576a9e4 Mon Sep 17 00:00:00 2001
From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com>
Date: Sat, 28 Mar 2026 11:31:05 +0000
Subject: [PATCH 1/7] fix: prevent TUnit props from flowing to transitive
consumers (#5277)
Previously, TUnit.Core.props, TUnit.Assertions.props, and TUnit.Engine.props
were packed in both build/ and buildTransitive/ NuGet paths, causing projects
that only transitively depend on TUnit (via ProjectReference) to get:
- TUnitImplicitUsings=true (adding `using TUnit.Core;` causing CS0104 ambiguity)
- EnableTUnitSourceGeneration=true (source generators running on non-test projects)
- IsTestProject=true and OutputType=Exe (from TUnit.Engine)
Fix:
- Create separate minimal buildTransitive props/targets for each package
- TUnit.Core transitive: only Polyfill configuration (needed for compilation compat)
- TUnit.Assertions transitive: empty (no implicit usings)
- TUnit.Engine transitive: empty (no test project settings)
- Add missing condition on TUnitImplicitUsings to respect user overrides
(matching TUnit.Assertions.props which already had this condition)
---
.../TUnit.Assertions.buildTransitive.props | 4 ++
.../TUnit.Assertions.buildTransitive.targets | 4 ++
TUnit.Assertions/TUnit.Assertions.csproj | 20 +++++-----
TUnit.Core/TUnit.Core.buildTransitive.props | 25 ++++++++++++
TUnit.Core/TUnit.Core.buildTransitive.targets | 38 +++++++++++++++++++
TUnit.Core/TUnit.Core.csproj | 20 +++++-----
TUnit.Core/TUnit.Core.props | 2 +-
.../TUnit.Engine.buildTransitive.props | 4 ++
TUnit.Engine/TUnit.Engine.csproj | 12 +++---
9 files changed, 105 insertions(+), 24 deletions(-)
create mode 100644 TUnit.Assertions/TUnit.Assertions.buildTransitive.props
create mode 100644 TUnit.Assertions/TUnit.Assertions.buildTransitive.targets
create mode 100644 TUnit.Core/TUnit.Core.buildTransitive.props
create mode 100644 TUnit.Core/TUnit.Core.buildTransitive.targets
create mode 100644 TUnit.Engine/TUnit.Engine.buildTransitive.props
diff --git a/TUnit.Assertions/TUnit.Assertions.buildTransitive.props b/TUnit.Assertions/TUnit.Assertions.buildTransitive.props
new file mode 100644
index 0000000000..b78e2e8ea3
--- /dev/null
+++ b/TUnit.Assertions/TUnit.Assertions.buildTransitive.props
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/TUnit.Assertions/TUnit.Assertions.buildTransitive.targets b/TUnit.Assertions/TUnit.Assertions.buildTransitive.targets
new file mode 100644
index 0000000000..b78e2e8ea3
--- /dev/null
+++ b/TUnit.Assertions/TUnit.Assertions.buildTransitive.targets
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/TUnit.Assertions/TUnit.Assertions.csproj b/TUnit.Assertions/TUnit.Assertions.csproj
index 1809c6042f..1596d86ec8 100644
--- a/TUnit.Assertions/TUnit.Assertions.csproj
+++ b/TUnit.Assertions/TUnit.Assertions.csproj
@@ -27,25 +27,27 @@
-
+
+
true
- $(BuildTransitivePath)
-
+ $(BuildPath)
+
-
+
true
$(BuildPath)
-
+
+
true
- $(BuildTransitivePath)
+ $(BuildTransitivePath)TUnit.Assertions.props
-
+
true
- $(BuildPath)
-
+ $(BuildTransitivePath)TUnit.Assertions.targets
+
diff --git a/TUnit.Core/TUnit.Core.buildTransitive.props b/TUnit.Core/TUnit.Core.buildTransitive.props
new file mode 100644
index 0000000000..4dce10fa07
--- /dev/null
+++ b/TUnit.Core/TUnit.Core.buildTransitive.props
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+ true
+
+ true
+
+
+
+
+
+ <_PolyfillPackageReference Include="@(PackageReference)"
+ Condition="'%(PackageReference.Identity)' == 'Polyfill'" />
+ <_PolyfillPackageVersion Include="@(PackageVersion)"
+ Condition="'%(PackageVersion.Identity)' == 'Polyfill'" />
+
+
+ <_PolyfillAlreadyDefined Condition="'@(_PolyfillPackageReference)' != '' Or '@(_PolyfillPackageVersion)' != ''">true
+
+
+
+
diff --git a/TUnit.Core/TUnit.Core.buildTransitive.targets b/TUnit.Core/TUnit.Core.buildTransitive.targets
new file mode 100644
index 0000000000..4df8f09e59
--- /dev/null
+++ b/TUnit.Core/TUnit.Core.buildTransitive.targets
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+ <_TUnitPolyfillVersion>9.23.0
+ <_TUnitNeedsPolyfill Condition="'$(TargetFramework)' == 'netstandard2.0' or '$(TargetFramework)' == 'netstandard2.1' or '$(TargetFrameworkIdentifier)' == '.NETFramework'">true
+
+
+
+
+
+ all
+ compile; analyzers
+ runtime; native; contentfiles; build; buildtransitive
+
+
+
+
+
+
+ all
+ compile; analyzers
+ runtime; native; contentfiles; build; buildtransitive
+
+
+
+
+
+
+
+
+
diff --git a/TUnit.Core/TUnit.Core.csproj b/TUnit.Core/TUnit.Core.csproj
index c0c1082638..0da0cef5d1 100644
--- a/TUnit.Core/TUnit.Core.csproj
+++ b/TUnit.Core/TUnit.Core.csproj
@@ -11,25 +11,27 @@
-
+
+
true
- $(BuildTransitivePath)
-
+ $(BuildPath)
+
-
+
true
$(BuildPath)
-
+
+
true
- $(BuildTransitivePath)
+ $(BuildTransitivePath)TUnit.Core.props
-
+
true
- $(BuildPath)
-
+ $(BuildTransitivePath)TUnit.Core.targets
+
-
+
true
diff --git a/TUnit.Engine/TUnit.Engine.buildTransitive.props b/TUnit.Engine/TUnit.Engine.buildTransitive.props
new file mode 100644
index 0000000000..1340bd7a41
--- /dev/null
+++ b/TUnit.Engine/TUnit.Engine.buildTransitive.props
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/TUnit.Engine/TUnit.Engine.csproj b/TUnit.Engine/TUnit.Engine.csproj
index 33b43df182..e7f4b044b9 100644
--- a/TUnit.Engine/TUnit.Engine.csproj
+++ b/TUnit.Engine/TUnit.Engine.csproj
@@ -21,15 +21,17 @@
-
- true
- $(BuildTransitivePath)
-
-
+
true
$(BuildPath)
+
+
+
+ true
+ $(BuildTransitivePath)TUnit.Engine.props
+
From f16a2cb1e35780f662b534ce71d969a7254b8fc9 Mon Sep 17 00:00:00 2001
From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com>
Date: Sat, 28 Mar 2026 11:39:12 +0000
Subject: [PATCH 2/7] refactor: deduplicate Polyfill logic per review feedback
Move Polyfill detection and package injection to buildTransitive files
only, since NuGet imports buildTransitive for both direct and transitive
consumers. This eliminates duplicate target definitions
(DetectPolyfillReference, _TUnitEnsurePolyfill) and the hardcoded
Polyfill version appearing in two places.
---
TUnit.Core/TUnit.Core.props | 20 +-------------------
TUnit.Core/TUnit.Core.targets | 35 ++---------------------------------
2 files changed, 3 insertions(+), 52 deletions(-)
diff --git a/TUnit.Core/TUnit.Core.props b/TUnit.Core/TUnit.Core.props
index 3fcd4f7c45..b64cb0a031 100644
--- a/TUnit.Core/TUnit.Core.props
+++ b/TUnit.Core/TUnit.Core.props
@@ -20,25 +20,7 @@
true
-
-
-
- <_PolyfillPackageReference Include="@(PackageReference)"
- Condition="'%(PackageReference.Identity)' == 'Polyfill'" />
- <_PolyfillPackageVersion Include="@(PackageVersion)"
- Condition="'%(PackageVersion.Identity)' == 'Polyfill'" />
-
-
- <_PolyfillAlreadyDefined Condition="'@(_PolyfillPackageReference)' != '' Or '@(_PolyfillPackageVersion)' != ''">true
-
-
-
-
-
- true
-
- true
-
+
diff --git a/TUnit.Core/TUnit.Core.targets b/TUnit.Core/TUnit.Core.targets
index 9c9d1416b1..545c45dbf1 100644
--- a/TUnit.Core/TUnit.Core.targets
+++ b/TUnit.Core/TUnit.Core.targets
@@ -1,4 +1,4 @@
-
+
@@ -6,37 +6,6 @@
-
-
- <_TUnitPolyfillVersion>9.23.0
- <_TUnitNeedsPolyfill Condition="'$(TargetFramework)' == 'netstandard2.0' or '$(TargetFramework)' == 'netstandard2.1' or '$(TargetFrameworkIdentifier)' == '.NETFramework'">true
-
-
-
-
-
- all
- compile; analyzers
- runtime; native; contentfiles; build; buildtransitive
-
-
-
-
-
-
- all
- compile; analyzers
- runtime; native; contentfiles; build; buildtransitive
-
-
-
-
-
-
-
+
From a9deb75b175015963e3c5e4d48ba72943c4ee6f1 Mon Sep 17 00:00:00 2001
From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com>
Date: Sat, 28 Mar 2026 11:47:20 +0000
Subject: [PATCH 3/7] fix: restore Polyfill logic to build/ files for direct
consumers
NuGet imports build/ and buildTransitive/ mutually exclusively:
direct consumers only get build/, transitive consumers only get
buildTransitive/. The previous commit incorrectly assumed both
are imported for direct consumers and removed Polyfill logic from
build/ files, which would break Polyfill injection for direct
consumers on older TFMs.
---
TUnit.Core/TUnit.Core.props | 18 +++++++++++++++++-
TUnit.Core/TUnit.Core.targets | 33 ++++++++++++++++++++++++++++++++-
2 files changed, 49 insertions(+), 2 deletions(-)
diff --git a/TUnit.Core/TUnit.Core.props b/TUnit.Core/TUnit.Core.props
index b64cb0a031..9721721a6a 100644
--- a/TUnit.Core/TUnit.Core.props
+++ b/TUnit.Core/TUnit.Core.props
@@ -20,7 +20,23 @@
true
-
+
+
+
+ <_PolyfillPackageReference Include="@(PackageReference)"
+ Condition="'%(PackageReference.Identity)' == 'Polyfill'" />
+ <_PolyfillPackageVersion Include="@(PackageVersion)"
+ Condition="'%(PackageVersion.Identity)' == 'Polyfill'" />
+
+
+ <_PolyfillAlreadyDefined Condition="'@(_PolyfillPackageReference)' != '' Or '@(_PolyfillPackageVersion)' != ''">true
+
+
+
+
+ true
+ true
+
diff --git a/TUnit.Core/TUnit.Core.targets b/TUnit.Core/TUnit.Core.targets
index 545c45dbf1..8b3ecd4f80 100644
--- a/TUnit.Core/TUnit.Core.targets
+++ b/TUnit.Core/TUnit.Core.targets
@@ -6,6 +6,37 @@
-
+
+
+ <_TUnitPolyfillVersion>9.23.0
+ <_TUnitNeedsPolyfill Condition="'$(TargetFramework)' == 'netstandard2.0' or '$(TargetFramework)' == 'netstandard2.1' or '$(TargetFrameworkIdentifier)' == '.NETFramework'">true
+
+
+
+
+
+ all
+ compile; analyzers
+ runtime; native; contentfiles; build; buildtransitive
+
+
+
+
+
+
+ all
+ compile; analyzers
+ runtime; native; contentfiles; build; buildtransitive
+
+
+
+
+
+
+
From bdeaf2e234ce37eb670183bacf700dc53bd40a2f Mon Sep 17 00:00:00 2001
From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com>
Date: Sat, 28 Mar 2026 11:58:15 +0000
Subject: [PATCH 4/7] fix: use build/-only approach (remove buildTransitive
entirely)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Empirical testing revealed that when both build/ and buildTransitive/
exist in a NuGet package, NuGet uses buildTransitive/ for ALL consumers
(direct AND transitive), making build/ dead code. The previous approach
of separate build/buildTransitive files was fundamentally broken.
The correct fix is simpler: only pack props/targets in build/ (no
buildTransitive/ at all). NuGet's build/ folder only applies to direct
consumers, so transitive consumers get nothing — which is exactly what
issue #5277 requests.
This eliminates all the buildTransitive files and the duplication they
caused, while preserving full functionality for direct consumers.
---
.../TUnit.Assertions.buildTransitive.props | 4 --
.../TUnit.Assertions.buildTransitive.targets | 4 --
TUnit.Assertions/TUnit.Assertions.csproj | 12 ------
TUnit.Core/TUnit.Core.buildTransitive.props | 25 ------------
TUnit.Core/TUnit.Core.buildTransitive.targets | 38 -------------------
TUnit.Core/TUnit.Core.csproj | 12 ------
.../TUnit.Engine.buildTransitive.props | 4 --
TUnit.Engine/TUnit.Engine.csproj | 7 ----
8 files changed, 106 deletions(-)
delete mode 100644 TUnit.Assertions/TUnit.Assertions.buildTransitive.props
delete mode 100644 TUnit.Assertions/TUnit.Assertions.buildTransitive.targets
delete mode 100644 TUnit.Core/TUnit.Core.buildTransitive.props
delete mode 100644 TUnit.Core/TUnit.Core.buildTransitive.targets
delete mode 100644 TUnit.Engine/TUnit.Engine.buildTransitive.props
diff --git a/TUnit.Assertions/TUnit.Assertions.buildTransitive.props b/TUnit.Assertions/TUnit.Assertions.buildTransitive.props
deleted file mode 100644
index b78e2e8ea3..0000000000
--- a/TUnit.Assertions/TUnit.Assertions.buildTransitive.props
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
diff --git a/TUnit.Assertions/TUnit.Assertions.buildTransitive.targets b/TUnit.Assertions/TUnit.Assertions.buildTransitive.targets
deleted file mode 100644
index b78e2e8ea3..0000000000
--- a/TUnit.Assertions/TUnit.Assertions.buildTransitive.targets
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
diff --git a/TUnit.Assertions/TUnit.Assertions.csproj b/TUnit.Assertions/TUnit.Assertions.csproj
index 1596d86ec8..a83ee06ef3 100644
--- a/TUnit.Assertions/TUnit.Assertions.csproj
+++ b/TUnit.Assertions/TUnit.Assertions.csproj
@@ -27,7 +27,6 @@
-
true
$(BuildPath)
@@ -37,17 +36,6 @@
true
$(BuildPath)
-
-
-
- true
- $(BuildTransitivePath)TUnit.Assertions.props
-
-
-
- true
- $(BuildTransitivePath)TUnit.Assertions.targets
-
diff --git a/TUnit.Core/TUnit.Core.buildTransitive.props b/TUnit.Core/TUnit.Core.buildTransitive.props
deleted file mode 100644
index 4dce10fa07..0000000000
--- a/TUnit.Core/TUnit.Core.buildTransitive.props
+++ /dev/null
@@ -1,25 +0,0 @@
-
-
-
-
-
-
- true
-
- true
-
-
-
-
-
- <_PolyfillPackageReference Include="@(PackageReference)"
- Condition="'%(PackageReference.Identity)' == 'Polyfill'" />
- <_PolyfillPackageVersion Include="@(PackageVersion)"
- Condition="'%(PackageVersion.Identity)' == 'Polyfill'" />
-
-
- <_PolyfillAlreadyDefined Condition="'@(_PolyfillPackageReference)' != '' Or '@(_PolyfillPackageVersion)' != ''">true
-
-
-
-
diff --git a/TUnit.Core/TUnit.Core.buildTransitive.targets b/TUnit.Core/TUnit.Core.buildTransitive.targets
deleted file mode 100644
index 4df8f09e59..0000000000
--- a/TUnit.Core/TUnit.Core.buildTransitive.targets
+++ /dev/null
@@ -1,38 +0,0 @@
-
-
-
-
-
-
- <_TUnitPolyfillVersion>9.23.0
- <_TUnitNeedsPolyfill Condition="'$(TargetFramework)' == 'netstandard2.0' or '$(TargetFramework)' == 'netstandard2.1' or '$(TargetFrameworkIdentifier)' == '.NETFramework'">true
-
-
-
-
-
- all
- compile; analyzers
- runtime; native; contentfiles; build; buildtransitive
-
-
-
-
-
-
- all
- compile; analyzers
- runtime; native; contentfiles; build; buildtransitive
-
-
-
-
-
-
-
-
-
diff --git a/TUnit.Core/TUnit.Core.csproj b/TUnit.Core/TUnit.Core.csproj
index 0da0cef5d1..18d3908be8 100644
--- a/TUnit.Core/TUnit.Core.csproj
+++ b/TUnit.Core/TUnit.Core.csproj
@@ -11,7 +11,6 @@
-
true
$(BuildPath)
@@ -21,17 +20,6 @@
true
$(BuildPath)
-
-
-
- true
- $(BuildTransitivePath)TUnit.Core.props
-
-
-
- true
- $(BuildTransitivePath)TUnit.Core.targets
-
-
-
-
diff --git a/TUnit.Engine/TUnit.Engine.csproj b/TUnit.Engine/TUnit.Engine.csproj
index e7f4b044b9..ed761a8b63 100644
--- a/TUnit.Engine/TUnit.Engine.csproj
+++ b/TUnit.Engine/TUnit.Engine.csproj
@@ -21,17 +21,10 @@
-
true
$(BuildPath)
-
-
-
- true
- $(BuildTransitivePath)TUnit.Engine.props
-
From 13bee1568e6324bc7254028545a1ab3fd67ecb8d Mon Sep 17 00:00:00 2001
From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com>
Date: Sun, 29 Mar 2026 11:59:08 +0100
Subject: [PATCH 5/7] fix: restore buildTransitive for TUnit.Engine to preserve
test framework registration
TUnit.Engine.props contains the TestingPlatformBuilderHook registration
which is essential for TUnit to function. Without transitive props, projects
that reference the TUnit meta-package (which depends on TUnit.Engine
transitively) fail with "The test framework adapter has not been registered".
Keep buildTransitive/ removed for TUnit.Core and TUnit.Assertions since
their props only set convenience features (implicit usings, source gen,
polyfills) that should not leak to transitive consumers.
---
TUnit.Engine/TUnit.Engine.csproj | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/TUnit.Engine/TUnit.Engine.csproj b/TUnit.Engine/TUnit.Engine.csproj
index ed761a8b63..33b43df182 100644
--- a/TUnit.Engine/TUnit.Engine.csproj
+++ b/TUnit.Engine/TUnit.Engine.csproj
@@ -21,6 +21,11 @@
+
+ true
+ $(BuildTransitivePath)
+
+
true
$(BuildPath)
From d52deb5f0eb748aa26ba92e1124ee74c3ef71b2e Mon Sep 17 00:00:00 2001
From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com>
Date: Sun, 29 Mar 2026 12:00:54 +0100
Subject: [PATCH 6/7] fix: restore buildTransitive for TUnit.Core and
TUnit.Assertions
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Implicit usings should flow transitively — users can opt out by setting
TUnitImplicitUsings=false. The condition guard in TUnit.Core.props is
the correct fix, not removing buildTransitive.
---
TUnit.Assertions/TUnit.Assertions.csproj | 10 ++++++++++
TUnit.Core/TUnit.Core.csproj | 10 ++++++++++
2 files changed, 20 insertions(+)
diff --git a/TUnit.Assertions/TUnit.Assertions.csproj b/TUnit.Assertions/TUnit.Assertions.csproj
index a83ee06ef3..1809c6042f 100644
--- a/TUnit.Assertions/TUnit.Assertions.csproj
+++ b/TUnit.Assertions/TUnit.Assertions.csproj
@@ -27,11 +27,21 @@
+
+ true
+ $(BuildTransitivePath)
+
+
true
$(BuildPath)
+
+ true
+ $(BuildTransitivePath)
+
+
true
$(BuildPath)
diff --git a/TUnit.Core/TUnit.Core.csproj b/TUnit.Core/TUnit.Core.csproj
index 18d3908be8..c0c1082638 100644
--- a/TUnit.Core/TUnit.Core.csproj
+++ b/TUnit.Core/TUnit.Core.csproj
@@ -11,11 +11,21 @@
+
+ true
+ $(BuildTransitivePath)
+
+
true
$(BuildPath)
+
+ true
+ $(BuildTransitivePath)
+
+
true
$(BuildPath)
From 5e2b23056845617b1830dd63ff6e39621095e1ce Mon Sep 17 00:00:00 2001
From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com>
Date: Sun, 29 Mar 2026 12:03:57 +0100
Subject: [PATCH 7/7] fix: remove dead xunit detection from props files
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The XunitPackageReferenceFound Target runs during execution phase
(BeforeTargets="PrepareForBuild") but the TUnitImplicitUsings
PropertyGroup is evaluated during the static evaluation phase — before
any targets run. So XunitPackageReferenceFound was always 'false' and
the xunit check never actually prevented implicit usings.
Simplify to just the '$(TUnitImplicitUsings)' == '' guard, which
correctly respects user overrides from Directory.Build.props.
---
TUnit.Assertions/TUnit.Assertions.props | 17 +----------------
TUnit.Core/TUnit.Core.props | 17 +----------------
2 files changed, 2 insertions(+), 32 deletions(-)
diff --git a/TUnit.Assertions/TUnit.Assertions.props b/TUnit.Assertions/TUnit.Assertions.props
index 5b82565449..6525bb1f7b 100644
--- a/TUnit.Assertions/TUnit.Assertions.props
+++ b/TUnit.Assertions/TUnit.Assertions.props
@@ -1,22 +1,7 @@
-
- false
-
-
-
-
-
- <_XunitReference Include="@(PackageReference)"
- Condition="'%(PackageReference.Identity)' == 'xunit' Or '%(PackageReference.Identity)' == 'xunit.v3'" />
-
-
- true
-
-
-
-
+
true
diff --git a/TUnit.Core/TUnit.Core.props b/TUnit.Core/TUnit.Core.props
index 9721721a6a..d4248315f1 100644
--- a/TUnit.Core/TUnit.Core.props
+++ b/TUnit.Core/TUnit.Core.props
@@ -1,22 +1,7 @@
-
- false
-
-
-
-
-
- <_XunitReference Include="@(PackageReference)"
- Condition="'%(PackageReference.Identity)' == 'xunit' Or '%(PackageReference.Identity)' == 'xunit.v3'" />
-
-
- true
-
-
-
-
+
true