From a10c97f3eb346d0613d977c51037f7ea778d2ee7 Mon Sep 17 00:00:00 2001
From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com>
Date: Mon, 28 Sep 2026 16:01:59 +0100
Subject: [PATCH 1/3] perf: emit source-generated test types after user code to
avoid slow Defender scans
The C# compiler writes namespaces in reverse order of first declaration, and
generator output is always compiled last, so TUnit.Generated (large static
constructors registering every test) landed at the start of the assembly.
Microsoft Defender analyses early methods far more expensively: a 10k-test
assembly took ~5s to scan versus ~0.2s for MSTest/xUnit of similar size.
Every build (CreateAppHost opens the dll) and the first test run paid it.
TUnit.Core.props now adds a file declaring the empty TUnit.Generated
namespace ahead of the SDK default Compile glob, which moves the generated
types to the end of the assembly.
---
src/TUnit.Core/TUnit.Core.GeneratedNamespace.cs | 13 +++++++++++++
src/TUnit.Core/TUnit.Core.csproj | 12 ++++++++++++
src/TUnit.Core/TUnit.Core.props | 9 +++++++++
3 files changed, 34 insertions(+)
create mode 100644 src/TUnit.Core/TUnit.Core.GeneratedNamespace.cs
diff --git a/src/TUnit.Core/TUnit.Core.GeneratedNamespace.cs b/src/TUnit.Core/TUnit.Core.GeneratedNamespace.cs
new file mode 100644
index 00000000000..2ac19f46719
--- /dev/null
+++ b/src/TUnit.Core/TUnit.Core.GeneratedNamespace.cs
@@ -0,0 +1,13 @@
+//
+// Added to test projects by TUnit.Core.props so it compiles ahead of the project's own files.
+//
+// The C# compiler writes namespaces to the assembly in reverse order of their first declaration.
+// Source-generated trees are always added after the project's files, so without this declaration
+// the TUnit.Generated types (large static constructors registering every test) are written at the
+// very start of the assembly. Antivirus scanners such as Microsoft Defender analyse code at the
+// start of an assembly far more expensively, which added seconds to every build and first test run
+// of large test projects on Windows. Declaring the namespace first moves the generated types to the
+// end of the assembly instead.
+namespace TUnit.Generated
+{
+}
diff --git a/src/TUnit.Core/TUnit.Core.csproj b/src/TUnit.Core/TUnit.Core.csproj
index 0ffb72e7670..9f759056ad6 100644
--- a/src/TUnit.Core/TUnit.Core.csproj
+++ b/src/TUnit.Core/TUnit.Core.csproj
@@ -35,6 +35,18 @@
true
$(BuildPath)
+
+
+
+
+ true
+ $(BuildTransitivePath)
+
+
+
+ true
+ $(BuildPath)
+
+
+
+
+
+
From 562eb4dd1a1a2beaef255105481f7196af158fff Mon Sep 17 00:00:00 2001
From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com>
Date: Mon, 28 Sep 2026 17:17:44 +0100
Subject: [PATCH 2/3] Add packaged-consumer check that generated types are
emitted after user types
Adds GeneratedTypeOrderTests to TUnit.NugetTester, which the pipeline runs against the packed
packages, so the real NuGet props import path is covered. Also documents in TUnit.Core.props why
the Compile item's conditions work from a .props: MSBuild evaluates all properties before items.
---
src/TUnit.Core/TUnit.Core.props | 5 +++
.../GeneratedTypeOrderTests.cs | 32 +++++++++++++++++++
2 files changed, 37 insertions(+)
create mode 100644 tools/tunit-nuget-tester/TUnit.NugetTester/TUnit.NugetTester/GeneratedTypeOrderTests.cs
diff --git a/src/TUnit.Core/TUnit.Core.props b/src/TUnit.Core/TUnit.Core.props
index b23be4dceb7..bdb67a4204f 100644
--- a/src/TUnit.Core/TUnit.Core.props
+++ b/src/TUnit.Core/TUnit.Core.props
@@ -37,6 +37,11 @@
Declares the TUnit.Generated namespace before the project's own files (items added here precede the SDK's
default Compile glob) so the compiler writes the source-generated types at the end of the assembly rather
than the start, where Microsoft Defender scans them far more slowly. See TUnit.Core.GeneratedNamespace.cs.
+
+ This must stay in the .props: items added from a .targets land after the default glob. The condition is safe
+ here even though Language is set later by the C# targets and EnableTUnitSourceGeneration may be set in the
+ project body, because MSBuild evaluates every property before any item, so item conditions see final values.
+ Guarded by TUnit.NugetTester's GeneratedTypeOrderTests.
-->
diff --git a/tools/tunit-nuget-tester/TUnit.NugetTester/TUnit.NugetTester/GeneratedTypeOrderTests.cs b/tools/tunit-nuget-tester/TUnit.NugetTester/TUnit.NugetTester/GeneratedTypeOrderTests.cs
new file mode 100644
index 00000000000..b4beefe47f5
--- /dev/null
+++ b/tools/tunit-nuget-tester/TUnit.NugetTester/TUnit.NugetTester/GeneratedTypeOrderTests.cs
@@ -0,0 +1,32 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Reflection;
+
+namespace TUnit.NugetTester;
+
+///
+/// TUnit.Core.props adds TUnit.Core.GeneratedNamespace.cs ahead of the project's own files so the compiler
+/// emits the source-generated TUnit.Generated types after the user's types. Emitting them first made
+/// Microsoft Defender scan large test assemblies for seconds on every build (see PR #6908).
+/// This project consumes the packed packages, so it guards the real NuGet import path.
+///
+public class GeneratedTypeOrderTests
+{
+ [Test]
+ [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Only inspects this assembly's own type definitions.")]
+ public async Task Generated_Types_Are_Emitted_After_User_Types()
+ {
+ var types = typeof(GeneratedTypeOrderTests).Assembly.GetTypes();
+
+ var generatedTypes = types.Where(t => t.Namespace == "TUnit.Generated").ToArray();
+ var userTypes = types.Where(t => !t.IsNested && t.Namespace == typeof(GeneratedTypeOrderTests).Namespace).ToArray();
+
+ await Assert.That(generatedTypes).IsNotEmpty();
+ await Assert.That(userTypes).IsNotEmpty();
+
+ // TypeDef metadata tokens follow the order the compiler emitted the types in.
+ var firstGenerated = generatedTypes.Min(t => t.MetadataToken);
+ var lastUser = userTypes.Max(t => t.MetadataToken);
+
+ await Assert.That(firstGenerated).IsGreaterThan(lastUser);
+ }
+}
From 2bd49faa47905d49bdf7016e24ea9ca14f248de0 Mon Sep 17 00:00:00 2001
From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com>
Date: Mon, 28 Sep 2026 17:46:38 +0100
Subject: [PATCH 3/3] Put the TUnit.Generated namespace file first for in-repo
test projects
Repo projects import TUnit.Core.props from their project body, after the SDK's default Compile
glob, so the namespace file landed last and the generated types were still emitted first. A
target in eng/TestLibrary.props now moves it to the front of the Compile items just before
CoreCompile. Evaluation is unchanged, and the shipped package is unaffected.
---
eng/TestLibrary.props | 17 +++++++++++++++++
1 file changed, 17 insertions(+)
diff --git a/eng/TestLibrary.props b/eng/TestLibrary.props
index d939173fa4d..7acc01fdf0a 100644
--- a/eng/TestLibrary.props
+++ b/eng/TestLibrary.props
@@ -1,6 +1,23 @@
+
+
+
+ <_TUnitGeneratedNamespaceCompile Include="@(Compile)" Condition="'%(Filename)%(Extension)' == 'TUnit.Core.GeneratedNamespace.cs'" />
+ <_TUnitOtherCompile Include="@(Compile)" Exclude="@(_TUnitGeneratedNamespaceCompile)" Condition="'@(_TUnitGeneratedNamespaceCompile)' != ''" />
+
+
+
+
+
Library