diff --git a/TUnit.Core/TestContext.cs b/TUnit.Core/TestContext.cs
index f3a2a87ac7..d28177f42e 100644
--- a/TUnit.Core/TestContext.cs
+++ b/TUnit.Core/TestContext.cs
@@ -181,6 +181,47 @@ internal override void SetAsyncLocalContext()
internal object[]? CachedEligibleEventObjects { get; set; }
+ // Pre-computed typed event receivers (filtered, sorted, scoped-attribute filtered)
+ // These are computed lazily on first access and cached
+#if NET
+ // Stage-specific caches for .NET 8+ (avoids runtime filtering by stage)
+ internal ITestStartEventReceiver[]? CachedTestStartReceiversEarly { get; set; }
+ internal ITestStartEventReceiver[]? CachedTestStartReceiversLate { get; set; }
+ internal ITestEndEventReceiver[]? CachedTestEndReceiversEarly { get; set; }
+ internal ITestEndEventReceiver[]? CachedTestEndReceiversLate { get; set; }
+#else
+ // Single cache for older frameworks (no stage concept)
+ internal ITestStartEventReceiver[]? CachedTestStartReceivers { get; set; }
+ internal ITestEndEventReceiver[]? CachedTestEndReceivers { get; set; }
+#endif
+ internal ITestSkippedEventReceiver[]? CachedTestSkippedReceivers { get; set; }
+ internal ITestDiscoveryEventReceiver[]? CachedTestDiscoveryReceivers { get; set; }
+ internal ITestRegisteredEventReceiver[]? CachedTestRegisteredReceivers { get; set; }
+
+ // Track the class instance used when building caches for invalidation on retry
+ internal object? CachedClassInstance { get; set; }
+
+ ///
+ /// Invalidates all cached event receiver data. Called when class instance changes (e.g., on retry).
+ ///
+ internal void InvalidateEventReceiverCaches()
+ {
+ CachedEligibleEventObjects = null;
+#if NET
+ CachedTestStartReceiversEarly = null;
+ CachedTestStartReceiversLate = null;
+ CachedTestEndReceiversEarly = null;
+ CachedTestEndReceiversLate = null;
+#else
+ CachedTestStartReceivers = null;
+ CachedTestEndReceivers = null;
+#endif
+ CachedTestSkippedReceivers = null;
+ CachedTestDiscoveryReceivers = null;
+ CachedTestRegisteredReceivers = null;
+ CachedClassInstance = null;
+ }
+
internal ConcurrentDictionary ObjectBag => _testBuilderContext.StateBag;
diff --git a/TUnit.Engine/Building/TestBuilder.cs b/TUnit.Engine/Building/TestBuilder.cs
index 8921b35590..ffb901e5dc 100644
--- a/TUnit.Engine/Building/TestBuilder.cs
+++ b/TUnit.Engine/Building/TestBuilder.cs
@@ -1029,9 +1029,8 @@ private async Task InvokeTestRegisteredEventReceiversAsync(TestContext context)
// First, invoke the global test argument registration service to register shared instances
await _testArgumentRegistrationService.RegisterTestArgumentsAsync(context);
- var eventObjects = context.GetEligibleEventObjects();
-
- foreach (var receiver in eventObjects.OfType())
+ // Use pre-computed receivers (already filtered, sorted, and scoped-attribute filtered)
+ foreach (var receiver in context.GetTestRegisteredReceivers())
{
await receiver.OnTestRegistered(registeredContext);
}
diff --git a/TUnit.Engine/Extensions/TestContextExtensions.cs b/TUnit.Engine/Extensions/TestContextExtensions.cs
index 3b64e65d7c..27c00913fa 100644
--- a/TUnit.Engine/Extensions/TestContextExtensions.cs
+++ b/TUnit.Engine/Extensions/TestContextExtensions.cs
@@ -1,21 +1,165 @@
using TUnit.Core;
+using TUnit.Core.Enums;
+using TUnit.Core.Interfaces;
+using TUnit.Engine.Utilities;
namespace TUnit.Engine.Extensions;
internal static class TestContextExtensions
{
- public static IEnumerable