Skip to content

[dotnet] [bidi] Additional Event streaming (breaking change)#17349

Merged
nvborisenko merged 73 commits into
SeleniumHQ:trunkfrom
nvborisenko:bidi-event-stream
May 11, 2026
Merged

[dotnet] [bidi] Additional Event streaming (breaking change)#17349
nvborisenko merged 73 commits into
SeleniumHQ:trunkfrom
nvborisenko:bidi-event-stream

Conversation

@nvborisenko

@nvborisenko nvborisenko commented Apr 15, 2026

Copy link
Copy Markdown
Member

Finally the events model is more cleaner. Each Subscription is backed by Channel, meaning all handlers want to be executed sequentially.
New API opens doors for consuming events via async LINQ - big features with minimal maintenance.

🔗 Related Issues

Contributes to #16095

💥 What does this PR do?

This pull request introduces a significant refactor to event subscription and dispatching in the BiDi (Bidirectional) WebDriver implementation. The main changes revolve around centralizing event handling logic into the EventDispatcher and making it accessible throughout the codebase, simplifying module construction and event subscription APIs, and improving resource management during disposal. The changes also update how modules and browsing context classes interact with event dispatching.

These changes collectively improve the maintainability, reliability, and usability of the BiDi event system.

1. Push-based (callback) — handler

// sync handler
await using var sub = await bidi.SubscribeAsync(NetworkEvent.BeforeRequestSent, (BeforeRequestSentEventArgs e) =>
{
    Console.WriteLine(e.Request.Url);
});

// async handler
await using var sub = await bidi.SubscribeAsync(NetworkEvent.ResponseCompleted, async (ResponseCompletedEventArgs e) =>
{
    await DoSomethingAsync(e);
});

// Dispose to unsubscribe
await sub.DisposeAsync();

2. Pull-based (stream) — IAsyncEnumerable via await foreach

await using var stream = await bidi.StreamAsync(NetworkEvent.BeforeRequestSent);

await foreach (var e in stream)
{
    Console.WriteLine(e.Request.Url);
    if (done) break; // break or dispose to stop
}

3. Multi-event subscription (ordered delivery)

Subscribe to multiple event types with a shared base type:

// Push — all events delivered to one handler in wire order
await using var sub = await bidi.SubscribeAsync<EventArgs>(
    [NetworkEvent.BeforeRequestSent, NetworkEvent.ResponseCompleted],
    (EventArgs e) =>
    {
        switch (e)
        {
            case BeforeRequestSentEventArgs req: /* ... */ break;
            case ResponseCompletedEventArgs resp: /* ... */ break;
        }
    });

// Pull — same but as a stream
await using var stream = await bidi.StreamAsync<EventArgs>(
    [NetworkEvent.BeforeRequestSent, NetworkEvent.ResponseCompleted]);

await foreach (var e in stream) { /* pattern match */ }

4. Module shortcuts — bidi.Network.BeforeRequestSent.SubscribeAsync(...)

Every module exposes IEventSource<T> properties — no need to import event descriptors:

// Push
await using var sub = await bidi.Network.BeforeRequestSent.SubscribeAsync(e =>
{
    Console.WriteLine(e.Request.Url);
});

// Pull
await using var stream = await bidi.Network.BeforeRequestSent.StreamAsync();
await foreach (var e in stream) { /* ... */ }

// Works on all modules:
await bidi.Log.EntryAdded.SubscribeAsync(e => { /* ... */ });
await bidi.BrowsingContext.Load.SubscribeAsync(e => { /* ... */ });
await bidi.BrowsingContext.NavigationStarted.StreamAsync();

Key points:

  • SubscribeAsync = push (callback), StreamAsync = pull (IAsyncEnumerable)
  • Multi-event arrays deliver in wire order, use pattern matching to dispatch
  • await using / DisposeAsync() sends the wire unsubscribe
  • Module shortcuts (bidi.Network.BeforeRequestSent) are the most concise form

🔄 Types of changes

  • New feature (non-breaking change which adds functionality and tests!)
  • Breaking change (fix or feature that would cause existing functionality to change)

Copilot AI review requested due to automatic review settings April 15, 2026 14:23
@selenium-ci selenium-ci added the C-dotnet .NET Bindings label Apr 15, 2026
@qodo-code-review

qodo-code-review Bot commented Apr 15, 2026

Copy link
Copy Markdown
Contributor

Review Summary by Qodo

(Agentic_describe updated until commit 18078e5)

[dotnet] [bidi] Refactor event subscription system with property-based event sources and async streaming (breaking change)

✨ Enhancement 🧪 Tests

Grey Divider

Walkthroughs

Description
  **Major refactoring of BiDi event subscription and streaming system with breaking API changes:**
  * Introduced generic event subscription API with new SubscribeAsync and StreamAsync methods in
  BiDi class supporting single and multiple event descriptors with both sync and async handlers
  * Replaced callback-based OnXxxAsync methods across all modules (BrowsingContext, Network, Script,
  Log, Input, Speculation) with property-based IEventSource<TEventArgs> pattern for cleaner, more
  intuitive event consumption
  * Added IEventStream<TEventArgs> interface enabling LINQ-style async enumerable event consumption
  via StreamAsync methods
  * Completely refactored EventDispatcher to support generic subscriptions with centralized event
  deserialization and delivery through TryDeserializeAndDispatch method
  * Redesigned Subscription<TEventArgs> system with channel-based event buffering and async
  dispatch, replacing the previous non-generic implementation
  * Introduced new EventDescriptor<TEventArgs> abstraction for type-safe event metadata with factory
  functions and JSON serialization info
  * Created new EventStream<TEventArgs> class implementing async enumerable event consumption with
  proper disposal and cancellation token handling
  * Added EventSource and ContextEventSource implementations for global and context-scoped event
  handling with optional filtering
  * Simplified Broker by delegating event handling to EventDispatcher and removing internal event
  metadata management
  * Updated all module interfaces and implementations to use new event source pattern with
  lazy-initialized properties
  * Updated all existing tests to use new property-based event subscription API
  * Added comprehensive new tests for SubscribeAsync, StreamAsync, and LINQ-based event stream
  consumption

Grey Divider

File Changes

1. dotnet/src/webdriver/BiDi/EventDispatcher.cs ✨ Enhancement +189/-56

Event dispatcher refactored for generic subscriptions

• Completely refactored EventDispatcher to support generic event subscription with
 SubscribeAsync and SubscribeReaderAsync methods
• Replaced channel-based event queuing with direct event slot management and subscription tracking
• Added TryDeserializeAndDispatch method to handle event deserialization and delivery to
 subscriptions
• Introduced CompleteAllAsync method for proper shutdown and disposal of all subscriptions

dotnet/src/webdriver/BiDi/EventDispatcher.cs


2. dotnet/src/webdriver/BiDi/Subscription.cs ✨ Enhancement +96/-28

Subscription system redesigned with generics

• Replaced old Subscription class with new Subscription<TEventArgs> generic implementation
• Added ISubscriptionSink interface for event delivery abstraction
• Implemented channel-based event buffering with async dispatch task
• Added support for event filtering and proper error handling with exception dispatch info

dotnet/src/webdriver/BiDi/Subscription.cs


3. dotnet/src/webdriver/BiDi/EventStream.cs ✨ Enhancement +107/-0

New event stream for async LINQ consumption

• New file implementing EventStream<TEventArgs> for async enumerable event consumption
• Supports LINQ-style event stream processing via IAsyncEnumerable<TEventArgs>
• Implements ISubscriptionSink for event delivery and filtering
• Provides proper disposal and cancellation token handling

dotnet/src/webdriver/BiDi/EventStream.cs


View more (45)
4. dotnet/src/webdriver/BiDi/BiDi.cs ✨ Enhancement +54/-5

BiDi class extended with event subscription APIs

• Added EventDispatcher property initialized during connection setup
• Added SubscribeAsync methods supporting single and multiple event descriptors with sync/async
 handlers
• Added StreamAsync methods for async enumerable event consumption
• Updated AsModule to pass EventDispatcher to module creation

dotnet/src/webdriver/BiDi/BiDi.cs


5. dotnet/src/webdriver/BiDi/Broker.cs ✨ Enhancement +9/-68

Broker simplified with centralized event handling

• Removed internal EventDispatcher instance and old subscription methods
• Removed _eventMetadata dictionary and event metadata management
• Updated ProcessReceivedMessage to delegate event deserialization to
 EventDispatcher.TryDeserializeAndDispatch
• Updated disposal logic to complete subscriptions before shutting down transport

dotnet/src/webdriver/BiDi/Broker.cs


6. dotnet/src/webdriver/BiDi/BrowsingContext/BrowsingContext.cs ✨ Enhancement +48/-330

BrowsingContext events converted to property-based sources

• Replaced all OnXxxAsync methods with property-based IEventSource<TEventArgs> properties
• Added lazy-initialized event source properties for all browsing context events
• Implemented CreateContextEventSource helper method for context-filtered event sources
• Updated module initialization to pass EventDispatcher to network and input modules

dotnet/src/webdriver/BiDi/BrowsingContext/BrowsingContext.cs


7. dotnet/src/webdriver/BiDi/BrowsingContext/BrowsingContextModule.cs ✨ Enhancement +28/-201

BrowsingContextModule events refactored to properties

