Skip to content

[dotnet] [bidi] Use System.Threading.Channels dependency for events dispatching#17004

Merged
nvborisenko merged 7 commits intoSeleniumHQ:trunkfrom
nvborisenko:bidi-channels
Jan 25, 2026
Merged

[dotnet] [bidi] Use System.Threading.Channels dependency for events dispatching#17004
nvborisenko merged 7 commits intoSeleniumHQ:trunkfrom
nvborisenko:bidi-channels

Conversation

@nvborisenko
Copy link
Member

@nvborisenko nvborisenko commented Jan 25, 2026

User description

This pull request updates the event processing mechanism in the BiDi Broker class to use System.Threading.Channels instead of BlockingCollection, improving async event handling.

🔧 Implementation Notes

It also adds System.Threading.Channels as a dependency across the build system, project files, and NuGet specifications for all relevant target frameworks.

🔄 Types of changes

  • Cleanup (formatting, renaming)

PR Type

Enhancement


Description

  • Replace BlockingCollection with System.Threading.Channels for async event dispatching

  • Implement unbounded channel with single reader/writer optimization

  • Add System.Threading.Channels dependency across build system

  • Update NuGet specifications and project files for all frameworks


Diagram Walkthrough

flowchart LR
  BC["BlockingCollection<br/>GetConsumingEnumerable"]
  CH["System.Threading.Channels<br/>Reader/Writer pattern"]
  ASYNC["Improved async<br/>event handling"]
  BC -- "replaced with" --> CH
  CH -- "enables" --> ASYNC
Loading

File Walkthrough

Relevant files
Enhancement
Broker.cs
Migrate event queue to System.Threading.Channels                 

dotnet/src/webdriver/BiDi/Broker.cs

  • Replace BlockingCollection<(string Method, EventArgs Params)> with
    Channel<(string Method, EventArgs Params)> using unbounded channel
    with single reader/writer options
  • Refactor ProcessEventsAwaiterAsync() to use channel reader with
    WaitToReadAsync() and TryRead() instead of GetConsumingEnumerable()
  • Update event writing from _pendingEvents.Add() to
    _pendingEvents.Writer.TryWrite()
  • Update disposal from _pendingEvents.CompleteAdding() to
    _pendingEvents.Writer.Complete()
+21/-16 
Configuration changes
BUILD.bazel
Add System.Threading.Channels to Bazel dependencies           

dotnet/src/webdriver/BUILD.bazel

  • Add System.Threading.Channels NuGet package dependency to
    webdriver-net462 target
  • Add System.Threading.Channels NuGet package dependency to
    webdriver-netstandard2.0 target
  • Add System.Threading.Channels NuGet package dependency to
    webdriver-net462-strongnamed target
  • Add System.Threading.Channels NuGet package dependency to
    webdriver-netstandard2.0-strongnamed target
+6/-4     
Selenium.WebDriver.nuspec
Add System.Threading.Channels to NuSpec dependencies         

dotnet/src/webdriver/Selenium.WebDriver.nuspec

  • Add System.Threading.Channels version 8.0.0 dependency for net462
    target framework
  • Add System.Threading.Channels version 8.0.0 dependency for
    netstandard2.0 target framework
+2/-0     
Selenium.WebDriver.csproj
Add System.Threading.Channels to project file                       

dotnet/src/webdriver/Selenium.WebDriver.csproj

  • Add System.Threading.Channels version 8.0.0 PackageReference for
    net462 and netstandard2.0 target frameworks
+1/-0     
Selenium.WebDriver.StrongNamed.nuspec
Add System.Threading.Channels to StrongNamed NuSpec           

dotnet/src/webdriver/Selenium.WebDriver.StrongNamed.nuspec

  • Add System.Threading.Channels version 8.0.0 dependency for net462
    target framework
  • Add System.Threading.Channels version 8.0.0 dependency for
    netstandard2.0 target framework
+2/-0     

@selenium-ci selenium-ci added C-dotnet .NET Bindings B-build Includes scripting, bazel and CI integrations labels Jan 25, 2026
@qodo-code-review
Copy link
Contributor

qodo-code-review bot commented Jan 25, 2026

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
Denial of service

Description: An unbounded Channel is used for _pendingEvents and writes ignore the TryWrite return
value, which can allow uncontrolled queue growth (memory exhaustion) or silent event
dropping under stress, creating a realistic denial-of-service vector if a remote endpoint
can generate events faster than they are processed.
Broker.cs [40-295]

Referred Code
private readonly Channel<(string Method, EventArgs Params)> _pendingEvents = Channel.CreateUnbounded<(string Method, EventArgs Params)>(new(){ SingleReader = true, SingleWriter = true });
private readonly Dictionary<string, JsonTypeInfo> _eventTypesMap = [];

private readonly ConcurrentDictionary<string, List<EventHandler>> _eventHandlers = new();

private long _currentCommandId;

