Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[dotnet] [bidi] Support cancellation of events registration#16996

Merged
nvborisenko merged 6 commits intoSeleniumHQ:trunkfrom
nvborisenko:bidi-events-reg-cancellation
Jan 24, 2026
Merged

[dotnet] [bidi] Support cancellation of events registration#16996
nvborisenko merged 6 commits intoSeleniumHQ:trunkfrom
nvborisenko:bidi-events-reg-cancellation

Conversation

@nvborisenko
Copy link
Member

@nvborisenko nvborisenko commented Jan 24, 2026

User description

This enhancement allows consumers of the API to cancel pending event subscriptions and unsubscriptions, improving responsiveness and resource management in scenarios where operations may need to be aborted.

🔗 Related Issues

Continuation of #16989

💥 What does this PR do?

  • Added a CancellationToken parameter to all event subscription methods in BrowsingContextModule, including navigation, history, download, context, and user prompt events, allowing callers to cancel subscriptions if needed.

🔄 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)

PR Type

Enhancement


Description

  • Add CancellationToken parameter to all BiDi event subscription methods

  • Enable cancellation of pending event subscriptions across modules

  • Support cancellation in SubscribeAsync and UnsubscribeAsync operations

  • Propagate cancellation tokens through broker and module layers


Diagram Walkthrough

flowchart LR
  A["Event Subscription Methods"] -->|Add CancellationToken| B["Module Layer"]
  B -->|Pass Token| C["Broker.SubscribeAsync"]
  C -->|Forward Token| D["SessionModule.SubscribeAsync"]
  E["Subscription.UnsubscribeAsync"] -->|Add CancellationToken| F["Broker.UnsubscribeAsync"]
  F -->|Forward Token| G["SessionModule.UnsubscribeAsync"]
Loading

File Walkthrough

Relevant files
Enhancement
11 files
Broker.cs
Add cancellation token to subscribe and unsubscribe methods
+4/-4     
Module.cs
Propagate cancellation token through subscription methods
+4/-4     
Subscription.cs
Add cancellation token to UnsubscribeAsync method               
+3/-2     
BrowsingContextModule.cs
Add cancellation tokens to all navigation and context events
+56/-56 
BrowsingContextInputModule.cs
Add cancellation token to file dialog event subscriptions
+6/-4     
BrowsingContextLogModule.cs
Add cancellation token to log entry event subscriptions   
+7/-4     
BrowsingContextNetworkModule.cs
Add cancellation tokens to network event subscriptions     
+30/-20 
NetworkModule.cs
Add cancellation tokens to network event subscription methods
+20/-20 
InputModule.cs
Add cancellation token to file dialog event subscriptions
+4/-4     
LogModule.cs
Add cancellation token to log entry event subscriptions   
+5/-4     
ScriptModule.cs
Add cancellation tokens to script event subscription methods
+12/-12 

@selenium-ci selenium-ci added the C-dotnet .NET Bindings label Jan 24, 2026
@qodo-code-review
Copy link
Contributor

qodo-code-review bot commented Jan 24, 2026

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
🟢
No security concerns identified No security vulnerabilities detected by AI analysis. Human verification advised for critical code.
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

  • Update
Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@qodo-code-review
Copy link
Contributor

qodo-code-review bot commented Jan 24, 2026

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
High-level
Consider simplifying the duplicated subscription logic

Refactor the duplicated event subscription logic by having the Action overload
call the Func<T, Task> overload. This will remove boilerplate code across
multiple modules and improve maintainability.

Examples:

dotnet/src/webdriver/BiDi/BrowsingContext/BrowsingContextModule.cs [117-255]
    public async Task<Subscription> OnNavigationStartedAsync(Func<NavigationInfo, Task> handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default)
    {
        return await SubscribeAsync("browsingContext.navigationStarted", handler, options, _jsonContext.NavigationInfo, cancellationToken).ConfigureAwait(false);
    }

    public async Task<Subscription> OnNavigationStartedAsync(Action<NavigationInfo> handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default)
    {
        return await SubscribeAsync("browsingContext.navigationStarted", handler, options, _jsonContext.NavigationInfo, cancellationToken).ConfigureAwait(false);
    }


 ... (clipped 129 lines)
dotnet/src/webdriver/BiDi/Network/NetworkModule.cs [128-176]
    public async Task<Subscription> OnBeforeRequestSentAsync(Func<BeforeRequestSentEventArgs, Task> handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default)
    {
        return await SubscribeAsync("network.beforeRequestSent", handler, options, _jsonContext.BeforeRequestSentEventArgs, cancellationToken).ConfigureAwait(false);
    }

    public async Task<Subscription> OnBeforeRequestSentAsync(Action<BeforeRequestSentEventArgs> handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default)
    {
        return await SubscribeAsync("network.beforeRequestSent", handler, options, _jsonContext.BeforeRequestSentEventArgs, cancellationToken).ConfigureAwait(false);
    }


 ... (clipped 39 lines)

Solution Walkthrough:

Before:

// In a module like `BrowsingContextModule`
public async Task<Subscription> OnNavigationStartedAsync(Func<NavigationInfo, Task> handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default)
{
    return await SubscribeAsync("browsingContext.navigationStarted", handler, options, _jsonContext.NavigationInfo, cancellationToken).ConfigureAwait(false);
}

public async Task<Subscription> OnNavigationStartedAsync(Action<NavigationInfo> handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default)
{
    return await SubscribeAsync("browsingContext.navigationStarted", handler, options, _jsonContext.NavigationInfo, cancellationToken).ConfigureAwait(false);
}

// ... this pattern is repeated for many other events

After:

// In a module like `BrowsingContextModule`
public async Task<Subscription> OnNavigationStartedAsync(Func<NavigationInfo, Task> handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default)
{
    return await SubscribeAsync("browsingContext.navigationStarted", handler, options, _jsonContext.NavigationInfo, cancellationToken).ConfigureAwait(false);
}

public async Task<Subscription> OnNavigationStartedAsync(Action<NavigationInfo> handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default)
{
    // Call the Func overload, wrapping the Action.
    return await OnNavigationStartedAsync(e => { handler(e); return Task.CompletedTask; }, options, cancellationToken).ConfigureAwait(false);
}

// ... this pattern is repeated for many other events
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies significant code duplication across multiple modules where Action and Func overloads for event subscriptions are nearly identical, and a refactoring would greatly improve code maintainability.

Medium
General
Pass cancellation token to subscription

Pass the cancellationToken to the Subscription constructor to enable cancellable
disposal of subscriptions.

dotnet/src/webdriver/BiDi/Broker.cs [155-167]

 public async Task<Subscription> SubscribeAsync<TEventArgs>(string eventName, EventHandler eventHandler, SubscriptionOptions? options, JsonTypeInfo<TEventArgs> jsonTypeInfo, CancellationToken cancellationToken)
     where TEventArgs : EventArgs
 {
     _eventTypesMap[eventName] = jsonTypeInfo;
 
     var handlers = _eventHandlers.GetOrAdd(eventName, (a) => []);
 
     var subscribeResult = await _bidi.SessionModule.SubscribeAsync([eventName], new() { Contexts = options?.Contexts, UserContexts = options?.UserContexts }, cancellationToken).ConfigureAwait(false);
 
     handlers.Add(eventHandler);
 
-    return new Subscription(subscribeResult.Subscription, this, eventHandler);
+    return new Subscription(subscribeResult.Subscription, this, eventHandler, cancellationToken);
 }
  • Apply / Chat
Suggestion importance[1-10]: 7

__

Why: This suggestion correctly identifies a way to improve the cancellation logic by passing the cancellationToken to the Subscription constructor, which enables its use during disposal. This enhances the robustness of the cancellation feature introduced in the PR.

Medium
Possible issue
Postpone state updates until after subscribe

Reorder operations in SubscribeAsync to update internal state like
_eventTypesMap and _eventHandlers only after the subscription call successfully
completes, preventing state inconsistency on failure or cancellation.

dotnet/src/webdriver/BiDi/Broker.cs [158-164]

-_eventTypesMap[eventName] = jsonTypeInfo;
-var handlers = _eventHandlers.GetOrAdd(eventName, (a) => []);
 var subscribeResult = await _bidi.SessionModule.SubscribeAsync(
     [eventName],
     new() { Contexts = options?.Contexts, UserContexts = options?.UserContexts },
     cancellationToken
 ).ConfigureAwait(false);
+_eventTypesMap[eventName] = jsonTypeInfo;
+var handlers = _eventHandlers.GetOrAdd(eventName, _ => new List<EventHandler>());
 handlers.Add(eventHandler);
  • Apply / Chat