• Removed static Event<TEventArgs, TEventParams> field definitions for all events
• Replaced all OnXxxAsync subscription methods with lazy-initialized IEventSource<TEventArgs>
 properties
• Simplified event exposure through property accessors with thread-safe initialization

dotnet/src/webdriver/BiDi/BrowsingContext/BrowsingContextModule.cs


8. dotnet/src/webdriver/BiDi/BrowsingContext/BrowsingContextEvent.cs ✨ Enhancement +100/-0

New browsing context event descriptors registry

• New file defining static BrowsingContextEvent class with event descriptors
• Provides EventDescriptor<TEventArgs> properties for all browsing context events
• Includes event name, factory function, and JSON type info for each event

dotnet/src/webdriver/BiDi/BrowsingContext/BrowsingContextEvent.cs


9. dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextModule.cs ✨ Enhancement +14/-28

IBrowsingContextModule interface simplified

• Removed all OnXxxAsync method overloads (both sync and async handlers)
• Added property-based IEventSource<TEventArgs> properties for all events
• Simplified interface to use event source pattern instead of callback registration

dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextModule.cs


10. dotnet/src/webdriver/BiDi/BrowsingContext/BrowsingContextNetworkModule.cs ✨ Enhancement +22/-173

BrowsingContextNetworkModule events converted to properties

• Added EventDispatcher parameter to constructor
• Replaced all OnXxxAsync methods with lazy-initialized IEventSource<TEventArgs> properties
• Removed context filtering handler methods, now handled by CreateContextEventSource helper

dotnet/src/webdriver/BiDi/BrowsingContext/BrowsingContextNetworkModule.cs


11. dotnet/src/webdriver/BiDi/BrowsingContext/BrowsingContextInputModule.cs ✨ Enhancement +4/-36

BrowsingContextInputModule event refactored

• Added EventDispatcher parameter to constructor
• Replaced OnFileDialogOpenedAsync methods with FileDialogOpened event source property
• Removed context filtering handler methods

dotnet/src/webdriver/BiDi/BrowsingContext/BrowsingContextInputModule.cs


12. dotnet/src/webdriver/BiDi/Network/NetworkModule.cs ✨ Enhancement +10/-70

NetworkModule events converted to properties

• Removed static Event<TEventArgs, TEventParams> field definitions for all network events
• Replaced all OnXxxAsync subscription methods with lazy-initialized IEventSource<TEventArgs>
 properties
• Simplified event exposure through property accessors

dotnet/src/webdriver/BiDi/Network/NetworkModule.cs


13. dotnet/src/webdriver/BiDi/Network/NetworkEvent.cs ✨ Enhancement +50/-0

New network event descriptors registry

• New file defining static NetworkEvent class with event descriptors
• Provides EventDescriptor<TEventArgs> properties for all network events
• Includes event name, factory function, and JSON type info for each event

dotnet/src/webdriver/BiDi/Network/NetworkEvent.cs


14. dotnet/src/webdriver/BiDi/Network/INetworkModule.cs ✨ Enhancement +5/-10

INetworkModule interface simplified

• Removed all OnXxxAsync method overloads (both sync and async handlers)
• Added property-based IEventSource<TEventArgs> properties for all events
• Simplified interface to use event source pattern

dotnet/src/webdriver/BiDi/Network/INetworkModule.cs


15. dotnet/src/webdriver/BiDi/Script/ScriptModule.cs ✨ Enhancement +6/-53

ScriptModule events converted to properties

• Removed static Event<TEventArgs, TEventParams> field definitions for script events
• Replaced all OnXxxAsync subscription methods with lazy-initialized IEventSource<TEventArgs>
 properties
• Simplified event exposure through property accessors

dotnet/src/webdriver/BiDi/Script/ScriptModule.cs


16. dotnet/src/webdriver/BiDi/Script/ScriptEvent.cs ✨ Enhancement +51/-0

New script event descriptors registry

• New file defining static ScriptEvent class with event descriptors
• Provides EventDescriptor<TEventArgs> properties for message, realm created, and realm destroyed
 events
• Includes event name, factory function, and JSON type info for each event

dotnet/src/webdriver/BiDi/Script/ScriptEvent.cs


17. dotnet/src/webdriver/BiDi/Log/LogModule.cs ✨ Enhancement +2/-21

LogModule event converted to property

• Removed static EntryAddedEvent field definition
• Replaced OnEntryAddedAsync methods with lazy-initialized EntryAdded event source property
• Simplified event exposure through property accessor

dotnet/src/webdriver/BiDi/Log/LogModule.cs


18. dotnet/test/webdriver/BiDi/Session/SessionTests.cs 🧪 Tests +137/-0

New tests for event subscription and streaming APIs

• Added new tests for SubscribeAsync with single and multiple event descriptors
• Added tests for async event stream consumption via StreamAsync and LINQ operations
• Added tests for event stream cancellation token handling
• Added test for custom module event subscription

dotnet/test/webdriver/BiDi/Session/SessionTests.cs


19. dotnet/test/webdriver/BiDi/Network/NetworkTests.cs 🧪 Tests +9/-9

Network tests updated to new event API

• Updated all network event subscription calls from OnXxxAsync to property-based SubscribeAsync
 pattern
• Changed from direct method calls to event source property access followed by subscription

dotnet/test/webdriver/BiDi/Network/NetworkTests.cs


20. dotnet/test/webdriver/BiDi/Network/NetworkEventsTests.cs 🧪 Tests +6/-6

Network event tests updated to new API

• Updated all network event subscription calls from OnXxxAsync to property-based SubscribeAsync
 pattern
• Changed from direct method calls to event source property access followed by subscription

dotnet/test/webdriver/BiDi/Network/NetworkEventsTests.cs


21. dotnet/src/webdriver/BiDi/EventDescriptor.cs ✨ Enhancement +58/-0

New event descriptor abstraction for type-safe events

• New file introducing abstract EventDescriptor base class and generic
 EventDescriptor<TEventArgs> for type-safe event metadata
• Provides factory method Create to construct descriptors with JSON serialization info and event
 args factory
• Enables centralized event definition with name, JSON type info, and args factory function

dotnet/src/webdriver/BiDi/EventDescriptor.cs


22. dotnet/src/webdriver/BiDi/IEventSource.cs ✨ Enhancement +29/-0

New event source interface for flexible event consumption

• New public interface defining event source contract with three methods
• Supports both synchronous Action<TEventArgs> and asynchronous Func<TEventArgs, Task> handlers
• Provides StreamAsync method for consuming events via async enumeration

dotnet/src/webdriver/BiDi/IEventSource.cs


23. dotnet/src/webdriver/BiDi/IEventStream.cs ✨ Enhancement +25/-0

New event stream interface for async enumerable events

• New public interface combining IAsyncEnumerable<TEventArgs> and IAsyncDisposable
• Enables LINQ-based event stream consumption with proper resource cleanup

dotnet/src/webdriver/BiDi/IEventStream.cs


24. dotnet/src/webdriver/BiDi/ISubscription.cs ✨ Enhancement +24/-0

New subscription interface for event lifecycle management

• New public interface for event subscription management
• Extends IAsyncDisposable for proper cleanup of event subscriptions

dotnet/src/webdriver/BiDi/ISubscription.cs


25. dotnet/src/webdriver/BiDi/EventSource.cs ✨ Enhancement +51/-0

New event source implementation for global events

• New internal class implementing IEventSource<TEventArgs> for global events
• Delegates subscription and streaming operations to EventDispatcher
• Supports both sync and async handlers with optional filtering

dotnet/src/webdriver/BiDi/EventSource.cs


26. dotnet/src/webdriver/BiDi/ContextEventSource.cs ✨ Enhancement +55/-0

New event source implementation for context events

• New internal class implementing IEventSource<TEventArgs> for context-scoped events
• Filters events based on browsing context using provided filter predicate
• Delegates to EventDispatcher with context-specific filtering

dotnet/src/webdriver/BiDi/ContextEventSource.cs


27. dotnet/src/webdriver/BiDi/IBiDi.cs ✨ Enhancement +12/-0

New generic event subscription methods in BiDi interface

• Added six new generic SubscribeAsync and StreamAsync methods supporting single and multiple
 event descriptors
• Methods support both synchronous and asynchronous handlers
• Enables flexible event subscription API with type safety

dotnet/src/webdriver/BiDi/IBiDi.cs


28. dotnet/src/webdriver/BiDi/Module.cs ✨ Enhancement +7/-10

Module refactoring for event dispatcher integration

• Added EventDispatcher property to module base class
• Replaced old SubscribeAsync methods with new CreateEventSource helper method
• Updated Create factory method to accept and inject EventDispatcher

dotnet/src/webdriver/BiDi/Module.cs


29. dotnet/src/webdriver/BiDi/Log/LogEvent.cs ✨ Enhancement +36/-0

New log event descriptor definition

• New file defining static LogEvent class with EntryAdded event descriptor
• Uses EventDescriptor<EntryAddedEventArgs>.Create to define event with factory and JSON
 serialization
• Handles multiple log entry types (console, javascript, generic) in factory

dotnet/src/webdriver/BiDi/Log/LogEvent.cs


30. dotnet/src/webdriver/BiDi/Log/ILogModule.cs ✨ Enhancement +1/-2

Log module API refactoring to property-based events

