@@ -9,6 +9,7 @@ namespace TUnit.Engine.Services;
99
1010internal sealed class HookCollectionService : IHookCollectionService
1111{
12+ private readonly EventReceiverOrchestrator _eventReceiverOrchestrator ;
1213 private readonly ConcurrentDictionary < Type , IReadOnlyList < Func < TestContext , CancellationToken , Task > > > _beforeTestHooksCache = new ( ) ;
1314 private readonly ConcurrentDictionary < Type , IReadOnlyList < Func < TestContext , CancellationToken , Task > > > _afterTestHooksCache = new ( ) ;
1415 private readonly ConcurrentDictionary < Type , IReadOnlyList < Func < TestContext , CancellationToken , Task > > > _beforeEveryTestHooksCache = new ( ) ;
@@ -19,6 +20,48 @@ internal sealed class HookCollectionService : IHookCollectionService
1920 // Cache for complete hook chains to avoid repeated lookups
2021 private readonly ConcurrentDictionary < Type , CompleteHookChain > _completeHookChainCache = new ( ) ;
2122
23+ // Cache for processed hooks to avoid re-processing event receivers
24+ private readonly ConcurrentDictionary < object , bool > _processedHooks = new ( ) ;
25+
26+ public HookCollectionService ( EventReceiverOrchestrator eventReceiverOrchestrator )
27+ {
28+ _eventReceiverOrchestrator = eventReceiverOrchestrator ;
29+ }
30+
31+ private async Task ProcessHookRegistrationAsync ( object hookMethod , CancellationToken cancellationToken = default )
32+ {
33+ // Only process each hook once
34+ if ( ! _processedHooks . TryAdd ( hookMethod , true ) )
35+ {
36+ return ;
37+ }
38+
39+ try
40+ {
41+ HookRegisteredContext context ;
42+
43+ if ( hookMethod is StaticHookMethod staticHook )
44+ {
45+ context = new HookRegisteredContext ( staticHook ) ;
46+ }
47+ else if ( hookMethod is InstanceHookMethod instanceHook )
48+ {
49+ context = new HookRegisteredContext ( instanceHook ) ;
50+ }
51+ else
52+ {
53+ return ; // Unknown hook type
54+ }
55+
56+ await _eventReceiverOrchestrator . InvokeHookRegistrationEventReceiversAsync ( context , cancellationToken ) ;
57+ }
58+ catch ( Exception )
59+ {
60+ // Ignore errors during hook registration event processing to avoid breaking hook execution
61+ // The EventReceiverOrchestrator already logs errors internally
62+ }
63+ }
64+
2265 private sealed class CompleteHookChain
2366 {
2467 public IReadOnlyList < Func < TestContext , CancellationToken , Task > > BeforeTestHooks { get ; init ; } = [
@@ -51,7 +94,7 @@ public ValueTask<IReadOnlyList<Func<TestContext, CancellationToken, Task>>> Coll
5194 {
5295 foreach ( var hook in sourceHooks )
5396 {
54- var hookFunc = CreateInstanceHookDelegate ( hook ) ;
97+ var hookFunc = await CreateInstanceHookDelegateAsync ( hook ) ;
5598 typeHooks . Add ( ( hook . Order , hook . RegistrationIndex , hookFunc ) ) ;
5699 }
57100 }
@@ -64,7 +107,7 @@ public ValueTask<IReadOnlyList<Func<TestContext, CancellationToken, Task>>> Coll
64107 {
65108 foreach ( var hook in openTypeHooks )
66109 {
67- var hookFunc = CreateInstanceHookDelegate ( hook ) ;
110+ var hookFunc = await CreateInstanceHookDelegateAsync ( hook ) ;
68111 typeHooks . Add ( ( hook . Order , hook . RegistrationIndex , hookFunc ) ) ;
69112 }
70113 }
@@ -111,7 +154,7 @@ public ValueTask<IReadOnlyList<Func<TestContext, CancellationToken, Task>>> Coll
111154 {
112155 foreach ( var hook in sourceHooks )
113156 {
114- var hookFunc = CreateInstanceHookDelegate ( hook ) ;
157+ var hookFunc = await CreateInstanceHookDelegateAsync ( hook ) ;
115158 typeHooks . Add ( ( hook . Order , hook . RegistrationIndex , hookFunc ) ) ;
116159 }
117160 }
@@ -124,7 +167,7 @@ public ValueTask<IReadOnlyList<Func<TestContext, CancellationToken, Task>>> Coll
124167 {
125168 foreach ( var hook in openTypeHooks )
126169 {
127- var hookFunc = CreateInstanceHookDelegate ( hook ) ;
170+ var hookFunc = await CreateInstanceHookDelegateAsync ( hook ) ;
128171 typeHooks . Add ( ( hook . Order , hook . RegistrationIndex , hookFunc ) ) ;
129172 }
130173 }
@@ -163,7 +206,7 @@ public ValueTask<IReadOnlyList<Func<TestContext, CancellationToken, Task>>> Coll
163206 // Collect all global BeforeEvery hooks
164207 foreach ( var hook in Sources . BeforeEveryTestHooks )
165208 {
166- var hookFunc = CreateStaticHookDelegate ( hook ) ;
209+ var hookFunc = await CreateStaticHookDelegateAsync ( hook ) ;
167210 allHooks . Add ( ( hook . Order , hook . RegistrationIndex , hookFunc ) ) ;
168211 }
169212
@@ -186,7 +229,7 @@ public ValueTask<IReadOnlyList<Func<TestContext, CancellationToken, Task>>> Coll
186229 // Collect all global AfterEvery hooks
187230 foreach ( var hook in Sources . AfterEveryTestHooks )
188231 {
189- var hookFunc = CreateStaticHookDelegate ( hook ) ;
232+ var hookFunc = await CreateStaticHookDelegateAsync ( hook ) ;
190233 allHooks . Add ( ( hook . Order , hook . RegistrationIndex , hookFunc ) ) ;
191234 }
192235
@@ -513,8 +556,11 @@ public ValueTask<IReadOnlyList<Func<AssemblyHookContext, CancellationToken, Task
513556 return new ValueTask < IReadOnlyList < Func < AssemblyHookContext , CancellationToken , Task > > > ( hooks ) ;
514557 }
515558
516- private static Func < TestContext , CancellationToken , Task > CreateInstanceHookDelegate ( InstanceHookMethod hook )
559+ private async Task < Func < TestContext , CancellationToken , Task > > CreateInstanceHookDelegateAsync ( InstanceHookMethod hook )
517560 {
561+ // Process hook registration event receivers
562+ await ProcessHookRegistrationAsync ( hook ) ;
563+
518564 return async ( context , cancellationToken ) =>
519565 {
520566 var timeoutAction = HookTimeoutHelper . CreateTimeoutHookAction (
@@ -528,8 +574,11 @@ private static Func<TestContext, CancellationToken, Task> CreateInstanceHookDele
528574 } ;
529575 }
530576
531- private static Func < TestContext , CancellationToken , Task > CreateStaticHookDelegate ( StaticHookMethod < TestContext > hook )
577+ private async Task < Func < TestContext , CancellationToken , Task > > CreateStaticHookDelegateAsync ( StaticHookMethod < TestContext > hook )
532578 {
579+ // Process hook registration event receivers
580+ await ProcessHookRegistrationAsync ( hook ) ;
581+
533582 return async ( context , cancellationToken ) =>
534583 {
535584 var timeoutAction = HookTimeoutHelper . CreateTimeoutHookAction (
0 commit comments