@@ -65,6 +65,11 @@ public sealed class AtlasSearchFixture : IDisposable
6565 private bool _returnScopeInitialized ;
6666 private bool ? _isRerankSupported ;
6767
68+ // Routes events from the shared cluster to whichever subscribers test classes
69+ // have currently attached. Replaces the previously-shared single EventCapturer
70+ // so per-test-class capturers can coexist without stepping on each other.
71+ private readonly CompositeEventSubscriber _eventRouter = new ( ) ;
72+
6873 public AtlasSearchFixture ( )
6974 {
7075 // The fixture must be tolerant of missing env vars so xUnit can skip individual
@@ -78,19 +83,27 @@ public AtlasSearchFixture()
7883 var settings = MongoClientSettings . FromConnectionString ( atlasSearchUri ) ;
7984 settings . ClusterSource = DisposingClusterSource . Instance ;
8085
81- EventCapturer = new EventCapturer ( ) . Capture < CommandStartedEvent > ( e => e . CommandName == "aggregate" ) ;
82- settings . ClusterConfigurator = b => b . Subscribe ( EventCapturer ) ;
86+ settings . ClusterConfigurator = b => b . Subscribe ( _eventRouter ) ;
8387
8488 Client = new MongoClient ( settings ) ;
8589 DatabaseName = "atlas_search_" + Guid . NewGuid ( ) . ToString ( "N" ) ;
8690 }
8791
8892 public IMongoClient Client { get ; }
8993
90- public EventCapturer EventCapturer { get ; }
91-
9294 public string DatabaseName { get ; }
9395
96+ /// <summary>
97+ /// Attaches a subscriber to the fixture-owned cluster. Each test class should
98+ /// register its own <see cref="EventCapturer"/> here (rather than sharing one)
99+ /// and detach it during teardown.
100+ /// </summary>
101+ public void AddEventSubscriber ( IEventSubscriber subscriber ) =>
102+ _eventRouter . Add ( subscriber ) ;
103+
104+ public void RemoveEventSubscriber ( IEventSubscriber subscriber ) =>
105+ _eventRouter . Remove ( subscriber ) ;
106+
94107 /// <summary>
95108 /// Lazily probes for $rerank support on first access. Tests guard themselves on this so the
96109 /// suite continues to work on Atlas deployments that don't yet ship the rerank model.
@@ -100,20 +113,22 @@ public bool IsRerankSupported
100113 get
101114 {
102115 EnsureMoviesInitialized ( ) ;
103- if ( _isRerankSupported is bool cached )
116+ if ( _isRerankSupported is { } cached )
104117 {
105118 return cached ;
106119 }
107120 lock ( _initLock )
108121 {
109- if ( _isRerankSupported is bool again )
122+ if ( _isRerankSupported is { } again )
110123 {
111124 return again ;
112125 }
113126 _isRerankSupported = ProbeRerankSupport ( ) ;
114127 // ProbeRerankSupport fires a real $search+$rerank aggregate; clear so
115128 // tests asserting on captured aggregate events don't see the probe.
116- EventCapturer ? . Clear ( ) ;
129+ // Run unconditionally — tests may have already attached capturers via
130+ // AddEventSubscriber before the first IsRerankSupported probe fires.
131+ ClearCapturedEvents ( ) ;
117132 return _isRerankSupported . Value ;
118133 }
119134 }
@@ -190,8 +205,6 @@ public void Dispose()
190205 Console . Error . WriteLine (
191206 $ "AtlasSearchFixture: failed to drop database '{ DatabaseName } ': { ex } ") ;
192207 }
193-
194- Client . Dispose ( ) ;
195208 }
196209
197210 // ---- Seeders ----
@@ -249,7 +262,7 @@ private void EnsureHistoricalInitialized()
249262 } ) ;
250263
251264 _historicalInitialized = true ;
252- EventCapturer ? . Clear ( ) ;
265+ ClearCapturedEvents ( ) ;
253266 }
254267 }
255268
@@ -315,7 +328,7 @@ private void EnsureMoviesInitialized()
315328 } ) ;
316329
317330 _moviesInitialized = true ;
318- EventCapturer ? . Clear ( ) ;
331+ ClearCapturedEvents ( ) ;
319332 }
320333 }
321334
@@ -371,7 +384,7 @@ private void EnsureEmbeddedMoviesInitialized()
371384 } ) ;
372385
373386 _embeddedMoviesInitialized = true ;
374- EventCapturer ? . Clear ( ) ;
387+ ClearCapturedEvents ( ) ;
375388 }
376389 }
377390
@@ -415,7 +428,7 @@ private void EnsureAirbnbInitialized()
415428 } ) ;
416429
417430 _airbnbInitialized = true ;
418- EventCapturer ? . Clear ( ) ;
431+ ClearCapturedEvents ( ) ;
419432 }
420433 }
421434
@@ -445,7 +458,7 @@ private void EnsureTestClassesInitialized()
445458 } ) ;
446459
447460 _testClassesInitialized = true ;
448- EventCapturer ? . Clear ( ) ;
461+ ClearCapturedEvents ( ) ;
449462 }
450463 }
451464
@@ -476,7 +489,7 @@ private void EnsureBinaryVectorInitialized()
476489 } ) ;
477490
478491 _binaryVectorInitialized = true ;
479- EventCapturer ? . Clear ( ) ;
492+ ClearCapturedEvents ( ) ;
480493 }
481494 }
482495
@@ -526,7 +539,7 @@ private void EnsureAutoEmbedInitialized()
526539 WaitForAutoEmbedIndexReady ( collection , AutoEmbedIndexName ) ;
527540
528541 _autoEmbedInitialized = true ;
529- EventCapturer ? . Clear ( ) ;
542+ ClearCapturedEvents ( ) ;
530543 }
531544 }
532545
@@ -715,7 +728,7 @@ private void EnsureReturnScopeInitialized()
715728 } ) ;
716729
717730 _returnScopeInitialized = true ;
718- EventCapturer ? . Clear ( ) ;
731+ ClearCapturedEvents ( ) ;
719732 }
720733 }
721734
@@ -861,5 +874,59 @@ private bool ProbeRerankSupport()
861874 return false ;
862875 }
863876 }
877+
878+ // Drops any events buffered by currently-attached EventCapturers. Called by seed
879+ // paths whose driver-side commands would otherwise pollute a test's capture.
880+ private void ClearCapturedEvents ( ) => _eventRouter . ClearCapturers ( ) ;
881+
882+ /// <summary>
883+ /// Forwards events from the cluster to whichever <see cref="IEventSubscriber"/>s
884+ /// test classes have currently attached. The cluster's <see cref="EventPublisher"/>
885+ /// caches handlers on first publish, so this subscriber always reports a handler
886+ /// for every event type — that cached delegate then consults the live subscriber
887+ /// list at each publish, letting tests add/remove their own capturers at will.
888+ /// </summary>
889+ private sealed class CompositeEventSubscriber : IEventSubscriber
890+ {
891+ private readonly object _gate = new ( ) ;
892+ private readonly List < IEventSubscriber > _subscribers = new ( ) ;
893+
894+ public void Add ( IEventSubscriber subscriber )
895+ {
896+ lock ( _gate ) _subscribers . Add ( subscriber ) ;
897+ }
898+
899+ public void Remove ( IEventSubscriber subscriber )
900+ {
901+ lock ( _gate ) _subscribers . Remove ( subscriber ) ;
902+ }
903+
904+ public void ClearCapturers ( )
905+ {
906+ IEventSubscriber [ ] snapshot ;
907+ lock ( _gate ) snapshot = _subscribers . ToArray ( ) ;
908+ foreach ( var s in snapshot )
909+ {
910+ ( s as EventCapturer ) ? . Clear ( ) ;
911+ }
912+ }
913+
914+ public bool TryGetEventHandler < TEvent > ( out Action < TEvent > handler )
915+ {
916+ handler = e =>
917+ {
918+ IEventSubscriber [ ] snapshot ;
919+ lock ( _gate ) snapshot = _subscribers . ToArray ( ) ;
920+ foreach ( var s in snapshot )
921+ {
922+ if ( s . TryGetEventHandler < TEvent > ( out var inner ) )
923+ {
924+ inner ( e ) ;
925+ }
926+ }
927+ } ;
928+ return true ;
929+ }
930+ }
864931 }
865932}
0 commit comments