• Replaced two OnEntryAddedAsync methods with single EntryAdded property returning
 IEventSource<EntryAddedEventArgs>
• Simplifies API by using property-based event access instead of method-based subscription

dotnet/src/webdriver/BiDi/Log/ILogModule.cs


31. dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextLogModule.cs ✨ Enhancement +1/-2

Context log module API refactoring to property-based events

• Replaced two OnEntryAddedAsync methods with single EntryAdded property returning
 IEventSource<EntryAddedEventArgs>
• Aligns context-scoped log events with new event source pattern

dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextLogModule.cs


32. dotnet/src/webdriver/BiDi/BrowsingContext/BrowsingContextLogModule.cs ✨ Enhancement +4/-36

Context log module implementation refactoring

• Changed constructor to accept EventDispatcher instead of ILogModule
• Replaced manual event filtering logic with ContextEventSource using filter predicate
• Simplified implementation by delegating to event dispatcher

dotnet/src/webdriver/BiDi/BrowsingContext/BrowsingContextLogModule.cs


33. dotnet/src/webdriver/BiDi/Input/InputEvent.cs ✨ Enhancement +30/-0

New input event descriptor definition

• New file defining static InputEvent class with FileDialogOpened event descriptor
• Uses EventDescriptor<FileDialogOpenedEventArgs>.Create with factory and JSON serialization

dotnet/src/webdriver/BiDi/Input/InputEvent.cs


34. dotnet/src/webdriver/BiDi/Input/IInputModule.cs ✨ Enhancement +1/-2

Input module API refactoring to property-based events

• Replaced two OnFileDialogOpenedAsync methods with single FileDialogOpened property returning
 IEventSource<FileDialogOpenedEventArgs>
• Simplifies API by using property-based event access

dotnet/src/webdriver/BiDi/Input/IInputModule.cs


35. dotnet/src/webdriver/BiDi/Input/InputModule.cs ✨ Enhancement +2/-14

Input module implementation refactoring to event sources

• Removed static FileDialogOpenedEvent definition
• Replaced two OnFileDialogOpenedAsync methods with lazy-initialized FileDialogOpened property
• Uses CreateEventSource helper with InputEvent.FileDialogOpened descriptor

dotnet/src/webdriver/BiDi/Input/InputModule.cs


36. dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextInputModule.cs ✨ Enhancement +1/-2

Context input module API refactoring to property-based events

• Replaced two OnFileDialogOpenedAsync methods with single FileDialogOpened property returning
 IEventSource<FileDialogOpenedEventArgs>
• Aligns context-scoped input events with new event source pattern

dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextInputModule.cs


37. dotnet/src/webdriver/BiDi/Script/IScriptModule.cs ✨ Enhancement +3/-6

Script module API refactoring to property-based events

• Replaced six OnMessageAsync, OnRealmCreatedAsync, and OnRealmDestroyedAsync methods with
 three properties
• New properties return IEventSource<TEventArgs> for Message, RealmCreated, and
 RealmDestroyed events

dotnet/src/webdriver/BiDi/Script/IScriptModule.cs


38. dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextNetworkModule.cs ✨ Enhancement +5/-10

Network module API refactoring to property-based events

• Replaced ten OnXxxAsync methods with five properties returning IEventSource<TEventArgs>
• New properties for AuthRequired, BeforeRequestSent, FetchError, ResponseCompleted, and
 ResponseStarted events

dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextNetworkModule.cs


39. dotnet/src/webdriver/BiDi/Speculation/SpeculationEvent.cs ✨ Enhancement +30/-0

New speculation event descriptor definition

• New file defining static SpeculationEvent class with PrefetchStatusUpdated event descriptor
• Uses EventDescriptor<PrefetchStatusUpdatedEventArgs>.Create with factory and JSON serialization

dotnet/src/webdriver/BiDi/Speculation/SpeculationEvent.cs


40. dotnet/src/webdriver/BiDi/Speculation/ISpeculationModule.cs ✨ Enhancement +1/-2

Speculation module API refactoring to property-based events

• Replaced two OnPrefetchStatusUpdatedAsync methods with single PrefetchStatusUpdated property
 returning IEventSource<PrefetchStatusUpdatedEventArgs>
• Simplifies API by using property-based event access

dotnet/src/webdriver/BiDi/Speculation/ISpeculationModule.cs


41. dotnet/src/webdriver/BiDi/Speculation/SpeculationModule.cs ✨ Enhancement +2/-15

Speculation module implementation refactoring to event sources

• Removed static PrefetchStatusUpdatedEvent definition and unused import
• Replaced two OnPrefetchStatusUpdatedAsync methods with lazy-initialized PrefetchStatusUpdated
 property
• Uses CreateEventSource helper with SpeculationEvent.PrefetchStatusUpdated descriptor

dotnet/src/webdriver/BiDi/Speculation/SpeculationModule.cs


42. dotnet/src/webdriver/BiDi/EventArgs.cs Formatting +1/-9

Copyright header filename correction

• Fixed copyright header filename from Event.cs to EventArgs.cs

dotnet/src/webdriver/BiDi/EventArgs.cs


43. dotnet/test/webdriver/BiDi/Log/LogTests.cs 🧪 Tests +3/-3

Log tests updated for new event source API

• Updated three test cases to use new EntryAdded.SubscribeAsync property-based API
• Changed from OnEntryAddedAsync(tcs.SetResult) to `EntryAdded.SubscribeAsync(e =>
 tcs.TrySetResult(e))`

dotnet/test/webdriver/BiDi/Log/LogTests.cs


44. dotnet/test/webdriver/BiDi/Script/ScriptEventsTests.cs 🧪 Tests +3/-3

Script event tests updated for new event source API

• Updated three test cases to use new property-based event source API
• Changed from OnXxxAsync(tcs.SetResult) to Xxx.SubscribeAsync(e => tcs.TrySetResult(e)) pattern
• Added explicit subscription disposal with await using statement

dotnet/test/webdriver/BiDi/Script/ScriptEventsTests.cs


45. dotnet/test/webdriver/BiDi/BrowsingContext/BrowsingContextEventsTests.cs 🧪 Tests +2/-2

Browsing context event tests updated for new API

• Updated two test cases to use new property-based event source API
• Changed from OnXxxAsync(tcs.SetResult) to Xxx.SubscribeAsync(e => tcs.TrySetResult(e)) pattern

dotnet/test/webdriver/BiDi/BrowsingContext/BrowsingContextEventsTests.cs


46. dotnet/test/webdriver/BiDi/Input/InputEventsTests.cs 🧪 Tests +1/-1

Input event tests updated for new event source API

• Updated test case to use new FileDialogOpened.SubscribeAsync property-based API
• Changed from OnFileDialogOpenedAsync(tcs.SetResult) to `FileDialogOpened.SubscribeAsync(e =>
 tcs.TrySetResult(e))`

dotnet/test/webdriver/BiDi/Input/InputEventsTests.cs


47. dotnet/test/webdriver/BiDi/Script/ScriptCommandsTests.cs 🧪 Tests +1/-1

Script command tests updated for new event source API

• Updated test case to use new EntryAdded.SubscribeAsync property-based API
• Changed from OnEntryAddedAsync(tcs.SetResult) to `EntryAdded.SubscribeAsync(e =>
 tcs.TrySetResult(e))`

dotnet/test/webdriver/BiDi/Script/ScriptCommandsTests.cs


48. dotnet/test/webdriver/BiDi/Speculation/SpeculationTests.cs 🧪 Tests +1/-1

Speculation tests updated for new event source API

• Updated test case to use new PrefetchStatusUpdated.SubscribeAsync property-based API
• Changed from OnPrefetchStatusUpdatedAsync(args => ...) to
 PrefetchStatusUpdated.SubscribeAsync(args => ...)

dotnet/test/webdriver/BiDi/Speculation/SpeculationTests.cs


Grey Divider

Qodo Logo

@qodo-code-review

qodo-code-review Bot commented Apr 15, 2026

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (4) 📘 Rule violations (4) 📎 Requirement gaps (1)

Grey Divider


Action required

1. CompleteAllAsync blocks shutdown 📘 Rule violation ☼ Reliability
Description
Broker.DisposeAsync() awaits EventDispatcher.CompleteAllAsync() before cancelling the receive
loop, so if CompleteAllAsync throws the transport/processing tasks may never be
cancelled/awaited/disposed. This can leak resources or hang shutdown, violating required cleanup
ordering and failure-path guarantees.
Code

dotnet/src/webdriver/BiDi/Broker.cs[R148-153]

+            // Dispose subscriptions while transport and processing loop are still active,
+            // allowing wire unsubscribe commands to be sent and handler drain tasks to complete.
+            await _bidi.EventDispatcher.CompleteAllAsync(_terminalReceiveException).ConfigureAwait(false);
+
+            _receiveMessagesCancellationTokenSource.Cancel();
+
Evidence
PR Compliance ID 20 requires cleanup to run on all failure paths and correct shutdown ordering. In
Broker.DisposeAsync(), cancellation (_receiveMessagesCancellationTokenSource.Cancel()) happens
only after await _bidi.EventDispatcher.CompleteAllAsync(...), so an exception from
CompleteAllAsync can skip the rest of shutdown.

dotnet/src/webdriver/BiDi/Broker.cs[148-166]
Best Practice: Learned patterns

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`Broker.DisposeAsync()` can skip cancellation and subsequent cleanup if `EventDispatcher.CompleteAllAsync(...)` throws, potentially hanging shutdown and leaking transport/processing resources.

## Issue Context
`CompleteAllAsync` may throw (e.g., subscription disposal errors). Shutdown should still cancel the receive loop, await/drain tasks, and dispose transport, then optionally rethrow the completion error.

## Fix Focus Areas
- dotnet/src/webdriver/BiDi/Broker.cs[148-166]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Removed On*Async without deprecation 📘 Rule violation ⚙ Maintainability
Description
Public BiDi module interfaces remove On*Async event-subscription methods and replace them with
IEventSource<T> properties, which is a breaking API change without any deprecation period/message.
This violates the requirements to maintain compatibility and to deprecate before removal with an
alternative indicated.
Code

dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextModule.cs[R32-45]

+    IEventSource<ContextCreatedEventArgs> ContextCreated { get; }
+    IEventSource<ContextDestroyedEventArgs> ContextDestroyed { get; }
+    IEventSource<DomContentLoadedEventArgs> DomContentLoaded { get; }
+    IEventSource<DownloadEndEventArgs> DownloadEnd { get; }
+    IEventSource<DownloadWillBeginEventArgs> DownloadWillBegin { get; }
+    IEventSource<FragmentNavigatedEventArgs> FragmentNavigated { get; }
+    IEventSource<HistoryUpdatedEventArgs> HistoryUpdated { get; }
+    IEventSource<LoadEventArgs> Load { get; }
+    IEventSource<NavigationAbortedEventArgs> NavigationAborted { get; }
+    IEventSource<NavigationCommittedEventArgs> NavigationCommitted { get; }
+    IEventSource<NavigationFailedEventArgs> NavigationFailed { get; }
+    IEventSource<NavigationStartedEventArgs> NavigationStarted { get; }
+    IEventSource<UserPromptClosedEventArgs> UserPromptClosed { get; }
+    IEventSource<UserPromptOpenedEventArgs> UserPromptOpened { get; }
Evidence
PR Compliance IDs 12 and 17 require avoiding unannounced breaking API changes and deprecating public
functionality before removal with an alternative. The interface now exposes only IEventSource<T>
properties where the On*Async methods previously existed, forcing consumer code changes without a
deprecation path.

AGENTS.md
AGENTS.md
dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextModule.cs[32-45]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`On*Async` methods were removed from public module interfaces and replaced with `IEventSource<T>` properties, creating a breaking change without deprecation guidance.

## Issue Context
To meet compliance, keep the old methods temporarily and mark them `[Obsolete("Use <EventName>.SubscribeAsync(...) or <EventName>.StreamAsync(...) instead") ]`, implementing them as thin wrappers over the new `IEventSource<T>` API.

## Fix Focus Areas
- dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextModule.cs[32-45]
- dotnet/src/webdriver/BiDi/Network/INetworkModule.cs[31-35]
- dotnet/src/webdriver/BiDi/Input/IInputModule.cs[24-24]
- dotnet/src/webdriver/BiDi/Log/ILogModule.cs[24-24]
- dotnet/src/webdriver/BiDi/Script/IScriptModule.cs[33-35]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. IBiDi cast can throw 🐞 Bug ≡ Correctness
Description
BrowsingContext.Log/Network/Input unconditionally cast the stored IBiDi instance to the concrete
BiDi type to access EventDispatcher, which can throw InvalidCastException for any non-BiDi IBiDi
implementation. Because BrowsingContext has a public constructor that accepts IBiDi, callers can hit
this at runtime when using test doubles or alternate implementations.
Code

dotnet/src/webdriver/BiDi/BrowsingContext/BrowsingContext.cs[R49-52]

+    public IBrowsingContextLogModule Log => _logModule ?? Interlocked.CompareExchange(ref _logModule, new BrowsingContextLogModule(this, ((BiDi)BiDi).EventDispatcher), null) ?? _logModule;

    [JsonIgnore]
-    public IBrowsingContextNetworkModule Network => _networkModule ?? Interlocked.CompareExchange(ref _networkModule, new BrowsingContextNetworkModule(this, BiDi.Network), null) ?? _networkModule;
+    public IBrowsingContextNetworkModule Network => _networkModule ?? Interlocked.CompareExchange(ref _networkModule, new BrowsingContextNetworkModule(this, BiDi.Network, ((BiDi)BiDi).EventDispatcher), null) ?? _networkModule;
Evidence
The type contract allows any IBiDi to be provided to BrowsingContext, but the new module properties
require the concrete BiDi type via a hard cast; this is a direct runtime crash path.

dotnet/src/webdriver/BiDi/BrowsingContext/BrowsingContext.cs[30-35]
dotnet/src/webdriver/BiDi/BrowsingContext/BrowsingContext.cs[49-52]
dotnet/src/webdriver/BiDi/BrowsingContext/BrowsingContext.cs[60-62]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`BrowsingContext` accepts an `IBiDi`, but `Log`/`Network`/`Input` now cast it to the concrete `BiDi` type to get `EventDispatcher`, which can throw `InvalidCastException`.

### Issue Context
This breaks substitutability of `IBiDi` (notably for tests/mocks) and creates a runtime crash path.

### Fix Focus Areas
- dotnet/src/webdriver/BiDi/BrowsingContext/BrowsingContext.cs[30-35]
- dotnet/src/webdriver/BiDi/BrowsingContext/BrowsingContext.cs[49-52]
- dotnet/src/webdriver/BiDi/BrowsingContext/BrowsingContext.cs[60-62]

### Suggested direction
Prefer one of:
1) Change `BrowsingContext` to store a concrete `BiDi` (constructor takes `BiDi`), or
2) Introduce an internal accessor interface implemented by `BiDi` (e.g., `IEventDispatcherOwner`) and use a safe cast with a clear exception message, or
3) Refactor context-scoped event sources to rely on public `IBiDi` APIs instead of requiring `EventDispatcher`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (8)
4. SubscriptionOptions.Timeout removed 📘 Rule violation ⚙ Maintainability
Description
The Timeout property was removed from the public SubscriptionOptions type (and related context
options), which is a breaking API/ABI change for existing callers. The removal is not preceded by
deprecation with guidance to replacement behavior.
Code

dotnet/src/webdriver/BiDi/Subscription.cs[57]

-    public TimeSpan? Timeout { get; init; }
+    public IEnumerable<Browser.UserContext>? UserContexts { get; init; }
Evidence
PR Compliance ID 3 disallows breaking public API changes, and PR Compliance ID 7 requires
deprecation before removal. The diff deletes public TimeSpan? Timeout { get; init; } from
SubscriptionOptions.

AGENTS.md
AGENTS.md
dotnet/src/webdriver/BiDi/Subscription.cs[141-163]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`SubscriptionOptions.Timeout` was removed, which breaks existing consumers.

## Issue Context
Public options types are part of the API contract; removals must be deprecated first and/or supported via a compatibility layer.

## Fix Focus Areas
- dotnet/src/webdriver/BiDi/Subscription.cs[141-163]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. IBiDi takes IEnumerable 📎 Requirement gap ☼ Reliability
Description
New public subscription/stream APIs accept IEnumerable<EventDescriptor> inputs, which can be lazy
or mutable and can throw or behave nondeterministically if modified during
enumeration/serialization. This violates the guideline to prefer materialized/immutable collection
inputs for command/options-like APIs.
Code

dotnet/src/webdriver/BiDi/IBiDi.cs[R64-70]

+    Task<ISubscription> SubscribeAsync<TEventArgs>(IEnumerable<EventDescriptor> descriptors, Action<TEventArgs> handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default) where TEventArgs : EventArgs;
+
+    Task<ISubscription> SubscribeAsync<TEventArgs>(IEnumerable<EventDescriptor> descriptors, Func<TEventArgs, Task> handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default) where TEventArgs : EventArgs;
+
+    Task<IEventStream<TEventArgs>> ReadAllAsync<TEventArgs>(EventDescriptor<TEventArgs> descriptor, EventStreamOptions? options = null, CancellationToken cancellationToken = default) where TEventArgs : EventArgs;
+
+    Task<IEventStream<TEventArgs>> ReadAllAsync<TEventArgs>(IEnumerable<EventDescriptor> descriptors, EventStreamOptions? options = null, CancellationToken cancellationToken = default) where TEventArgs : EventArgs;
Evidence
PR Compliance ID 2 requires avoiding mutable IEnumerable inputs; the diff introduces multiple new
public APIs on IBiDi that accept IEnumerable<EventDescriptor> (and similarly in the BiDi
implementation), enabling mutation-related runtime failures during enumeration.

Command APIs must avoid accepting mutable IEnumerable inputs; prefer materialized/immutable collections (e.g., ImmutableArray)
dotnet/src/webdriver/BiDi/IBiDi.cs[64-70]
dotnet/src/webdriver/BiDi/BiDi.cs[97-125]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
New public APIs accept `IEnumerable<EventDescriptor>` inputs, which can be mutated/lazy and cause runtime failures during enumeration.

## Issue Context
Compliance requires using materialized/immutable collection types (e.g., `ImmutableArray`) for collection inputs to avoid mutation-related exceptions.

