Architecture Overview
This document provides an architectural overview of WebDriverBiDi.NET, explaining how the library is organized and how data flows through the system.
High-Level Architecture
┌─────────────────────────────────────────────────────┐
│ Your .NET Application │
└────────────────┬────────────────────────────────────┘
│
│ Uses
▼
┌─────────────────────────────────────────────────────┐
│ BiDiDriver │
│ ┌─────────────────────────────────────────────┐ │
│ │ Module Layer │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ Browser │ │ Browsing │ │ Script │ │ │
│ │ │ Module │ │ Context │ │ Module │ │ │
│ │ └──────────┘ │ Module │ └──────────┘ │ │
│ │ └──────────┘ │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ Network │ │ Input │ │ Log │ │ │
│ │ │ Module │ │ Module │ │ Module │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ │ │
│ └─────────────────────────────────────────────┘ │
│ │ │
│ ┌─────────────────────────────────────────────┐ │
│ │ Protocol Layer │ │
│ │ ┌──────────┐ │ │
│ │ │Transport │ │ │
│ │ └────┬─────┘ │ │
│ │ │ │ │
│ │ ┌────▼─────────┐ │ │
│ │ │ Connection │ (Abstract) │ │
│ │ └────┬─────────┘ │ │
│ │ │ │ │
│ │ ┌───────┴────────┐ │ │
│ │ ▼ ▼ │ │
│ │ ┌────────────┐ ┌──────────┐ │ │
│ │ │ WebSocket │ │ Pipes │ │ │
│ │ │ Connection │ │Connection│ │ │
│ │ └─────┬──────┘ └────┬─────┘ │ │
│ └────────┼──────────────┼─────────────────────┘ │
└───────────┼──────────────┼──────────────────────────┘
│ │
▼ ▼
WebSocket Pipes
(JSON messages) (Null-terminated)
│ │
▼ ▼
┌─────────────────────────────────────────────────────┐
│ Browser (Remote End) │
│ Chrome / Edge / Firefox / etc. │
└─────────────────────────────────────────────────────┘
Core Components
BiDiDriver
The BiDiDriver class is the facade that provides access to all functionality.
Responsibilities:
- Manages the WebSocket connection lifecycle
- Provides access to all modules
- Coordinates command execution
- Dispatches events to observers
- Exposes driver-level events
Key Methods:
StartAsync(url): Establishes WebSocket connectionStopAsync(): Closes connectionExecuteCommandAsync<T>(command): Sends commands and waits for responsesRegisterModule(module): Registers custom modules
Transport Layer
The Transport class handles low-level communication with the browser through an abstract Connection.
Responsibilities:
- Manages connection lifecycle through the Connection abstraction
- Serializes commands to JSON
- Deserializes responses and events from JSON
- Correlates responses with sent commands
- Routes events to appropriate handlers
- Supports multiple transport types (WebSocket, Pipes)
Message Flow:
Commands (Your Code → Browser):
┌──────────────┐ Serialize ┌───────────┐ WebSocket ┌─────────┐
│ Command │────────────────▶│ JSON │──────────────▶│ Browser │
│ Parameters │ │ Message │ └─────────┘
└──────────────┘ └───────────┘
Responses (Browser → Your Code):
┌─────────┐ WebSocket ┌────────────┐ Deserialize ┌──────────────┐
│ Browser │──────────────▶│ JSON │─────────────────▶│ Command │
└─────────┘ │ Message │ │ Result │
└────────────┘ └──────────────┘
Events (Browser → Observers):
┌─────────┐ WebSocket ┌───────────┐ Deserialize ┌──────────────┐
│ Browser │──────────────▶│ JSON │─────────────────▶│ Event Args │
└─────────┘ │ Message │ └──────┬───────┘
└───────────┘ │
▼
┌────────────────────┐
│ Event Observers │
└────────────────────┘
Message Queue Architecture:
The Transport uses an unbounded Channel<IncomingMessage> to buffer incoming messages:
- Design: Single-reader, single-writer unbounded channel
- Purpose: Decouple connection I/O from message processing
- Normal Behavior: Queue stays near-empty as processing is typically faster than message arrival
- High-Throughput: Queue can grow if messages arrive faster than processing (see Performance Considerations)
Connection → [Unbounded Queue] → Message Processor → Event Dispatch
↓
No size limit
Fast enqueue
Sequential processing
Key Characteristics:
- No backpressure mechanism (unbounded)
- Optimal for typical usage patterns
- Requires attention in high-event scenarios (>1000 events/second)
- See XML documentation on
Transportclass for mitigation strategies
Module Layer
Each module encapsulates a specific area of WebDriver BiDi functionality.
Module Structure:
See BrowsingContextModule in the WebDriverBiDi.BrowsingContext namespace. Each module has a constructor taking IBiDiModuleHost, command methods returning Task<CommandResult>, and observable events of type ObservableEvent<TEventArgs>.
All Modules:
BrowserModule: Browser windows and user contextsBrowsingContextModule: Tab/window/iframe managementScriptModule: JavaScript executionNetworkModule: Network traffic controlInputModule: User input simulationLogModule: Console logsSessionModule: Session and subscription managementStorageModule: Cookies and storageEmulationModule: Device emulationPermissionsModule: Permission controlBluetoothModule: Web Bluetooth APIDigitalCredentialsModule: Virtual digital wallet simulationWebExtensionModule: Extension managementSpeculationModule: Navigation prefetchingUserAgentClientHintsModule: User agent client hints emulation
Connection Types
WebDriverBiDi.NET uses an abstract Connection class to support multiple transport layers, allowing communication with browsers through different mechanisms.
Connection Architecture
The Connection abstract class defines the contract for all transport implementations. See the Connection class in the WebDriverBiDi.Protocol namespace for the full API, including IsActive, ConnectionKind, StartAsync, StopAsync, SendDataAsync, observable events (OnDataReceived, which transfers buffer ownership and so admits only a single observer — the Transport; OnConnectionError; OnRemoteDisconnected; and OnLogMessage), and configurable timeouts (StartupTimeout, ShutdownTimeout, DataTimeout).
WebSocket Connection
When to Use:
- Development and debugging scenarios
- Remote browser control
- Multiple clients connecting to the same browser
- Browser launched separately from your application
- Cross-machine communication
Implementation Details:
- Uses
System.Net.WebSockets.ClientWebSocket - Validates URL scheme (must be
ws://orwss://) - Supports secure WebSocket connections
- Socket options (request headers, proxy, keep-alive interval, certificate validation) are configurable by overriding
CreateClientWebSocket; see Connection Management - Text-based JSON message protocol
- Handles multi-frame WebSocket messages
Example:
// Connect to browser at WebSocket URL
BiDiDriver driver = new BiDiDriver(TimeSpan.FromSeconds(30));
await driver.StartAsync("ws://localhost:9515/session/abc-123");
try
{
// Use the driver
await driver.BrowsingContext.NavigateAsync(navParams);
}
finally
{
await driver.StopAsync();
}
Browser Launch:
chromedriver --port=9515
A classic new-session request to the driver with the webSocketUrl: true capability returns the WebDriver BiDi WebSocket URL. (Chrome's own --remote-debugging-port endpoint speaks CDP, not BiDi; see Browser Setup.) Firefox launched with --remote-debugging-port serves BiDi directly at /session.
Pipe Connection
When to Use:
- Automation frameworks controlling browser lifecycle
- Programmatic browser management
- Single-client scenarios
- Enhanced process isolation
- Lower latency requirements
Implementation Details:
- Uses anonymous pipes (
AnonymousPipeServerStream) - Protocol: Null-terminated JSON messages
- Windows: Anonymous pipes via
CreatePipeAPI - Unix/Linux/macOS: File descriptors 3 (browser reads) and 4 (browser writes)
- Requires
IPipeServerProcessProviderfor process management - Direct process-to-process communication
Example:
Note: The
WebDriverBiDipackage does not ship a browser launcher. The sample usesMyChromiumPipeLauncher, a launcher of your own that implementsIPipeServerProcessProviderto launch the browser and build aTransportover aPipeConnection(see Browser Setup):
// Your own IPipeServerProcessProvider, which launches Chromium with --remote-debugging-pipe and whose
// CreateTransport() installs a BiDi-over-CDP mapper (see Browser Setup).
MyChromiumPipeLauncher launcher = new MyChromiumPipeLauncher();
await launcher.StartAsync();
await launcher.LaunchBrowserAsync();
try
{
// Create driver with launcher's connection
BiDiDriver driver = new BiDiDriver(
TimeSpan.FromSeconds(30),
launcher.CreateTransport());
await driver.StartAsync("pipes");
// The mapper does not create a session; do it here
await driver.Session.NewSessionAsync(new NewCommandParameters());
// Use the driver
await driver.BrowsingContext.NavigateAsync(navParams);
await driver.StopAsync();
}
finally
{
await launcher.QuitBrowserAsync();
await launcher.StopAsync();
}
Browser Launch:
chrome --remote-debugging-pipe
The --remote-debugging-pipe flag instructs the browser to communicate over a pair of inherited anonymous pipes instead of opening a TCP port. The browser reads from file descriptor 3 and writes to file descriptor 4; standard input and output are left alone.
IPipeServerProcessProvider Interface
The IPipeServerProcessProvider interface enables dependency injection for pipe connections. See the interface in the WebDriverBiDi.Protocol namespace—it defines Process? PipeServerProcess { get; }. PipeConnection creates the two anonymous pipes itself and needs the provider to start the browser process with their handles inherited, and to report whether that process is still running. You must implement this interface to manage the browser process lifecycle and provide it to the connection.
ConnectionKind Enum
The ConnectionKind enum identifies which transport mechanism is being used (WebSocket or Pipes). See the enum in the WebDriverBiDi.Protocol namespace. Launchers use this to determine which browser flags to use (--remote-debugging-port vs --remote-debugging-pipe).
Choosing a Connection Type
| Factor | WebSocket | Pipes |
|---|---|---|
| Latency | Moderate (TCP overhead) | Lower (direct IPC) |
| Remote Access | ✓ Yes | ✗ No (same machine only) |
| Multi-Client | ✓ Yes | ✗ No (single client) |
| Process Coupling | Loose | Tight |
| Debugging | Easy (can inspect traffic) | Moderate |
| Platform Support | Universal | Universal |
| Setup Complexity | Simple | Moderate |
Recommendation:
- Use WebSocket for development, debugging, and flexible deployment scenarios
- Use Pipes for automation frameworks and when you control the browser lifecycle
Command Pattern
Commands follow a strict pattern for type safety and consistency.
Command Flow:
1. Create Parameters Object
↓
2. Pass to Module Method
↓
3. Module Creates Command
↓
4. Driver Executes via Transport
↓
5. Transport Sends WebSocket Message
↓
6. Browser Processes Command
↓
7. Browser Sends Response
↓
8. Transport Receives Message
↓
9. Transport Deserializes to Result
↓
10. Driver Returns Result to Module
↓
11. Module Returns to Your Code
Example:
// 1. Create parameters (mutable)
NavigateCommandParameters parameters = new NavigateCommandParameters(contextId, url);
parameters.Wait = ReadinessState.Complete;
// 2-11. Execute command (returns immutable result)
NavigateCommandResult result = await driver.BrowsingContext.NavigateAsync(parameters);
Event System
WebDriverBiDi.NET uses an observable event pattern for handling browser events.
Event Architecture:
Browser
│
│ Emits Event
▼
Transport
│
│ Deserializes
▼
ObservableEvent<TEventArgs>
│
│ Notifies
▼
┌─────────────────────────────┐
│ EventObserver<TEventArgs> │
│ EventObserver<TEventArgs> │
│ EventObserver<TEventArgs> │
└─────────────────────────────┘
│
│ Invokes
▼
Your Event Handlers
Observable Event: See ObservableEvent<T> in the WebDriverBiDi namespace—it provides AddObserver(Func<T, Task> handler, ObservableEventHandlerOptions handlerOptions = ObservableEventHandlerOptions.RunHandlerSynchronously, string description = "") and a protected NotifyObserversAsync(T notifyData) that only the producing side (ObservableEventInvocable<T>) can call.
Event Observer: See EventObserver<T> in the WebDriverBiDi namespace—it provides StartCapturingTasks, StopCapturingTasks, WaitForCapturedTasksAsync, WaitForCapturedTasksCompleteAsync, GetCapturedTasks, and Unobserve.
Data Flow Patterns
Command Execution
// Synchronous-looking code (with async/await)
NavigateCommandResult result = await driver.BrowsingContext.NavigateAsync(navParams);
// What actually happens:
// 1. NavigateAsync creates a Command object
// 2. Command is serialized to JSON
// 3. JSON sent via WebSocket
// 4. Method awaits response
// 5. Browser processes navigation
// 6. Browser sends response JSON
// 7. Transport deserializes to NavigateCommandResult
// 8. Awaited method returns result
Event Handling
// Setup (before events occur)
driver.Log.OnEntryAdded.AddObserver((e) =>
{
Console.WriteLine(e.Text);
});
await driver.Session.SubscribeAsync(subscribeParams);
// Runtime (when event occurs):
// 1. Browser emits log.entryAdded event
// 2. Transport receives JSON message
// 3. Transport deserializes to EntryAddedEventArgs
// 4. ObservableEvent.NotifyObserversAsync called
// 5. All registered observers invoked
// 6. Your handler executes
Bidirectional Communication
WebDriver BiDi is truly bidirectional:
Your Code ────Commands────▶ Browser
◀───Responses────
Your Code ◀────Events────── Browser
───Subscribe────▶
Serialization
WebDriverBiDi.NET uses System.Text.Json for JSON serialization.
Custom JSON Converters
The library includes specialized converters for WebDriver BiDi types:
CommandJsonConverter: Serializes command parametersSentinelNullJsonConverter<T, TSentinelChecker>: Serializes types where a specific, "sentinel" value yields anullvalue in the serialized JSON.SentinelNullJsonConverter<T, TSentinelChecker, TValueConverter>does the same for a property whose values need a converter of their own, writing every value that is not the sentinel throughTValueConverterDiscriminatedUnionJsonConverter<T>: Deserializes received types modelled as discriminated unions, such asEvaluateResult,RealmInfo,LogEntryandDownloadEndEventArgsBigIntegerJsonConverter: Deserializes BigInteger valuesNumberJsonConverter: Deserializes JavaScript numeric valuesRemoteValueDictionaryJsonConverter: Deserializes RemoteValues for types containing dictionary types (maps, objects, etc.)RemoteValueListJsonConverter: Deserializes RemoteValues for list types (arrays, sets, etc.)
Extension Data
Command results and event args expose extension properties from two distinct positions: those found inside the result/params object beside the specified members (AdditionalData) and those found on the message envelope beside id, type, result, method and params (AdditionalResponseProperties on results, AdditionalEventProperties on event args) — mirroring CommandParameters.AdditionalData and Command.AdditionalCommandProperties on the sending side. Both are captured by the transport for every result and event without any attribute on the type. Nested objects that the protocol marks Extensible additionally capture their own: RequestData and ResponseData (Chromium's goog:-prefixed request and response fields), Cookie, CapabilitiesResult (as AdditionalCapabilities) and the ProxyConfigurationResult it carries as Proxy, the storage partition types, and SharedReferenceInfo (the element of input.fileDialogOpened). See API Design Guide — Reading vendor extension data. This allows forward compatibility with new protocol versions.
Threading Model
WebDriverBiDi.NET is fully asynchronous and thread-safe for most operations.
Transport Reader Task
The transport maintains a dedicated reader task, running on the thread pool rather than on a thread of its own, that processes queued WebSocket messages one at a time:
- Receives messages from WebSocket
- Deserializes JSON to objects
- Dispatches events to observers
- Completes command Tasks when responses arrive
Your Thread(s)
Your application code runs on your own threads:
- Sends commands via async methods
- Awaits results (doesn't block threads)
- Receives event notifications on Transport thread or Task pool
Event Handler Execution
Event handlers have two modes:
Synchronous Mode (default):
driver.Log.OnEntryAdded.AddObserver((e) =>
{
// Runs on Transport thread
// Blocks other message processing until complete
Console.WriteLine(e.Text);
});
Asynchronous Mode:
driver.Log.OnEntryAdded.AddObserver(
async (e) =>
{
// The handler still starts on the transport's dispatch thread; only the
// continuation after the first incomplete await runs elsewhere, and the
// transport does not wait for it, so message processing is not blocked.
await ProcessLogEntryAsync(e);
},
ObservableEventHandlerOptions.RunHandlerAsynchronously
);
Error Handling Configuration
WebDriverBiDi.NET provides configurable error handling behavior at multiple levels of the system.
Transport Error Behavior
The Transport class can be configured with different error behaviors. See TransportErrorBehavior enum in the WebDriverBiDi.Protocol namespace: Ignore (default), Collect, and Terminate.
Terminate Mode
Throws an exception when the next command is sent after a transport error:
// Throws on next command call after error occurs
BiDiDriver driver = new BiDiDriver(TimeSpan.FromSeconds(30));
driver.TransportConfiguration.EventHandlerExceptionBehavior = TransportErrorBehavior.Terminate;
driver.TransportConfiguration.ProtocolErrorBehavior = TransportErrorBehavior.Terminate;
driver.TransportConfiguration.UnknownMessageBehavior = TransportErrorBehavior.Terminate;
driver.TransportConfiguration.UnexpectedErrorBehavior = TransportErrorBehavior.Terminate;
try
{
await driver.StartAsync("ws://localhost:9515/session/YOUR-SESSION-ID");
// Terminate mode surfaces an accumulated error on the next command, so it takes a command to
// observe one. This one throws if anything was accumulated while the session was running.
await driver.Session.StatusAsync();
}
catch (AggregateException ex)
{
// More than one error accumulated: each is an inner exception.
Console.WriteLine($"Errors: {string.Join(", ", ex.InnerExceptions.Select(inner => inner.Message))}");
}
catch (WebDriverBiDiException ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
Use When:
- You want fast failure on errors
- Errors indicate unrecoverable conditions
- You prefer explicit error handling
Collect Mode
Stores transport errors in a list for later inspection:
WebSocketConnection connection = new WebSocketConnection();
Transport transport = new Transport(connection)
{
EventHandlerExceptionBehavior = TransportErrorBehavior.Collect,
ProtocolErrorBehavior = TransportErrorBehavior.Collect,
UnknownMessageBehavior = TransportErrorBehavior.Collect,
UnexpectedErrorBehavior = TransportErrorBehavior.Collect,
};
BiDiDriver driver = new BiDiDriver(TimeSpan.FromSeconds(30), transport);
await driver.StartAsync("ws://localhost:9515/session/YOUR-SESSION-ID");
// Perform operations...
await driver.BrowsingContext.NavigateAsync(navParams);
try
{
await driver.StopAsync();
}
catch (AggregateException ex)
{
// Check for collected errors
if (ex.InnerExceptions.Count > 0)
{
Console.WriteLine($"Encountered {ex.InnerExceptions.Count} transport errors:");
foreach (Exception error in ex.InnerExceptions)
{
Console.WriteLine($" - {error.Message}");
}
}
}
finally
{
await driver.DisposeAsync();
}
Use When:
- You want to continue operation despite errors
- Collecting diagnostics for troubleshooting
- Errors might be transient or non-critical
Ignore Mode
Neither collects nor throws transport errors. Each is still reported through the driver's diagnostic events (see Error Handling — Ignore Mode):
WebSocketConnection connection = new WebSocketConnection();
Transport transport = new Transport(connection);
BiDiDriver driver = new BiDiDriver(TimeSpan.FromSeconds(30), transport);
await driver.StartAsync("ws://localhost:9515/session/YOUR-SESSION-ID");
// Errors won't be thrown or collected
await driver.BrowsingContext.NavigateAsync(navParams);
await driver.StopAsync();
Use When:
- Operating in fire-and-forget mode
- Errors are expected and acceptable
- You have alternative error detection mechanisms
Warning: Use this mode cautiously—it can mask real problems.
Connection-Level Error Handling
Connections provide observable events for error monitoring:
WebSocketConnection connection = new WebSocketConnection();
// Subscribe to connection errors
connection.OnConnectionError.AddObserver((errorArgs) =>
{
Console.WriteLine($"Connection error: {errorArgs.Exception.Message}");
});
// Subscribe to log messages
connection.OnLogMessage.AddObserver((logArgs) =>
{
Console.WriteLine($"[{logArgs.Level}] {logArgs.Message}");
});
Transport transport = new Transport(connection);
BiDiDriver driver = new BiDiDriver(TimeSpan.FromSeconds(30), transport);
await driver.StartAsync("ws://localhost:9515/session/YOUR-SESSION-ID");
Event Handler Error Behavior
Event handlers can also throw exceptions. They never propagate to the code that raised the event; the transport captures them and applies BiDiDriver.TransportConfiguration.EventHandlerExceptionBehavior (see Granular Error Control below). ObservableEventHandlerOptions only decides whether the handler's task is awaited, not how its exceptions are handled:
// Exceptions thrown by handlers never reach the code that raised the event; the
// transport captures them and applies EventHandlerExceptionBehavior (Ignore by
// default: raised on OnEventHandlerErrorOccurred, neither collected nor thrown; Collect: thrown from StopAsync; Terminate: thrown
// from the next command). This applies to synchronous handlers...
driver.TransportConfiguration.EventHandlerExceptionBehavior = TransportErrorBehavior.Collect;
driver.Log.OnEntryAdded.AddObserver((e) =>
{
ProcessLogEntry(e); // If this throws, the transport captures the exception
});
// ...and to asynchronous handlers, whose faults are reported when the task completes.
driver.Network.OnBeforeRequestSent.AddObserver(
async (e) =>
{
await ProcessRequestAsync(e); // A fault here is captured when the task completes
},
ObservableEventHandlerOptions.RunHandlerAsynchronously
);
See the Error Handling Guide for comprehensive error management strategies.
Granular Error Control
Beyond the transport-level error behavior, ITransportConfiguration exposes four properties for fine-grained control over different error scenarios, reached through BiDiDriver.TransportConfiguration. All use TransportErrorBehavior (Ignore/Collect/Terminate) and default to Ignore.
Important: With Terminate mode, exceptions don't propagate immediately when they occur. Due to the asynchronous nature of the library, termination errors are thrown when the next command is sent by the driver, not when the error is first encountered on the message processing thread.
EventHandlerExceptionBehavior
Controls how exceptions thrown by your event handlers are handled:
BiDiDriver driver = new BiDiDriver(TimeSpan.FromSeconds(30));
driver.TransportConfiguration.EventHandlerExceptionBehavior = TransportErrorBehavior.Terminate;
driver.Log.OnEntryAdded.AddObserver((e) =>
{
// If this throws, driver will terminate on next command
ProcessLogEntry(e);
});
try
{
await driver.StartAsync("ws://localhost:9515/session/YOUR-SESSION-ID");
// Perform operations...
await driver.BrowsingContext.NavigateAsync(navParams); // Exception thrown here if handler failed
}
catch (AggregateException ex)
{
// More than one error accumulated before this command: each is an inner exception.
Console.WriteLine($"Event handler errors: {string.Join(", ", ex.InnerExceptions.Select(inner => inner.Message))}");
}
catch (WebDriverBiDiException ex)
{
Console.WriteLine($"Event handler error: {ex.Message}");
}
When to Use Each Mode:
- Ignore (default): Event handler exceptions are raised on
OnEventHandlerErrorOccurredbut don't interrupt message processing. The same applies to exceptions from asynchronously run handlers when those tasks are not captured via a capture session. Use for non-critical handlers. - Collect: Exceptions are stored and thrown when
StopAsync()is called (not byDisposeAsync(), which logs and discards them—callStopAsync()first). Exceptions from asynchronously run handlers are collected the same way when those tasks are not captured via a capture session. Use when debugging event handler issues. - Terminate: Driver terminates when next command is sent after exception. Exceptions from asynchronously run handlers also surface on the next command when those tasks are not captured via a capture session. Use when event handler failure indicates unrecoverable state.
If you explicitly capture async handler tasks with WaitForCapturedTasksAsync() or WaitForCapturedTasksCompleteAsync(), those task exceptions are instead owned by the caller and are not surfaced a second time through transport termination or collection.
ProtocolErrorBehavior
Controls how protocol errors are handled (an error response or registered event whose payload cannot be deserialized, or an unexpected failure while processing a message):
BiDiDriver driver = new BiDiDriver(TimeSpan.FromSeconds(30));
driver.TransportConfiguration.ProtocolErrorBehavior = TransportErrorBehavior.Collect;
await driver.StartAsync("ws://localhost:9515/session/YOUR-SESSION-ID");
// Perform operations...
await driver.BrowsingContext.NavigateAsync(navParams);
// Collected errors are thrown from StopAsync as a single AggregateException
try
{
await driver.StopAsync();
}
catch (AggregateException ex)
{
Console.WriteLine($"Protocol errors encountered: {ex.InnerExceptions.Count}");
}
When to Use Each Mode:
- Ignore (default): Protocol errors are logged but processing continues. Use when working with experimental or unstable protocol versions.
- Collect: Errors are stored and thrown at shutdown. Use when troubleshooting browser compatibility issues.
- Terminate: Driver terminates when next command is sent after error. Use in production when protocol violations indicate serious issues.
UnknownMessageBehavior
Controls how unknown messages are handled (a message that is not valid JSON, or not a command response, error response or registered event):
BiDiDriver driver = new BiDiDriver(TimeSpan.FromSeconds(30));
driver.TransportConfiguration.UnknownMessageBehavior = TransportErrorBehavior.Ignore;
await driver.StartAsync("ws://localhost:9515/session/YOUR-SESSION-ID");
// Browser sends new event type not yet supported by library
// With Ignore mode, these are raised on OnUnknownMessageReceived but don't cause errors
await driver.BrowsingContext.NavigateAsync(navParams);
await driver.StopAsync(); // Completes without exception
When to Use Each Mode:
- Ignore (default): Unknown messages are raised on
OnUnknownMessageReceivedbut don't interrupt processing. Use when working with browsers implementing experimental features. - Collect: Unknown messages are stored and thrown at shutdown. Use when discovering new protocol features or debugging compatibility.
- Terminate: Driver terminates when next command is sent after unknown message. Use when strict protocol conformance is required.
UnexpectedErrorBehavior
Controls how unexpected errors are handled (error responses received with no corresponding command):
BiDiDriver driver = new BiDiDriver(TimeSpan.FromSeconds(30));
driver.TransportConfiguration.UnexpectedErrorBehavior = TransportErrorBehavior.Terminate;
try
{
await driver.StartAsync("ws://localhost:9515/session/YOUR-SESSION-ID");
// If browser sends error response without matching command ID, exception thrown on next command
await driver.BrowsingContext.NavigateAsync(navParams);
}
catch (WebDriverBiDiException ex)
{
Console.WriteLine($"Unexpected error: {ex.Message}");
}
When to Use Each Mode:
- Ignore (default): Unexpected errors are raised on
OnUnexpectedErrorReceivedbut don't interrupt processing. Use when browser may send asynchronous errors. - Collect: Errors are stored and thrown at shutdown. Use when debugging communication issues.
- Terminate: Driver terminates when next command is sent after unexpected error. Use when unexpected errors indicate protocol implementation bugs.
Configuring Multiple Behaviors
All four error behaviors can be configured independently:
BiDiDriver driver = new BiDiDriver(TimeSpan.FromSeconds(30));
// Different strategies for different error types
driver.TransportConfiguration.EventHandlerExceptionBehavior = TransportErrorBehavior.Collect; // Collect handler errors
driver.TransportConfiguration.ProtocolErrorBehavior = TransportErrorBehavior.Terminate; // Fail fast on protocol errors
driver.TransportConfiguration.UnknownMessageBehavior = TransportErrorBehavior.Ignore; // Ignore unknown messages
driver.TransportConfiguration.UnexpectedErrorBehavior = TransportErrorBehavior.Collect; // Collect unexpected errors
await driver.StartAsync("ws://localhost:9515/session/YOUR-SESSION-ID");
// Perform operations...
await driver.BrowsingContext.NavigateAsync(navParams);
// Errors with Collect behavior are thrown here, as one AggregateException
try
{
await driver.StopAsync();
}
catch (AggregateException ex)
{
Console.WriteLine($"Collected errors: {ex.InnerExceptions.Count}");
foreach (Exception error in ex.InnerExceptions)
{
Console.WriteLine($" - {error.Message}");
}
}
Best Practices:
- Start with Terminate during development to catch issues early
- Use Collect when diagnosing intermittent problems
- Switch to Ignore in production for non-critical errors
- Monitor the diagnostic events and
OnLogMessageregardless of behavior setting - Consider your application's error tolerance when choosing behaviors
- Remember that Collect mode defers errors until
StopAsync()is called, and thatDisposeAsync()alone discards them - Remember that Terminate mode throws errors on the next command, not immediately when the error occurs
Extension Points
WebDriverBiDi.NET can be extended in several ways:
Custom Modules
See Custom Modules for the full pattern. Register with the driver:
public class MyCustomModule : Module
{
public const string MyCustomModuleName = "myCustom";
public MyCustomModule(IBiDiModuleHost driver)
: base(driver) { }
public override string ModuleName => MyCustomModuleName;
public async Task<MyCommandResult> MyCommandAsync(
MyCommandParameters parameters)
{
return await this.Driver.ExecuteCommandAsync<MyCommandResult>(
parameters);
}
}
// Register with driver
driver.RegisterModule(new MyCustomModule(driver));
Custom Transport
Create a class that extends Transport and overrides CreateIncomingMessage for custom message processing. Pass an instance to BiDiDriver(TimeSpan, Transport). See Custom Modules for details.
Performance Considerations
Command Batching
Commands are sent one at a time over the connection, because the transport serializes the send. The browser may still process them concurrently, so independent commands are worth issuing in parallel:
// ✅ Good: Execute independent commands in parallel
Task<GetTreeCommandResult> t1 = driver.BrowsingContext.GetTreeAsync(params1);
Task<StatusCommandResult> t2 = driver.Session.StatusAsync(params2);
await Task.WhenAll(t1, t2);
// ❌ Slower: Execute sequentially when not needed
var r1 = await driver.BrowsingContext.GetTreeAsync(params1);
var r2 = await driver.Session.StatusAsync(params2);
Event Handler Performance
Long-running event handlers block message processing:
// ❌ Bad: Blocks message processing
driver.Log.OnEntryAdded.AddObserver((e) =>
{
Thread.Sleep(1000); // Blocks for 1 second
});
// ✅ Good: Run asynchronously
driver.Log.OnEntryAdded.AddObserver(
async (e) =>
{
await Task.Delay(1000); // Doesn't block
},
ObservableEventHandlerOptions.RunHandlerAsynchronously
);
Memory Management
- Unsubscribe from events when no longer needed
- Remove observers to prevent memory leaks
- Stop (and dispose) the driver to close the WebSocket connection
// Remove observer when done
EventObserver<EntryAddedEventArgs> observer =
driver.Log.OnEntryAdded.AddObserver(handler);
// Later...
observer.Unobserve();
// Unsubscribe from events
await driver.Session.UnsubscribeAsync(unsubscribeParams);
// Stop driver
await driver.StopAsync();
Summary
- BiDiDriver: Main facade and entry point
- Transport: WebSocket communication layer
- Modules: Organize functionality by domain
- Commands: Request-response pattern with type safety
- Events: Observable pattern with async support
- Serialization: System.Text.Json with custom converters
- Threading: Fully async, with a dedicated message-processing task on the thread pool
- Extensibility: Custom modules and transport implementations
See Also
- Error Handling: Exception handling, timeout patterns, troubleshooting
- API Design Guide: Timeout and cancellation, command parameter patterns
Next Steps
- Core Concepts: Understand modules, commands, and events
- Browser Setup: Configure browsers for WebSocket or Pipe connections
- Connection Management: Deep dive into connection architecture
- Events and Observables: Deep dive into event handling
- Error Handling: Comprehensive error management strategies
- Module Guides: Learn each module in detail