Suggestion importance[1-10]: 6

__

Why: The suggestion correctly points out a potential state inconsistency issue if the SubscribeAsync call fails or is cancelled. Reordering the operations to update internal state only after the remote call succeeds makes the method more robust and atomic.

Low
Swap unsubscribe order for consistency

In UnsubscribeAsync, call the remote unsubscribe method before removing the
local event handler to ensure state consistency in case of cancellation or
errors.

dotnet/src/webdriver/BiDi/Broker.cs [171-175]

-var eventHandlers = _eventHandlers[subscription.EventHandler.EventName];
-eventHandlers.Remove(subscription.EventHandler);
 await _bidi.SessionModule.UnsubscribeAsync(
     [subscription.SubscriptionId],
     null,
     cancellationToken
 ).ConfigureAwait(false);
+var eventHandlers = _eventHandlers[subscription.EventHandler.EventName];
+eventHandlers.Remove(subscription.EventHandler);

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 6

__

Why: This suggestion correctly identifies a potential state inconsistency issue. By performing the remote UnsubscribeAsync call before removing the local event handler, it ensures the local state is only updated upon successful unsubscription, making the operation more robust.

Low
Learned
best practice
Make unsubscribe/dispose idempotent

Track an _unsubscribed flag (via Interlocked.Exchange) so
UnsubscribeAsync/DisposeAsync are idempotent and safe if called multiple times
or concurrently.

dotnet/src/webdriver/BiDi/Subscription.cs [27-51]

 public class Subscription : IAsyncDisposable
 {
     private readonly Broker _broker;
+    private int _unsubscribed;
 ...
     public async Task UnsubscribeAsync(CancellationToken cancellationToken = default)
     {
+        if (Interlocked.Exchange(ref _unsubscribed, 1) != 0)
+        {
+            return;
+        }
+
         await _broker.UnsubscribeAsync(this, cancellationToken).ConfigureAwait(false);
     }
 
     public async ValueTask DisposeAsync()
     {
         await UnsubscribeAsync().ConfigureAwait(false);
     }
 }

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 6

__

Why:
Relevant best practice - Make lifecycle/concurrency-sensitive disposal robust and idempotent to prevent double-unsubscribe and races.

Low
Validate inputs and cancellation early

Validate eventName (null/whitespace) and call
cancellationToken.ThrowIfCancellationRequested() before mutating
_eventTypesMap/_eventHandlers so canceled calls don’t leave partial state.

dotnet/src/webdriver/BiDi/Broker.cs [155-167]

 public async Task<Subscription> SubscribeAsync<TEventArgs>(string eventName, EventHandler eventHandler, SubscriptionOptions? options, JsonTypeInfo<TEventArgs> jsonTypeInfo, CancellationToken cancellationToken)
     where TEventArgs : EventArgs
 {
+    if (string.IsNullOrWhiteSpace(eventName))
+    {
+        throw new ArgumentException("Event name must be provided.", nameof(eventName));
+    }
+
+    cancellationToken.ThrowIfCancellationRequested();
+
     _eventTypesMap[eventName] = jsonTypeInfo;
 
-    var handlers = _eventHandlers.GetOrAdd(eventName, (a) => []);
+    var handlers = _eventHandlers.GetOrAdd(eventName, _ => []);
 
-    var subscribeResult = await _bidi.SessionModule.SubscribeAsync([eventName], new() { Contexts = options?.Contexts, UserContexts = options?.UserContexts }, cancellationToken).ConfigureAwait(false);
+    var subscribeResult = await _bidi.SessionModule
+        .SubscribeAsync([eventName], new() { Contexts = options?.Contexts, UserContexts = options?.UserContexts }, cancellationToken)
+        .ConfigureAwait(false);
 
     handlers.Add(eventHandler);
 
     return new Subscription(subscribeResult.Subscription, this, eventHandler);
 }
  • Apply / Chat
Suggestion importance[1-10]: 5

__

Why:
Relevant best practice - Add explicit validation and cancellation guards at integration boundaries before mutating internal state or making remote calls.

Low
  • Update

@nvborisenko nvborisenko merged commit 29cb60f into SeleniumHQ:trunk Jan 24, 2026
11 checks passed
@nvborisenko nvborisenko deleted the bidi-events-reg-cancellation branch January 24, 2026 13:05
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.

2 participants