## Fix Focus Areas
- dotnet/src/webdriver/BiDi/IBiDi.cs[64-70]
- dotnet/src/webdriver/BiDi/BiDi.cs[97-125]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. SingleWriter channel race 🐞 Bug ☼ Reliability
Description
Subscription<TEventArgs> creates a channel with SingleWriter=true but calls
Writer.TryWrite/TryComplete from multiple threads (dispatcher delivery, DispatchEventsAsync task,
and disposal/shutdown). This violates Channel’s single-writer contract and can cause dropped events
or runtime failures under load or during shutdown.
Code

dotnet/src/webdriver/BiDi/Subscription.cs[R44-45]

+    private readonly Channel<TEventArgs> _channel = Channel.CreateUnbounded<TEventArgs>(
+        new UnboundedChannelOptions { SingleReader = true, SingleWriter = true });
Evidence
The subscription channel is configured for a single writer, but writes/completions occur from (1)
the broker processing path via EventDispatcher.TryDeserializeAndDispatch -> subscription.Deliver ->
TryWrite, (2) the background dispatch task started with Task.Run which calls TryComplete on handler
exception, and (3) shutdown code calling CompleteAllAsync before canceling the receive/processing
loops. These code paths can run concurrently, meaning multiple threads can touch the writer despite
SingleWriter=true.

dotnet/src/webdriver/BiDi/Subscription.cs[44-54]
dotnet/src/webdriver/BiDi/Subscription.cs[56-69]
dotnet/src/webdriver/BiDi/Subscription.cs[105-122]
dotnet/src/webdriver/BiDi/EventDispatcher.cs[109-147]
dotnet/src/webdriver/BiDi/Broker.cs[151-172]
dotnet/src/webdriver/BiDi/EventStream.cs[33-54]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`Subscription<TEventArgs>` (and `EventStream<TEventArgs>`) create channels with `SingleWriter = true`, but the channel writer is used from multiple threads (dispatcher delivery thread, the subscription’s dispatch task, and dispose/shutdown). This violates `Channel`’s single-writer contract and can lead to race conditions (dropped events or runtime failures).

### Issue Context
- `Subscription<TEventArgs>` starts a background task (`Task.Run(DispatchEventsAsync)`) which may call `_channel.Writer.TryComplete(ex)`.
- Event delivery calls `_channel.Writer.TryWrite(...)` from `EventDispatcher.TryDeserializeAndDispatch`.
- Shutdown (`Broker.DisposeAsync`) calls `EventDispatcher.CompleteAllAsync(...)` before canceling/awaiting the processing loop, so `Complete()`/`DisposeAsync()` can run concurrently with delivery.

### Fix Focus Areas
- dotnet/src/webdriver/BiDi/Subscription.cs[44-45]
- dotnet/src/webdriver/BiDi/Subscription.cs[105-122]
- dotnet/src/webdriver/BiDi/EventStream.cs[33-34]

### Suggested change
- Remove the `SingleWriter = true` optimization (set it to `false` or omit it) in both `Subscription<TEventArgs>` and `EventStream<TEventArgs>`.
- If you want to keep `SingleWriter = true`, refactor so that **only one thread** ever calls into the channel writer (e.g., ensure `TryComplete` happens on the same thread that performs delivery, and prevent dispose/shutdown from racing with delivery).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Added methods to IBiDi ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
New members were added to the public IBiDi interface, which is a breaking API/ABI change for any
downstream implementers of the interface. This violates the requirement to keep public interfaces
backward-compatible.
Code

dotnet/src/webdriver/BiDi/IBiDi.cs[R60-70]

+    Task<ISubscription> SubscribeAsync<TEventArgs>(EventDescriptor<TEventArgs> descriptor, Action<TEventArgs> handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default) where TEventArgs : EventArgs;
+
+    Task<ISubscription> SubscribeAsync<TEventArgs>(EventDescriptor<TEventArgs> descriptor, Func<TEventArgs, Task> handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default) where TEventArgs : EventArgs;
+
+    Task<ISubscription> SubscribeAsync<TEventArgs>(IEnumerable<EventDescriptor> descriptors, Action<TEventArgs> handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default) where TEventArgs : EventArgs;
+
+    Task<ISubscription> SubscribeAsync<TEventArgs>(IEnumerable<EventDescriptor> descriptors, Func<TEventArgs, Task> handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default) where TEventArgs : EventArgs;
+
+    Task<IEventStream<TEventArgs>> ReadAllAsync<TEventArgs>(EventDescriptor<TEventArgs> descriptor, EventStreamOptions? options = null, CancellationToken cancellationToken = default) where TEventArgs : EventArgs;
+
+    Task<IEventStream<TEventArgs>> ReadAllAsync<TEventArgs>(IEnumerable<EventDescriptor> descriptors, EventStreamOptions? options = null, CancellationToken cancellationToken = default) where TEventArgs : EventArgs;
Evidence
PR Compliance ID 3 forbids breaking changes to public APIs/ABIs; adding methods to a public
interface breaks compilation for external implementers. The diff adds multiple new
SubscribeAsync/ReadAllAsync members to IBiDi.

AGENTS.md
dotnet/src/webdriver/BiDi/IBiDi.cs[60-70]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`IBiDi` gained new members, which is a breaking change for any external implementers.

## Issue Context
Adding methods to a public interface is an ABI/API breaking change in .NET.

## Fix Focus Areas
- dotnet/src/webdriver/BiDi/IBiDi.cs[60-70]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


8. DrainAsync swallows cancellation ✓ Resolved 📘 Rule violation ☼ Reliability
Description
Subscription<TEventArgs>.DrainAsync catches Exception and logs it, which will also swallow
OperationCanceledException and can break cooperative cancellation. This can prevent
shutdown/cancel flows from propagating correctly and leave tasks running unexpectedly.
Code

dotnet/src/webdriver/BiDi/Subscription.cs[R124-136]

+                if (_handler is not null)
+                {
+                    try
+                    {
+                        await _handler(args).ConfigureAwait(false);
+                    }
+                    catch (Exception ex)
+                    {
+                        if (Logger.IsEnabled(LogEventLevel.Error))
+                        {
+                            Logger.Error($"Unhandled error processing BiDi event handler: {ex}");
+                        }
+                    }
Evidence
PR Compliance ID 14 requires preserving cancellation signals when catching broad exceptions. The
added catch (Exception ex) block in DrainAsync does not special-case
OperationCanceledException, so cancellation can be suppressed instead of being
propagated/preserved.

dotnet/src/webdriver/BiDi/Subscription.cs[124-136]
Best Practice: Learned patterns

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`DrainAsync` catches all exceptions from user handlers, which also catches `OperationCanceledException` and suppresses cancellation.

## Issue Context
Cancellation should remain observable and should not be unintentionally converted into a logged-and-ignored error.

## Fix Focus Areas
- dotnet/src/webdriver/BiDi/Subscription.cs[118-137]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


9. Unsubscribe cleanup not exception-safe ✓ Resolved 🐞 Bug ☼ Reliability
Description
Broker.UnsubscribeAsync only removes the subscription from the local registry after the remote
UnsubscribeAsync succeeds, so if the transport call throws the subscription remains registered and
still traversed for every event. Subscription.DisposeAsync also sets its disposed flag before
awaiting UnsubscribeAsync, preventing retry after a transient failure and increasing the chance of a
permanent local leak.
Code

dotnet/src/webdriver/BiDi/Broker.cs[R93-101]

    public async ValueTask UnsubscribeAsync(Subscription subscription, CancellationToken cancellationToken)
    {
        await _bidi.Session.UnsubscribeAsync([subscription.SubscriptionId], null, cancellationToken).ConfigureAwait(false);
-        _eventDispatcher.RemoveHandler(subscription.EventName, subscription.Handler);
+
+        if (_subscriptions.TryGetValue(subscription.EventName, out var registry))
+        {
+            registry.Remove(subscription);
+        }
    }
Evidence
Broker.UnsubscribeAsync has no try/finally, so local registry removal is skipped on exceptions from
the remote unsubscribe call. Separately, Subscription.DisposeAsync marks the subscription disposed
before awaiting UnsubscribeAsync; if that await throws, subsequent DisposeAsync calls will not
attempt cleanup again.

dotnet/src/webdriver/BiDi/Broker.cs[93-101]
dotnet/src/webdriver/BiDi/Subscription.cs[50-57]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`Broker.UnsubscribeAsync` performs local cleanup (removing from `_subscriptions`) only after the remote unsubscribe completes successfully. If the remote call throws, the local subscription remains in the registry, and the broker continues iterating it on every event.

Additionally, `Subscription.DisposeAsync` sets `_disposed` before awaiting `UnsubscribeAsync()`. If `UnsubscribeAsync()` throws, the subscription becomes non-retryable for disposal/unsubscribe, making the local leak harder to recover from.

### Issue Context
- Local cleanup should be resilient to transport/remote failures.
- Even if the remote unsubscribe fails, local detachment should happen to stop further event delivery and avoid retaining stale subscriptions.

### Fix Focus Areas
- dotnet/src/webdriver/BiDi/Broker.cs[93-101]
- dotnet/src/webdriver/BiDi/Subscription.cs[50-57]

### Suggested change
1) Wrap the remote unsubscribe in `try/finally` so local registry removal always runs:
```csharp
public async ValueTask UnsubscribeAsync(Subscription subscription, CancellationToken cancellationToken)
{
 try
 {
   await _bidi.Session.UnsubscribeAsync([subscription.SubscriptionId], null, cancellationToken).ConfigureAwait(false);
 }
 finally
 {
   if (_subscriptions.TryGetValue(subscription.EventName, out var registry))
     registry.Remove(subscription);
 }
}
```