private static readonly TaskFactory _myTaskFactory = new(CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskContinuationOptions.None, TaskScheduler.Default);

private Task? _receivingMessageTask;
private Task? _eventEmitterTask;
private CancellationTokenSource? _receiveMessagesCancellationTokenSource;

internal Broker(BiDi bidi, Uri url)
{
    _bidi = bidi;
    _transport = new WebSocketTransport(url);
}

public async Task ConnectAsync(CancellationToken cancellationToken)
{


 ... (clipped 235 lines)
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: 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: 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

🔴
Generic: Robust Error Handling and Edge Case Management

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

Status:
Dropped event on write: The result of _pendingEvents.Writer.TryWrite(messageEvent) is ignored, so a failed write
can silently drop events without fallback handling or logging.

Referred Code
    var messageEvent = (method, eventArgs);
    _pendingEvents.Writer.TryWrite(messageEvent);
}

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:
Exception details logged: The error log interpolates the full exception object ({ex}), which may include stack
traces and sensitive runtime data and is not structured for safe auditing.

Referred Code
catch (Exception ex)
{
    if (_logger.IsEnabled(LogEventLevel.Error))
    {
        _logger.Error($"Unhandled error processing BiDi event handler: {ex}");
    }

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 25, 2026

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Handle failed event write on disposal

Check the return value of _pendingEvents.Writer.TryWrite and throw an
ObjectDisposedException if it returns false to avoid silently dropping events
when the broker is being disposed.

dotnet/src/webdriver/BiDi/Broker.cs [294]

-_pendingEvents.Writer.TryWrite(messageEvent);
+if (!_pendingEvents.Writer.TryWrite(messageEvent))
+{
+    throw new ObjectDisposedException(nameof(Broker), "Cannot process event as the broker is being disposed.");
+}
  • Apply / Chat
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies that TryWrite can fail silently and proposes throwing an exception, which aligns with the behavior of the previous BlockingCollection implementation, thus improving robustness and preventing silent data loss during shutdown.

Medium
Learned
best practice
Make disposal idempotent

Guard DisposeAsync() so it can be safely called multiple times (and from
multiple threads) without Complete() throwing or disposing resources twice.

dotnet/src/webdriver/BiDi/Broker.cs [183-197]

+private int _disposed;
+
 public async ValueTask DisposeAsync()
 {
-    _pendingEvents.Writer.Complete();
+    if (Interlocked.Exchange(ref _disposed, 1) == 1)
+    {
+        return;
+    }
+
+    _pendingEvents.Writer.TryComplete();
 
     _receiveMessagesCancellationTokenSource?.Cancel();
 
     if (_eventEmitterTask is not null)
     {
         await _eventEmitterTask.ConfigureAwait(false);
     }
 
     _transport.Dispose();
 
     GC.SuppressFinalize(this);
 }
  • Apply / Chat
Suggestion importance[1-10]: 6

__

Why:
Relevant best practice - Make lifecycle/disposal code idempotent and concurrency-safe (avoid throwing or double-dispose side effects).

Low
General
Use explicit channel options
Suggestion Impact:The channel options initializer was reformatted into a multi-line block for readability, aligning with the suggestion’s intent, but it still uses `new()` instead of explicitly naming `UnboundedChannelOptions`.

code diff:

-    private readonly Channel<(string Method, EventArgs Params)> _pendingEvents = Channel.CreateUnbounded<(string Method, EventArgs Params)>(new(){ SingleReader = true, SingleWriter = true });
+    private readonly Channel<(string Method, EventArgs Params)> _pendingEvents = Channel.CreateUnbounded<(string Method, EventArgs Params)>(new()
+    {
+        SingleReader = true,
+        SingleWriter = true
+    });

Improve readability by using an explicit UnboundedChannelOptions object when
creating the Channel.

dotnet/src/webdriver/BiDi/Broker.cs [40]

-private readonly Channel<(string Method, EventArgs Params)> _pendingEvents = Channel.CreateUnbounded<(string Method, EventArgs Params)>(new(){ SingleReader = true, SingleWriter = true });
+private readonly Channel<(string Method, EventArgs Params)> _pendingEvents =
+    Channel.CreateUnbounded<(string Method, EventArgs Params)>(new UnboundedChannelOptions
+    {
+        SingleReader = true,
+        SingleWriter = true
+    });

[Suggestion processed]

Suggestion importance[1-10]: 3

__

Why: This is a valid code style suggestion that improves readability by explicitly naming UnboundedChannelOptions, making the configuration clearer at a glance.

Low
  • Update

@nvborisenko nvborisenko merged commit 3e4e22f into SeleniumHQ:trunk Jan 25, 2026
17 checks passed
@nvborisenko nvborisenko deleted the bidi-channels branch January 25, 2026 18:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

B-build Includes scripting, bazel and CI integrations C-dotnet .NET Bindings Review effort 3/5

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants