diff --git a/docs/docs/writing-tests/event-subscribing.md b/docs/docs/writing-tests/event-subscribing.md
index d622884acbb..45866a2c594 100644
--- a/docs/docs/writing-tests/event-subscribing.md
+++ b/docs/docs/writing-tests/event-subscribing.md
@@ -21,6 +21,12 @@ The interfaces they can implement are:
This can be useful especially when generating data that you need to track and maybe dispose later. By hooking into these events, we can do things like track and dispose our objects when we need.
+## Registration callbacks for injected properties
+
+TUnit creates injected property values after the first `OnTestRegistered` pass, so that a receiver such as a `SkipAttribute` can skip the test before expensive data sources are created. An injected property value that implements `ITestRegisteredEventReceiver` receives its callback in a second pass, after the property values are resolved and before the test runs. The second pass uses the same `Order` sorting, but it always runs after every receiver from the first pass.
+
+A property value can still call `SetParallelLimiter`, `SetSkipped` and the other `TestRegisteredContext` methods. If it skips the test, its shared data source objects have already been created.
+
## Registration callbacks for executors
An executor that implements `ITestRegisteredEventReceiver` receives `OnTestRegistered` when a registration receiver installs it through `SetTestExecutor` or `SetHookExecutor`, unless it has already received the callback for that test. Its callback runs after the installing receiver returns. Dynamically installed executors are not globally sorted by their own `Order`.
diff --git a/src/TUnit.Core/Interfaces/ITestRegisteredEventReceiver.cs b/src/TUnit.Core/Interfaces/ITestRegisteredEventReceiver.cs
index b1af3c5fb6a..f7470b50a0b 100644
--- a/src/TUnit.Core/Interfaces/ITestRegisteredEventReceiver.cs
+++ b/src/TUnit.Core/Interfaces/ITestRegisteredEventReceiver.cs
@@ -28,6 +28,12 @@ namespace TUnit.Core.Interfaces;
/// Each receiver instance is invoked at most once per test, even when it is both an eligible
/// event object and an installed executor. Distinct instances are compared by reference identity.
///
+///
+/// Injected property values are resolved after the first registration pass, so that receivers
+/// such as can skip a test before expensive data sources are created.
+/// An injected property value that implements this interface is invoked in a second pass, after
+/// every receiver from the first pass and before the test executes.
+///
///
public interface ITestRegisteredEventReceiver : IEventReceiver
{
diff --git a/src/TUnit.Engine/Services/TestFilterService.cs b/src/TUnit.Engine/Services/TestFilterService.cs
index 1ba1d8630a4..4bdab17af62 100644
--- a/src/TUnit.Engine/Services/TestFilterService.cs
+++ b/src/TUnit.Engine/Services/TestFilterService.cs
@@ -81,9 +81,42 @@ private async Task InvokeTestRegisteredReceiver(ITestRegisteredEventReceiver rec
}
}
+ private async Task InvokeTestRegisteredReceivers(ITestRegisteredEventReceiver[] receivers, TestRegisteredContext context,
+ HashSet invokedReceivers)
+ {
+ foreach (var receiver in receivers)
+ {
+ await InvokeTestRegisteredReceiver(receiver, context, invokedReceivers).ConfigureAwait(false);
+
+ // Dynamically installed executor callbacks run after their installer, regardless of their
+ // own Order. The shared identity set also covers executors in the original receiver list.
+ while (context.TryDequeueExecutorEventReceiver(out var executorReceiver))
+ {
+ await InvokeTestRegisteredReceiver(executorReceiver, context, invokedReceivers).ConfigureAwait(false);
+ }
+ }
+ }
+
+ private static TestRegisteredContext CreateTestRegisteredContext(TestContext testContext)
+ {
+ var discoveredTest = new DiscoveredTest