2) Consider making `Subscription.DisposeAsync` retry-safe by only setting `_disposed` after a successful unsubscribe, or resetting `_disposed` on failure (so callers can retry), e.g. `try { ... } catch { Interlocked.Exchange(ref _disposed, 0); throw; }`.

This prevents permanent local leaks when the transport is flaky or shutting down.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


10. Enumerator child-channel leak ✓ Resolved 🐞 Bug ☼ Reliability
Description
Subscription<TEventArgs>.GetAsyncEnumerator() adds a per-enumerator child Channel to _children but
never removes it when enumeration ends or is cancelled, so DrainAsync keeps writing events into
abandoned channels for the lifetime of the subscription. This can cause unbounded memory growth and
retain references even after consumers stop iterating.
Code

dotnet/src/webdriver/BiDi/Subscription.cs[R91-107]

+    public IAsyncEnumerator<TEventArgs> GetAsyncEnumerator(CancellationToken cancellationToken = default)
+    {
+        var child = Channel.CreateUnbounded<TEventArgs>();
+        lock (_children) { _children.Add(child); }
+        return ReadChannelAsync(child.Reader, cancellationToken);
+    }
+
+    private static async IAsyncEnumerator<TEventArgs> ReadChannelAsync(ChannelReader<TEventArgs> reader, CancellationToken cancellationToken)
+    {
+        while (await reader.WaitToReadAsync(cancellationToken).ConfigureAwait(false))
+        {
+            while (reader.TryRead(out var item))
+            {
+                yield return item;
+            }
+        }
+    }
Evidence
GetAsyncEnumerator creates an unbounded child channel and only ever adds it to the _children list;
ReadChannelAsync has no cleanup path on cancellation/completion, and DrainAsync writes every
incoming event to every child in _children. Children are only completed when the *subscription*
finishes draining, not when an individual enumerator is disposed/cancelled, so abandoned enumerators
remain writable targets indefinitely.

dotnet/src/webdriver/BiDi/Subscription.cs[91-107]
dotnet/src/webdriver/BiDi/Subscription.cs[118-157]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`Subscription<TEventArgs>.GetAsyncEnumerator()` registers a per-enumerator child `Channel<TEventArgs>` in `_children` but never unregisters it when the consumer stops iterating (natural completion, `DisposeAsync`, or cancellation). As a result, `DrainAsync()` continues to write every event into channels that no longer have active readers, leading to unbounded buffering/memory growth and retained references.

### Issue Context
- Child channels are stored in `_children` and written-to on every event.
- Cancellation of the returned async iterator does not trigger any code that removes the child from `_children`.

### Fix Focus Areas
- dotnet/src/webdriver/BiDi/Subscription.cs[91-107]
- dotnet/src/webdriver/BiDi/Subscription.cs[118-157]

### Suggested change
Implement enumeration via an instance `async IAsyncEnumerable<TEventArgs>` that:
1) creates and registers the child channel,
2) yields items from `child.Reader.ReadAllAsync(cancellationToken)`,
3) **in a `finally` block** removes the child from `_children` and completes the child writer.

Example shape:
```csharp
public IAsyncEnumerator<TEventArgs> GetAsyncEnumerator(CancellationToken ct = default)
 => Enumerate(ct).GetAsyncEnumerator(ct);

private async IAsyncEnumerable<TEventArgs> Enumerate([EnumeratorCancellation] CancellationToken ct)
{
 var child = Channel.CreateUnbounded<TEventArgs>(new UnboundedChannelOptions { SingleReader = true, SingleWriter = true });
 lock (_children) _children.Add(child);
 try
 {
   await foreach (var item in child.Reader.ReadAllAsync(ct).ConfigureAwait(false))
     yield return item;
 }
 finally
 {
   lock (_children) _children.Remove(child);
   child.Writer.TryComplete();
 }
}
```
This ensures abandoned enumerators do not keep accumulating writes.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


11. Task<Subscription> overloads removed 📘 Rule violation ⚙ Maintainability
Description
Public BiDi module interfaces changed from returning Task<Subscription> to
Task<Subscription<TEventArgs>>, which is a breaking API/ABI change for existing consumers.
Upgrading would require code changes beyond version bump, violating the compatibility requirement.
Code

dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextModule.cs[L32-59]

-    Task<Subscription> OnContextCreatedAsync(Func<ContextCreatedEventArgs, Task> handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default);
-    Task<Subscription> OnContextCreatedAsync(Action<ContextCreatedEventArgs> handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default);
-    Task<Subscription> OnContextDestroyedAsync(Func<ContextDestroyedEventArgs, Task> handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default);
-    Task<Subscription> OnContextDestroyedAsync(Action<ContextDestroyedEventArgs> handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default);
-    Task<Subscription> OnDomContentLoadedAsync(Func<DomContentLoadedEventArgs, Task> handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default);
-    Task<Subscription> OnDomContentLoadedAsync(Action<DomContentLoadedEventArgs> handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default);
-    Task<Subscription> OnDownloadEndAsync(Func<DownloadEndEventArgs, Task> handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default);
-    Task<Subscription> OnDownloadEndAsync(Action<DownloadEndEventArgs> handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default);
-    Task<Subscription> OnDownloadWillBeginAsync(Func<DownloadWillBeginEventArgs, Task> handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default);
-    Task<Subscription> OnDownloadWillBeginAsync(Action<DownloadWillBeginEventArgs> handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default);
-    Task<Subscription> OnFragmentNavigatedAsync(Func<FragmentNavigatedEventArgs, Task> handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default);
-    Task<Subscription> OnFragmentNavigatedAsync(Action<FragmentNavigatedEventArgs> handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default);
-    Task<Subscription> OnHistoryUpdatedAsync(Func<HistoryUpdatedEventArgs, Task> handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default);
-    Task<Subscription> OnHistoryUpdatedAsync(Action<HistoryUpdatedEventArgs> handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default);
-    Task<Subscription> OnLoadAsync(Func<LoadEventArgs, Task> handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default);
-    Task<Subscription> OnLoadAsync(Action<LoadEventArgs> handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default);
-    Task<Subscription> OnNavigationAbortedAsync(Func<NavigationAbortedEventArgs, Task> handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default);
-    Task<Subscription> OnNavigationAbortedAsync(Action<NavigationAbortedEventArgs> handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default);
-    Task<Subscription> OnNavigationCommittedAsync(Func<NavigationCommittedEventArgs, Task> handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default);
-    Task<Subscription> OnNavigationCommittedAsync(Action<NavigationCommittedEventArgs> handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default);
-    Task<Subscription> OnNavigationFailedAsync(Func<NavigationFailedEventArgs, Task> handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default);
-    Task<Subscription> OnNavigationFailedAsync(Action<NavigationFailedEventArgs> handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default);
-    Task<Subscription> OnNavigationStartedAsync(Func<NavigationStartedEventArgs, Task> handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default);
-    Task<Subscription> OnNavigationStartedAsync(Action<NavigationStartedEventArgs> handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default);
-    Task<Subscription> OnUserPromptClosedAsync(Func<UserPromptClosedEventArgs, Task> handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default);
-    Task<Subscription> OnUserPromptClosedAsync(Action<UserPromptClosedEventArgs> handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default);
-    Task<Subscription> OnUserPromptOpenedAsync(Func<UserPromptOpenedEventArgs, Task> handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default);
-    Task<Subscription> OnUserPromptOpenedAsync(Action<UserPromptOpenedEventArgs> handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default);
+    Task<Subscription<ContextCreatedEventArgs>> OnContextCreatedAsync(SubscriptionOptions? options = null, CancellationToken cancellationToken = default);
+    Task<Subscription<ContextCreatedEventArgs>> OnContextCreatedAsync(Func<ContextCreatedEventArgs, Task> handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default);
+    Task<Subscription<ContextCreatedEventArgs>> OnContextCreatedAsync(Action<ContextCreatedEventArgs> handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default);
+    Task<Subscription<ContextDestroyedEventArgs>> OnContextDestroyedAsync(SubscriptionOptions? options = null, CancellationToken cancellationToken = default);
+    Task<Subscription<ContextDestroyedEventArgs>> OnContextDestroyedAsync(Func<ContextDestroyedEventArgs, Task> handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default);
+    Task<Subscription<ContextDestroyedEventArgs>> OnContextDestroyedAsync(Action<ContextDestroyedEventArgs> handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default);
+    Task<Subscription<DomContentLoadedEventArgs>> OnDomContentLoadedAsync(SubscriptionOptions? options = null, CancellationToken cancellationToken = default);
+    Task<Subscription<DomContentLoadedEventArgs>> OnDomContentLoadedAsync(Func<DomContentLoadedEventArgs, Task> handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default);
+    Task<Subscription<DomContentLoadedEventArgs>> OnDomContentLoadedAsync(Action<DomContentLoadedEventArgs> handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default);
+    Task<Subscription<DownloadEndEventArgs>> OnDownloadEndAsync(SubscriptionOptions? options = null, CancellationToken cancellationToken = default);
+    Task<Subscription<DownloadEndEventArgs>> OnDownloadEndAsync(Func<DownloadEndEventArgs, Task> handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default);
+    Task<Subscription<DownloadEndEventArgs>> OnDownloadEndAsync(Action<DownloadEndEventArgs> handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default);
+    Task<Subscription<DownloadWillBeginEventArgs>> OnDownloadWillBeginAsync(SubscriptionOptions? options = null, CancellationToken cancellationToken = default);
+    Task<Subscription<DownloadWillBeginEventArgs>> OnDownloadWillBeginAsync(Func<DownloadWillBeginEventArgs, Task> handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default);
+    Task<Subscription<DownloadWillBeginEventArgs>> OnDownloadWillBeginAsync(Action<DownloadWillBeginEventArgs> handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default);
+    Task<Subscription<FragmentNavigatedEventArgs>> OnFragmentNavigatedAsync(SubscriptionOptions? options = null, CancellationToken cancellationToken = default);
+    Task<Subscription<FragmentNavigatedEventArgs>> OnFragmentNavigatedAsync(Func<FragmentNavigatedEventArgs, Task> handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default);
+    Task<Subscription<FragmentNavigatedEventArgs>> OnFragmentNavigatedAsync(Action<FragmentNavigatedEventArgs> handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default);
+    Task<Subscription<HistoryUpdatedEventArgs>> OnHistoryUpdatedAsync(SubscriptionOptions? options = null, CancellationToken cancellationToken = default);
+    Task<Subscription<HistoryUpdatedEventArgs>> OnHistoryUpdatedAsync(Func<HistoryUpdatedEventArgs, Task> handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default);
+    Task<Subscription<HistoryUpdatedEventArgs>> OnHistoryUpdatedAsync(Action<HistoryUpdatedEventArgs> handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default);
+    Task<Subscription<LoadEventArgs>> OnLoadAsync(SubscriptionOptions? options = null, CancellationToken cancellationToken = default);
+    Task<Subscription<LoadEventArgs>> OnLoadAsync(Func<LoadEventArgs, Task> handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default);
+    Task<Subscription<LoadEventArgs>> OnLoadAsync(Action<LoadEventArgs> handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default);
+    Task<Subscription<NavigationAbortedEventArgs>> OnNavigationAbortedAsync(SubscriptionOptions? options = null, CancellationToken cancellationToken = default);
+    Task<Subscription<NavigationAbortedEventArgs>> OnNavigationAbortedAsync(Func<NavigationAbortedEventArgs, Task> handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default);
+    Task<Subscription<NavigationAbortedEventArgs>> OnNavigationAbortedAsync(Action<NavigationAbortedEventArgs> handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default);
+    Task<Subscription<NavigationCommittedEventArgs>> OnNavigationCommittedAsync(SubscriptionOptions? options = null, CancellationToken cancellationToken = default);
+    Task<Subscription<NavigationCommittedEventArgs>> OnNavigationCommittedAsync(Func<NavigationCommittedEventArgs, Task> handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default);
+    Task<Subscription<NavigationCommittedEventArgs>> OnNavigationCommittedAsync(Action<NavigationCommittedEventArgs> handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default);
+    Task<Subscription<NavigationFailedEventArgs>> OnNavigationFailedAsync(SubscriptionOptions? options = null, CancellationToken cancellationToken = default);
+    Task<Subscription<NavigationFailedEventArgs>> OnNavigationFailedAsync(Func<NavigationFailedEventArgs, Task> handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default);
+    Task<Subscription<NavigationFailedEventArgs>> OnNavigationFailedAsync(Action<NavigationFailedEventArgs> handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default);
+    Task<Subscription<NavigationStartedEventArgs>> OnNavigationStartedAsync(SubscriptionOptions? options = null, CancellationToken cancellationToken = default);
+    Task<Subscription<NavigationStartedEventArgs>> OnNavigationStartedAsync(Func<NavigationStartedEventArgs, Task> handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default);
+    Task<Subscription<NavigationStartedEventArgs>> OnNavigationStartedAsync(Action<NavigationStartedEventArgs> handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default);
+    Task<Subscription<UserPromptClosedEventArgs>> OnUserPromptClosedAsync(SubscriptionOptions? options = null, CancellationToken cancellationToken = default);
+    Task<Subscription<UserPromptClosedEventArgs>> OnUserPromptClosedAsync(Func<UserPromptClosedEventArgs, Task> handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default);
+    Task<Subscription<UserPromptClosedEventArgs>> OnUserPromptClosedAsync(Action<UserPromptClosedEventArgs> handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default);
+    Task<Subscription<UserPromptOpenedEventArgs>> OnUserPromptOpenedAsync(SubscriptionOptions? options = null, CancellationToken cancellationToken = default);
+    Task<Subscription<UserPromptOpenedEventArgs>> OnUserPromptOpenedAsync(Func<UserPromptOpenedEventArgs, Task> handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default);
+    Task<Subscription<UserPromptOpenedEventArgs>> OnUserPromptOpenedAsync(Action<UserPromptOpenedEventArgs> handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default);
Evidence
PR Compliance ID 4 requires public API/ABI changes to be backward-compatible. The diff removes
multiple Task<Subscription> method signatures from the public IBrowsingContextModule interface
and replaces them with new generic Task<Subscription<TEventArgs>> overloads, forcing downstream
breaking changes.

AGENTS.md
dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextModule.cs[32-59]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Public interfaces removed `Task<Subscription>` overloads and replaced them with `Task<Subscription<TEventArgs>>`, which breaks API/ABI compatibility.

## Issue Context
Compatibility rules require upgrades to be possible by changing only the package version. Introduce a transition path by keeping the prior signatures and forwarding to the new generic implementation.

## Fix Focus Areas
- dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextModule.cs[32-73]
- dotnet/src/webdriver/BiDi/Network/INetworkModule.cs[31-45]
- dotnet/src/webdriver/BiDi/Script/IScriptModule.cs[33-41]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

12. Linked CTS not disposed 🐞 Bug ☼ Reliability
Description
EventStream.GetAsyncEnumerator creates a linked CancellationTokenSource when both the stream token
and enumerator token are cancellable, but it never disposes that CTS. This leaks token
registrations/resources per enumeration.
Code

dotnet/src/webdriver/BiDi/EventStream.cs[R65-73]

+        var effectiveToken = (_cancellationToken.CanBeCanceled, cancellationToken.CanBeCanceled) switch
+        {
+            (true, true) => CancellationTokenSource.CreateLinkedTokenSource(_cancellationToken, cancellationToken).Token,
+            (true, false) => _cancellationToken,
+            (false, true) => cancellationToken,
+            _ => default
+        };
+
+        return ReadChannelAsync(_channel.Reader, effectiveToken);
Evidence
The code allocates a linked CTS and drops the reference immediately, so it cannot be disposed when
enumeration ends.

dotnet/src/webdriver/BiDi/EventStream.cs[63-74]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`GetAsyncEnumerator` uses `CancellationTokenSource.CreateLinkedTokenSource(...)` but doesn't dispose the created CTS, leaking registrations.

### Issue Context
This happens when both `_cancellationToken` and the passed `cancellationToken` are cancellable, and can accumulate if streams are enumerated multiple times.

### Fix Focus Areas
- dotnet/src/webdriver/BiDi/EventStream.cs[63-74]

### Suggested fix
Create the linked CTS inside the async iterator and dispose it in a `finally` block (or otherwise ensure disposal when the enumerator completes), e.g. pass a `CancellationTokenSource?` into `ReadChannelAsync` and dispose it at the end of the iterator.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


13. Descriptor metadata conflict unhandled 🐞 Bug ☼ Reliability
Description
EventDispatcher caches event slots only by descriptor name and does not validate that later
subscriptions for the same name have matching JsonTypeInfo/ArgsFactory metadata. If two descriptors
share the same Name but differ in metadata, events may be deserialized using the first-registered
metadata and then delivered to subscriptions expecting a different EventArgs type, causing delivery
failures and subscription completion.
Code

dotnet/src/webdriver/BiDi/EventDispatcher.cs[R214-224]

+        return _events.GetOrAdd(descriptor.Name, _ =>
+        {
+            if (descriptor.JsonTypeInfo is null || descriptor.ArgsFactory is null)
+            {
+                throw new InvalidOperationException($"Event '{descriptor.Name}' does not have registration metadata.");
+            }

-        GC.SuppressFinalize(this);
+            var bidi = _bidi;
+            var argsFactory = descriptor.ArgsFactory;
+            return new EventSlot(descriptor.JsonTypeInfo, ep => argsFactory(bidi, ep));
+        });
Evidence
Slots are keyed strictly by descriptor.Name and _events.GetOrAdd(...) ignores metadata for
subsequent descriptors with the same name. Since EventDescriptor<TEventArgs>.Create(...) is
public, callers can construct conflicting descriptors, and downstream delivery enforces exact
event-args type (raising when mismatched).

dotnet/src/webdriver/BiDi/EventDispatcher.cs[214-224]
dotnet/src/webdriver/BiDi/EventDescriptor.cs[51-57]
dotnet/src/webdriver/BiDi/Subscription.cs[59-69]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`EventDispatcher` caches event metadata by event name only and doesn't validate metadata consistency when the same name is subscribed with different descriptors.

### Issue Context
This can lead to deserializing event params using the wrong `JsonTypeInfo` and then delivering mismatched `EventArgs`, which trips type checks and completes subscriptions.

### Fix Focus Areas
- dotnet/src/webdriver/BiDi/EventDispatcher.cs[214-224]
- dotnet/src/webdriver/BiDi/EventDescriptor.cs[51-57]

### Suggested fix
After `GetOrAdd`, if the slot already exists (or even unconditionally), compare `descriptor.JsonTypeInfo` (and/or an identity for the args-factory/type) with the cached slot metadata; if they differ, throw an `ArgumentException/InvalidOperationException` explaining the event name is already registered with different metadata.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment thread dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextModule.cs Outdated
Comment thread dotnet/src/webdriver/BiDi/Subscription.cs Outdated
Comment thread dotnet/src/webdriver/BiDi/Subscription.cs Outdated
Comment thread dotnet/src/webdriver/BiDi/Broker.cs Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Refactors the .NET BiDi event subscription pipeline to support strongly-typed subscriptions and optional event streaming, replacing the prior centralized dispatcher approach with per-event subscription registries.

Changes:

  • Introduces Subscription<TEventArgs> backed by Channel<T> and implements IAsyncEnumerable<TEventArgs> for streaming consumption.
  • Replaces EventDispatcher with a per-event SubscriptionRegistry inside Broker, and propagates terminal exceptions to active subscriptions on shutdown.
  • Updates BiDi module and BrowsingContext event APIs to return Subscription<TEventArgs> and adds handlerless overloads.

Reviewed changes

Copilot reviewed 23 out of 23 changed files in this pull request and generated 19 comments.

Show a summary per file
File Description
dotnet/src/webdriver/BiDi/Subscription.cs Adds generic, channel-backed subscriptions with streaming support and sequential handler draining.
dotnet/src/webdriver/BiDi/Broker.cs Replaces dispatcher with per-event registries and direct delivery/completion to subscriptions.
dotnet/src/webdriver/BiDi/Module.cs Updates subscribe helper to return Subscription<TEventArgs> and accept ValueTask handlers.
dotnet/src/webdriver/BiDi/EventDispatcher.cs Removes the previous centralized queued dispatcher implementation.
dotnet/src/webdriver/BiDi/Speculation/SpeculationModule.cs Updates speculation event subscription APIs to typed subscriptions + handlerless overload.
dotnet/src/webdriver/BiDi/Speculation/ISpeculationModule.cs Updates public speculation interface signatures to typed subscriptions and adds handlerless overload.
dotnet/src/webdriver/BiDi/Script/ScriptModule.cs Updates script event subscription APIs to typed subscriptions + handlerless overload.
dotnet/src/webdriver/BiDi/Script/IScriptModule.cs Updates public script interface signatures to typed subscriptions and adds handlerless overloads.
dotnet/src/webdriver/BiDi/Network/NetworkModule.cs Updates network event subscription APIs to typed subscriptions + handlerless overloads.
dotnet/src/webdriver/BiDi/Network/INetworkModule.cs Updates public network interface signatures to typed subscriptions and adds handlerless overloads.
dotnet/src/webdriver/BiDi/Log/LogModule.cs Updates log event subscription APIs to typed subscriptions + handlerless overload.
dotnet/src/webdriver/BiDi/Log/ILogModule.cs Updates public log interface signatures to typed subscriptions and adds handlerless overload.
dotnet/src/webdriver/BiDi/Input/InputModule.cs Updates input event subscription APIs to typed subscriptions + handlerless overload.
dotnet/src/webdriver/BiDi/Input/IInputModule.cs Updates public input interface signatures to typed subscriptions and adds handlerless overload.
dotnet/src/webdriver/BiDi/BrowsingContext/BrowsingContextModule.cs Updates browsing-context module event subscription APIs to typed subscriptions + handlerless overloads.
dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextModule.cs Updates public browsing-context interface signatures to typed subscriptions and adds handlerless overloads.
dotnet/src/webdriver/BiDi/BrowsingContext/BrowsingContext.cs Updates BrowsingContext facade methods to use typed subscriptions and adds handlerless overloads.
dotnet/src/webdriver/BiDi/BrowsingContext/BrowsingContextNetworkModule.cs Updates context-scoped network event subscription wrappers to typed subscriptions + handlerless overloads.
dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextNetworkModule.cs Updates public context-scoped network interface signatures to typed subscriptions and adds handlerless overloads.
dotnet/src/webdriver/BiDi/BrowsingContext/BrowsingContextLogModule.cs Updates context-scoped log event subscription wrappers to typed subscriptions + handlerless overloads.
dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextLogModule.cs Updates public context-scoped log interface signatures to typed subscriptions and adds handlerless overloads.
dotnet/src/webdriver/BiDi/BrowsingContext/BrowsingContextInputModule.cs Updates context-scoped input event subscription wrappers to typed subscriptions + handlerless overloads.
dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextInputModule.cs Updates public context-scoped input interface signatures to typed subscriptions and adds handlerless overloads.

Comment thread dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextInputModule.cs Outdated
Comment thread dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextModule.cs Outdated
Comment thread dotnet/src/webdriver/BiDi/Log/LogModule.cs Outdated
Comment thread dotnet/src/webdriver/BiDi/Speculation/SpeculationModule.cs Outdated
Comment thread dotnet/src/webdriver/BiDi/BrowsingContext/BrowsingContextModule.cs Outdated
Comment thread dotnet/src/webdriver/BiDi/Broker.cs Outdated
Comment thread dotnet/src/webdriver/BiDi/Broker.cs Outdated
Comment thread dotnet/src/webdriver/BiDi/Subscription.cs Outdated
Comment thread dotnet/src/webdriver/BiDi/Network/INetworkModule.cs Outdated
Comment thread dotnet/src/webdriver/BiDi/Log/ILogModule.cs Outdated
@nvborisenko

Copy link
Copy Markdown
Member Author

Need more design decisions..

@nvborisenko nvborisenko marked this pull request as draft April 15, 2026 19:54
@nvborisenko

Copy link
Copy Markdown
Member Author

Perfect, I understood how to design events.

Before:

// Push
await using var sub = await network.OnBeforeRequestSentAsync(e => Console.WriteLine(e));

After:

// Push
await using var sub = await network.BeforeRequestSent.OnAsync(e => Console.WriteLine(e));

// Pull
await using var sub = await network.BeforeRequestSent.SubscribeAsync();
await foreach (var args in sub) { ... }

2 separate clean concepts. Why this way...

  • resolves method overloading problem, when one method behaves differently based on parameters
  • property is an accessor, opening a door for new possibilities

Thus example of usage becomes very simple (purely native C#):

await using var sub = await network.BeforeRequestSent.SubscribeAsync();
await context.NavigateAsync("example.com");
await sub.Where(req => req.Url.Contains("/api")).FirstAsync();

Or if somebody wants to develop extension method:

var request = await network.BeforeRequestSent
    .During(() => context.NavigateAsync("example.com"))
    .Where(req => req.Url.Contains("/api")).FirstAsync()

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copilot AI review requested due to automatic review settings May 6, 2026 16:49
@nvborisenko nvborisenko marked this pull request as draft May 6, 2026 19:21
@nvborisenko nvborisenko marked this pull request as ready for review May 8, 2026 09:57
@qodo-code-review

qodo-code-review Bot commented May 8, 2026

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 18078e5

Comment thread dotnet/src/webdriver/BiDi/Broker.cs
Comment thread dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextModule.cs
Comment thread dotnet/src/webdriver/BiDi/BrowsingContext/BrowsingContext.cs
@nvborisenko

Copy link
Copy Markdown
Member Author

Checkpoint

await using var _ = await bidi.SubscribeAsync([NetworkEvent.BeforeRequestSent], args => { });
await using var stream = await bidi.StreamAsync([NetworkEvent.BeforeRequestSent]); // or single

Shortcuts

await using var _ = await bidi.Network.BeforeRequestSent.SubscribeAsync(args => { });
// the same for streaming

Key points

Dispatching is ordered, always, per subscription/stream.
Error handling - bad handler breaks its dispatching (we are logging it). Would be nice to "configure" it? Probably, user is still able to "try/catch".
Removed all extensions/helpers. Keep it minimal but powerful for further extensibility. This is another topic. Backpressure is also another topic. Let's focus on public API.

@diemol

diemol commented May 10, 2026

Copy link
Copy Markdown
Member

@nvborisenko, the way I understand this PR is that this touches only the low-level BiDi API, also making it more organized and easier to extend. When you merge it, please add to the commit the link to the blog post: https://www.selenium.dev/blog/2026/dotnet-strong-name-signing/

@nvborisenko

Copy link
Copy Markdown
Member Author

@diemol the blog post is about #17397 change.

@qodo-code-review

Copy link
Copy Markdown
Contributor

Code Review by Qodo

Grey Divider

Looking for bugs?

Check back in a few minutes. An AI review agent is analyzing this pull request.

Grey Divider

Qodo Logo

@nvborisenko nvborisenko merged commit f342fee into SeleniumHQ:trunk May 11, 2026
20 checks passed
@nvborisenko nvborisenko deleted the bidi-event-stream branch May 11, 2026 17:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

C-dotnet .NET Bindings

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants