Core Concepts
Understanding the fundamental concepts of WebDriverBiDi.NET will help you use the library effectively. This guide covers the key architectural elements and design patterns.
The BiDiDriver
The BiDiDriver class is the central entry point for all WebDriver BiDi operations.
// Create a driver with default timeout (60 seconds)
BiDiDriver driver = new BiDiDriver();
// Create a driver with a specific command timeout
BiDiDriver driverWithTimeout = new BiDiDriver(TimeSpan.FromSeconds(30));
// Start the connection
await driver.StartAsync("ws://localhost:9515/session/YOUR-SESSION-ID");
// Stop the connection when done
await driver.StopAsync();
Key Responsibilities
- Manages the connection to the browser (WebSocket or Pipes)
- Provides access to all protocol modules
- Handles command execution and response correlation
- Dispatches events to registered observers
Driver Lifecycle
The BiDiDriver has a well-defined lifecycle with important timing restrictions:
// 1. Create driver
BiDiDriver driver = new BiDiDriver(TimeSpan.FromSeconds(30));
// 2. Register modules and event handlers BEFORE starting
driver.RegisterModule(customModule);
driver.Log.OnEntryAdded.AddObserver((e) => Console.WriteLine(e.Text));
// 3. Start the driver
await driver.StartAsync("ws://localhost:9515/session/YOUR-SESSION-ID");
// 4. Check if started
if (driver.IsStarted)
{
// Execute commands...
await driver.BrowsingContext.NavigateAsync(navParams);
}
// 5. Stop when done
await driver.StopAsync();
IsStarted Property
The IsStarted property indicates whether the driver is currently connected:
if (!driver.IsStarted)
{
await driver.StartAsync(webSocketUrl);
}
// Execute commands only when started
if (driver.IsStarted)
{
await driver.BrowsingContext.NavigateAsync(navParams);
}
Use Cases:
- Check driver state before operations
- Verify connection before executing commands
- Safe disposal patterns
Timing Restrictions
Critical: Module registration (RegisterModule), custom event registration (RegisterEvent), and type resolver registration (RegisterTypeInfoResolverAsync) must happen BEFORE calling StartAsync(). Adding observers with AddObserver is not restricted and may be done at any time:
// ✅ CORRECT: Register before starting
BiDiDriver driver = new BiDiDriver(TimeSpan.FromSeconds(30));
driver.RegisterModule(new CustomModule(driver));
driver.Log.OnEntryAdded.AddObserver((e) => Console.WriteLine(e.Text));
await driver.StartAsync(webSocketUrl);
BiDiDriver driver = new BiDiDriver(TimeSpan.FromSeconds(30));
await driver.StartAsync(webSocketUrl);
// ❌ WRONG: Cannot register a module after starting - This will throw an exception!
driver.RegisterModule(new CustomModule(driver));
// Adding observers is not restricted and works here too; it is shown
// before StartAsync in the correct sample only by convention.
driver.Log.OnEntryAdded.AddObserver((e) => Console.WriteLine(e.Text));
Why This Restriction Exists:
- Ensures every event the transport can receive has a registered deserializer before messages start flowing
- Prevents race conditions
- Maintains predictable initialization order
Observers are deliberately exempt: ObservableEvent<T>.AddObserver is thread-safe with respect to event dispatch, so observers can be added and removed while the driver is running (for example, a temporary observer around a single navigation). The ordering that matters for observers is that they are added before the corresponding Session.SubscribeAsync() call, so no events are missed.
Thread Safety:
The RegisterModule method is thread-safe and can be called concurrently from multiple threads:
// Each module registers under its own name; two modules sharing a name is rejected however they
// are registered.
CoreConceptsDocModule customModule1 = new CoreConceptsDocModule(driver);
CoreConceptsOtherDocModule customModule2 = new CoreConceptsOtherDocModule(driver);
// This is safe - concurrent registration is handled properly
Parallel.Invoke(
() => driver.RegisterModule(customModule1),
() => driver.RegisterModule(customModule2));
Thread safety is enforced using an internal lock that makes the lifecycle check and the module addition atomic. The check is against the transport's state rather than IsStarted: registration is rejected as soon as the transport leaves Disconnected, which happens when a connect begins, not when it completes. Registration is legal again once a teardown returns the transport to Disconnected.
Command Timeout Configuration
Configure command timeout when creating the driver:
// Default timeout (60 seconds)
BiDiDriver defaultDriver = new BiDiDriver();
// Custom timeout (30 seconds)
BiDiDriver customDriver = new BiDiDriver(TimeSpan.FromSeconds(30));
// Short timeout for fast operations
BiDiDriver shortDriver = new BiDiDriver(TimeSpan.FromSeconds(5));
// Long timeout for slow operations
BiDiDriver longDriver = new BiDiDriver(TimeSpan.FromMinutes(10));
The timeout applies to all command executions by default, but can be overridden per-command. Prefer the timeoutOverride parameter on module methods (e.g., NavigateAsync(parameters, TimeSpan.FromSeconds(60))) as the standard way to set per-command timeouts:
// Use driver's default timeout (30 seconds)
await driver.BrowsingContext.NavigateAsync(navParams);
// Override with custom timeout for this command
await driver.BrowsingContext.NavigateAsync(navParams, TimeSpan.FromSeconds(60));
For timeout patterns (returning null instead of throwing, custom retry logic), see Error Handling - Timeout Handling.
Proper Disposal
Always dispose of the driver when done:
// Stopping ends the session; disposing also releases the driver's own resources, so do both. The
// `await using` declaration disposes the driver when the method returns, whatever happens.
await using BiDiDriver driver = new BiDiDriver(TimeSpan.FromSeconds(30));
try
{
await driver.StartAsync(webSocketUrl);
// Use driver...
}
finally
{
// Stop inside the try/finally so that collected errors are observed here rather than by
// DisposeAsync, which logs them and moves on.
await driver.StopAsync();
}
// Or with async disposal
await using BiDiDriver driver = new BiDiDriver(TimeSpan.FromSeconds(30));
await driver.StartAsync(webSocketUrl);
// Use driver...
// Automatically disposed at end of scope
Complete Lifecycle Example
// Create driver with custom timeout
BiDiDriver driver = new BiDiDriver(TimeSpan.FromSeconds(30));
try
{
// Register event handlers before starting
driver.Log.OnEntryAdded.AddObserver((e) =>
{
Console.WriteLine($"[{e.Level}] {e.Text}");
});
driver.BrowsingContext.OnLoad.AddObserver((e) =>
{
Console.WriteLine($"Page loaded: {e.Url}");
});
// Start the driver
await driver.StartAsync("ws://localhost:9515/session/YOUR-SESSION-ID");
// Verify driver is started
if (!driver.IsStarted)
{
throw new InvalidOperationException("Driver failed to start");
}
// Subscribe to events
SubscribeCommandParameters subscribe = new(
[
driver.Log.OnEntryAdded.EventName,
driver.BrowsingContext.OnLoad.EventName,
]
);
await driver.Session.SubscribeAsync(subscribe);
// Execute commands
GetTreeCommandResult tree = await driver.BrowsingContext.GetTreeAsync(
new GetTreeCommandParameters());
string contextId = tree.ContextTree[0].BrowsingContextId;
NavigateCommandParameters navParams = new(contextId, "https://example.com")
{
Wait = ReadinessState.Complete
};
await driver.BrowsingContext.NavigateAsync(navParams);
}
finally
{
// Always stop the driver
if (driver.IsStarted)
{
await driver.StopAsync();
}
}
Modules
WebDriver BiDi organizes functionality into modules, each representing a specific area of browser control.
Available Modules
WebDriverBiDi.NET includes the following modules:
| Module | Purpose | Access Property |
|---|---|---|
| Browser | Browser-level operations | driver.Browser |
| BrowsingContext | Tab/window management and navigation | driver.BrowsingContext |
| Script | JavaScript execution | driver.Script |
| Network | Network traffic monitoring and control | driver.Network |
| Input | User input simulation | driver.Input |
| Log | Console and browser log access | driver.Log |
| Session | Session management and subscriptions | driver.Session |
| Storage | Cookies and storage management | driver.Storage |
| Emulation | Device and media emulation | driver.Emulation |
| Permissions | Permission management | driver.Permissions |
| Bluetooth | Web Bluetooth API control | driver.Bluetooth |
| DigitalCredentials | Virtual digital wallet simulation | driver.DigitalCredentials |
| WebExtension | Browser extension management | driver.WebExtension |
| Speculation | Navigation prefetching | driver.Speculation |
| UserAgentClientHints | Emulates browser, platform, and device reporting | driver.UserAgentClientHints |
Accessing Modules
// Access a module through the driver
BrowsingContextModule browsingContext = driver.BrowsingContext;
NetworkModule network = driver.Network;
ScriptModule script = driver.Script;
Commands and Responses
WebDriver BiDi uses a command-response pattern for browser operations.
Command Structure
All commands follow this pattern:
- Create a command parameters object
- Execute the command through the appropriate module
- Receive a typed response object
// 1. Create parameters
NavigateCommandParameters parameters = new NavigateCommandParameters(
contextId,
"https://example.com")
{
Wait = ReadinessState.Complete
};
// 2. Execute command
NavigateCommandResult result = await driver.BrowsingContext.NavigateAsync(parameters);
// 3. Use the result
Console.WriteLine($"Navigated to: {result.Url}");
Command Parameters
- Mutable: You can set properties on parameter objects before sending
- Required Parameters: Passed through the constructor
- Optional Parameters: Set through properties with initializers
// Required parameters in constructor
NavigateCommandParameters @params = new NavigateCommandParameters(contextId, url);
// Optional parameters via properties
@params.Wait = ReadinessState.Complete;
// Timeout overrides are supplied when executing the command
await driver.BrowsingContext.NavigateAsync(
@params,
TimeSpan.FromSeconds(30));
When Parameters Are Optional
Some module commands accept an optional CommandParameters object—you can pass null or omit it, and the library will use default parameters. Other commands always require a parameters object. The rule depends on whether the command has a "reset" capability.
Optional parameters (no required properties, no reset property):
These commands allow omitting parameters when you want default behavior:
// ✅ CORRECT: Parameters optional—use defaults
GetTreeCommandResult tree1 = await driver.BrowsingContext.GetTreeAsync(new GetTreeCommandParameters());
// Or equivalently:
GetTreeCommandResult tree2 = await driver.BrowsingContext.GetTreeAsync(null);
// ✅ CORRECT: Same for other optional-parameter commands
StatusCommandResult status = await driver.Session.StatusAsync(null);
GetCookiesCommandResult cookies = await driver.Storage.GetCookiesAsync(null);
Required parameters (has a reset property):
Some commands can reset a value on the remote end to its original state. These commands always require a parameters object so the intent is explicit. Passing no parameters would be ambiguous—are you setting a value or resetting it?
// ✅ CORRECT: Use the reset property when resetting
await driver.UserAgentClientHints.SetClientHintsOverrideAsync(
SetClientHintsOverrideCommandParameters.ResetClientHintsOverride);
// ✅ CORRECT: Pass explicit parameters when setting
SetClientHintsOverrideCommandParameters setParams = new SetClientHintsOverrideCommandParameters();
setParams.ClientHints = new ClientHintsMetadata
{
Brands = new List<BrandVersion>() { new BrandVersion("MyBrowser", "120.0") }
};
await driver.UserAgentClientHints.SetClientHintsOverrideAsync(setParams);
// ❌ WRONG: SetClientHintsOverrideAsync always requires parameters
// The command name alone doesn't indicate whether you're setting or resetting
// await driver.UserAgentClientHints.SetClientHintsOverrideAsync(null); // Not allowed
Commands with optional parameters include: Browser.CloseAsync, Browser.CreateUserContextAsync, Browser.GetClientWindowsAsync, Browser.GetUserContextsAsync, BrowsingContext.GetTreeAsync, Script.GetRealmsAsync, Session.EndAsync, Session.NewSessionAsync, Session.StatusAsync, Storage.DeleteCookiesAsync, Storage.GetCookiesAsync.
Commands that require parameters (because they have reset properties) include: UserAgentClientHints.SetClientHintsOverrideAsync, Browser.SetDownloadBehaviorAsync, BrowsingContext.SetViewportAsync, BrowsingContext.SetBypassCSPAsync, Network.SetExtraHeadersAsync, and every command on the Emulation module — both the Set*OverrideAsync commands (e.g., SetUserAgentOverrideAsync, SetGeolocationOverrideAsync) and SetNetworkConditionsAsync and SetScriptingEnabledAsync. See API Design Principles for the complete table.
Command Results
- Immutable: Properties are read-only
- Strongly Typed: Each command returns a specific result type
- Error Handling: Errors throw
WebDriverBiDiException
try
{
NavigateCommandResult result = await driver.BrowsingContext.NavigateAsync(@params);
// Result properties are read-only
string url = result.Url;
string? navigationId = result.NavigationId;
}
catch (WebDriverBiDiException ex)
{
Console.WriteLine($"Command failed: {ex.Message}");
}
Events and Observable Events
WebDriver BiDi is event-driven, allowing you to react to browser events as they occur.
Observable Events
Each event is exposed through an ObservableEvent<T> property on the relevant module:
// Events on BrowsingContext module
_ = driver.BrowsingContext.OnLoad;
_ = driver.BrowsingContext.OnDomContentLoaded;
_ = driver.BrowsingContext.OnNavigationStarted;
// Events on Network module
_ = driver.Network.OnBeforeRequestSent;
_ = driver.Network.OnResponseCompleted;
// Events on Log module
_ = driver.Log.OnEntryAdded;
Event Subscription
Before receiving events, you must subscribe to them through the Session module:
// Create subscription parameters
SubscribeCommandParameters subscribe = new SubscribeCommandParameters(
[
driver.Log.OnEntryAdded.EventName,
driver.Network.OnResponseCompleted.EventName,
]
);
// Subscribe to events
await driver.Session.SubscribeAsync(subscribe);
Event Observers
Add observers to handle events:
// Add a simple observer
driver.Log.OnEntryAdded.AddObserver((EntryAddedEventArgs e) =>
{
Console.WriteLine($"Console log: {e.Text}");
});
// Add an async observer
driver.Network.OnBeforeRequestSent.AddObserver(
async (BeforeRequestSentEventArgs e) =>
{
Console.WriteLine($"Request to: {e.Request.Url}");
await Task.Delay(100); // Can perform async operations
},
ObservableEventHandlerOptions.RunHandlerAsynchronously);
Event Observer Pattern
Observers can be managed and synchronized:
// Create an observer with reference
EventObserver<EntryAddedEventArgs> observer =
driver.Log.OnEntryAdded.AddObserver((e) =>
{
Console.WriteLine(e.Text);
});
// Start capturing to wait for N events
observer.StartCapturingTasks();
// Perform operations that trigger events
await driver.BrowsingContext.NavigateAsync(navParams);
// Wait for 5 events
Task[] tasks = await observer.WaitForCapturedTasksAsync(5, TimeSpan.FromSeconds(10));
bool fulfilled = tasks.Length == 5;
observer.StopCapturingTasks();
// Remove the observer
observer.Unobserve();
Browsing Contexts
A browsing context represents a document environment (tab, window, or iframe).
Context IDs
Every operation that interacts with a page requires a context ID:
// Get all contexts
GetTreeCommandResult tree = await driver.BrowsingContext.GetTreeAsync(
new GetTreeCommandParameters());
// Get the first context ID
string contextId = tree.ContextTree[0].BrowsingContextId;
// Navigate in that context
await driver.BrowsingContext.NavigateAsync(
new NavigateCommandParameters(contextId, url));
Context Tree
Contexts form a tree structure (windows contain iframes):
GetTreeCommandResult tree = await driver.BrowsingContext.GetTreeAsync(
new GetTreeCommandParameters());
foreach (BrowsingContextInfo context in tree.ContextTree)
{
Console.WriteLine($"Context: {context.BrowsingContextId}");
Console.WriteLine($" URL: {context.Url}");
// Children is null when the tree was not walked to this depth (for example, getTree with
// maxDepth 0, or a contextCreated event); an empty list means there are no children.
Console.WriteLine($" Children: {context.Children?.Count ?? 0}");
}
Creating Contexts
// Create a new tab
CreateCommandParameters createParams = new CreateCommandParameters(CreateType.Tab);
CreateCommandResult newContext = await driver.BrowsingContext.CreateAsync(createParams);
string newContextId = newContext.BrowsingContextId;
Remote Values
JavaScript values are represented as RemoteValue objects when returned from the browser.
Value Types
Remote values have a Type property indicating their JavaScript type. It is a RemoteValueType enumeration value rather than a string, so compare it against enum members (remoteValue.Type == RemoteValueType.Node):
String,Number,Boolean,Undefined,NullObject,Array,Function,PromiseNode(DOM elements)Window,RegExp,Date,Map,SetNodeList,HtmlCollection(both surfaced asCollectionRemoteValue)
See Working with Remote Values for the full type mapping.
Accessing Values
Pattern match or use As<T>() to cast to the concrete type and access the Value property:
EvaluateResult result = await driver.Script.EvaluateAsync(
new EvaluateCommandParameters("42", new ContextTarget(contextId), true));
if (result is EvaluateResultSuccess success &&
success.Result is NumberRemoteValue remoteValue)
{
// Convert to appropriate .NET type
long number = remoteValue; // JavaScript number -> long
// Check the type
Console.WriteLine($"Type: {remoteValue.Type}"); // RemoteValueType.Number
}
Working with DOM Elements
DOM elements have a SharedId that allows them to be referenced in subsequent commands:
// Get a script target against which to run JavaScript
Target target = new ContextTarget(contextId);
// Get an element
EvaluateResult result = await driver.Script.EvaluateAsync(
new EvaluateCommandParameters(
"document.querySelector('button')",
target,
true));
if (result is EvaluateResultSuccess success &&
success.Result is NodeRemoteValue element)
{
// Get node properties
NodeProperties nodeProps = element.GetNodeProperties();
Console.WriteLine($"Tag: {nodeProps.LocalName}");
// Create a reference to use in other commands
SharedReference elementRef = element.ToSharedReference();
// Use the reference in another script call
CallFunctionCommandParameters clickParams = new CallFunctionCommandParameters(
"(element) => element.click()",
target,
false);
clickParams.Arguments.Add(elementRef);
await driver.Script.CallFunctionAsync(clickParams);
}
Creating Local Values
When passing values to JavaScript, create LocalValue instances:
CallFunctionCommandParameters @params = new CallFunctionCommandParameters(
"(a, b, c) => a + b + c.length",
new ContextTarget(contextId),
true);
// Add arguments as local values
@params.Arguments.Add(LocalValue.Number(5));
@params.Arguments.Add(LocalValue.Number(10));
@params.Arguments.Add(LocalValue.String("hello"));
EvaluateResult result = await driver.Script.CallFunctionAsync(@params);
// Result: 20 (5 + 10 + 5)
Async/Await Pattern
All WebDriverBiDi.NET operations are asynchronous.
Best Practices
// ✅ Good: Await async operations
NavigateCommandResult result = await driver.BrowsingContext.NavigateAsync(parameters);
// ❌ Bad: Don't block with .Result or .Wait()
var badResult = driver.BrowsingContext.NavigateAsync(parameters).Result; // Can deadlock
// ✅ Good: Use ConfigureAwait(false) in library code
await driver.BrowsingContext.NavigateAsync(parameters).ConfigureAwait(false);
Parallel Operations
You can execute multiple independent commands in parallel:
// Execute multiple navigations in parallel
Task<NavigateCommandResult> nav1 = driver.BrowsingContext.NavigateAsync(
new NavigateCommandParameters(contextId1, url1));
Task<NavigateCommandResult> nav2 = driver.BrowsingContext.NavigateAsync(
new NavigateCommandParameters(contextId2, url2));
await Task.WhenAll(nav1, nav2);
Console.WriteLine($"Context 1: {nav1.Result.Url}");
Console.WriteLine($"Context 2: {nav2.Result.Url}");
Error Handling
WebDriver BiDi operations can fail for various reasons.
WebDriverBiDiException
Every protocol-level failure is reported through WebDriverBiDiException. An error response from the browser arrives as WebDriverBiDiCommandException (with an ErrorCode), a missing response as WebDriverBiDiTimeoutException, and a lost connection as WebDriverBiDiConnectionException; catching the base type covers all of those. Caller mistakes are a separate matter: they surface as the usual .NET exceptions, such as ArgumentNullException, ObjectDisposedException and InvalidOperationException. See Error Handling — Exception Hierarchy for the full list of both.
try
{
await driver.BrowsingContext.NavigateAsync(@params);
}
catch (WebDriverBiDiCommandException ex)
{
// The browser answered with an error response; ErrorCode tells you which
Console.WriteLine($"Command error {ex.ErrorCode}: {ex.ProtocolErrorMessage}");
}
catch (WebDriverBiDiException ex)
{
// Timeouts, connection loss, serialization failures, and other library errors
Console.WriteLine($"BiDi error: {ex.Message}");
}
catch (Exception ex)
{
Console.WriteLine($"Unexpected error: {ex.Message}");
}
Script Exceptions
JavaScript errors are returned as EvaluateResultException:
EvaluateResult result = await driver.Script.EvaluateAsync(
new EvaluateCommandParameters("throw new Error('Oops!')", target, true));
if (result is EvaluateResultException exception)
{
Console.WriteLine($"Script error: {exception.ExceptionDetails.Text}");
Console.WriteLine($"Line: {exception.ExceptionDetails.LineNumber}");
Console.WriteLine($"Column: {exception.ExceptionDetails.ColumnNumber}");
}
Timeouts
Commands that exceed the timeout will throw an exception. Use the timeoutOverride parameter on module methods to set per-command timeouts:
// The driver's default applies to every command unless overridden
BiDiDriver driver = new BiDiDriver(TimeSpan.FromSeconds(30));
try
{
// This navigation gets a 5-second timeout via the timeoutOverride parameter
await driver.BrowsingContext.NavigateAsync(parameters, TimeSpan.FromSeconds(5));
}
catch (WebDriverBiDiTimeoutException)
{
Console.WriteLine("Navigation took too long");
}
Immutability Principle
A key design principle: data from the browser is immutable, data to the browser is mutable.
Immutable (From Browser)
NavigateCommandResult result = await driver.BrowsingContext.NavigateAsync(@params);
// ❌ Cannot modify - properties are read-only
// result.Url = "something else"; // Compilation error
Mutable (To Browser)
NavigateCommandParameters parameters = new NavigateCommandParameters(contextId, url);
// ✅ Can modify - properties are settable
parameters.Wait = ReadinessState.Complete;
// Timeout overrides are supplied when executing the command
await driver.BrowsingContext.NavigateAsync(
parameters,
TimeSpan.FromSeconds(30));
Advanced Topics
⚠️ Note for Most Users: The features described in this section are for advanced scenarios and library developers building on top of WebDriverBiDi.NET (such as Selenium, Puppeteer, or Playwright maintainers). Most application developers will never need these features. The standard
BiDiDriverclass with its built-in modules covers the vast majority of use cases.Skip this section if you are:
- Building a typical browser automation application
- Using WebDriverBiDi.NET for testing or web scraping
- Learning the library for the first time
Read this section if you are:
- Building a higher-level automation framework or library
- Extending the protocol with custom modules
- Implementing protocol features not yet in the library
- Need fine-grained control over serialization (AOT scenarios)
Advanced Abstractions
For normal application code, use the concrete BiDiDriver class directly.
BiDiDriver driver = new BiDiDriver(TimeSpan.FromSeconds(30));
await driver.StartAsync(webSocketUrl);
That is the intended experience for almost every consumer of this library. Most users should never need to reference any interface type directly.
For advanced framework, testing, and extensibility scenarios, BiDiDriver also implements five focused interfaces, and exposes two more through its properties:
| Interface | Purpose | Typical advanced use |
|---|---|---|
IBiDiModuleHost |
Command execution and event registration | Custom modules and test doubles that execute commands or register protocol events: the surface a module needs from its host |
IBiDiDriverLifecycleManager |
Driver lifecycle | Framework code that owns the driver's lifetime without executing commands itself: starting, stopping and disposing the driver, or checking IsStarted |
IBiDiDriverConfiguration |
Pre-start extensibility hooks | Registering custom modules and additional JSON type resolvers before StartAsync() |
IBiDiDriverEvents |
Driver observability | Subscribing to top-level driver events |
IEventObserverErrorReporter |
Observer failure reporting | Implemented by a custom IBiDiModuleHost to receive the failures of asynchronous observers of its modules' events; BiDiDriver already implements it |
ITransportConfiguration |
Tunable transport settings | Adjusting the log level, the transport error behaviors, the shutdown and connection-lock timeouts, and how many canceled commands are remembered, via BiDiDriver.TransportConfiguration |
ITransportDiagnostics |
Observable transport state | Polling lifecycle state, incoming queue depth and pending command count, via BiDiDriver.TransportDiagnostics |
The hierarchy is intentionally split by capability rather than by end-user workflow:
BiDiDriveris the primary type for applications.IBiDiModuleHostis the narrow surface a module needs from its host: executing commands and registering its protocol events.IBiDiDriverLifecycleManagercovers starting, stopping and disposing the driver.IBiDiDriverConfigurationcovers advanced pre-start customization.IBiDiDriverEventscovers top-level driver events.IEventObserverErrorReporterreceives the failures of observers that run asynchronously. Unlike the others, it is meant to be implemented, by a customIBiDiModuleHost;BiDiDriverimplements it already, so application code never needs it.ITransportConfigurationandITransportDiagnosticsare the transport's settings and its observable state. They are reached fromBiDiDriver.TransportConfigurationandBiDiDriver.TransportDiagnostics, so a driver built withnew BiDiDriver()can be tuned and observed without constructing aTransportby hand. They deliberately exclude the transport's lifecycle and messaging operations, which belong to the driver that owns it.
If you are building a higher-level library on top of WebDriverBiDi.NET, choose the narrowest interface that matches the capability you need. If you are writing application code, ignore the interfaces and use BiDiDriver.
Custom Modules
Custom modules allow you to extend WebDriverBiDi.NET with protocol features not yet implemented in the library, or to add proprietary browser-specific extensions.
⚠️ Advanced Feature: Custom modules are only needed when:
- Implementing cutting-edge protocol features before they're added to the library
- Supporting browser-specific extensions to the WebDriver BiDi protocol
- Building a framework that needs to expose additional capabilities
Most users should use the built-in modules (Browser, BrowsingContext, Network, Script, etc.), which cover all standard protocol features.
Creating a Custom Module:
public class CustomModule : Module
{
// "custom" is the protocol module name
public const string CustomModuleName = "custom";
private readonly ObservableEventInvocable<CustomEventArgs> onCustomEvent =
new ObservableEventInvocable<CustomEventArgs>("custom.eventOccurred");
public CustomModule(IBiDiModuleHost driver)
: base(driver)
{
// Register custom events using the base class helper
this.RegisterObservableEvent(this.onCustomEvent);
}
public override string ModuleName => CustomModuleName;
// Define custom commands
public async Task<CustomCommandResult> MyCustomCommandAsync(
CustomCommandParameters parameters)
{
return await this.Driver.ExecuteCommandAsync<CustomCommandResult>(
parameters);
}
// Define custom events — expose as ObservableEvent<T> so callers cannot invoke it
public ObservableEvent<CustomEventArgs> OnCustomEvent => this.onCustomEvent;
}
// Command parameters (mutable - sent to browser)
public class CustomCommandParameters : CommandParameters<CustomCommandResult>
{
public CustomCommandParameters()
: base() // Protocol method name
{
}
public string CustomProperty { get; set; }
public override string MethodName => "custom.myCommand";
}
// Command result (immutable - received from browser)
public record CustomCommandResult : CommandResult
{
// [JsonInclude] is what opts a non-public accessor in; without it the member never populates.
[JsonInclude]
public string ResultData { get; internal set; } = string.Empty;
}
// Event arguments (immutable - received from browser)
public record CustomEventArgs : WebDriverBiDiEventArgs
{
[JsonInclude]
public string EventData { get; internal set; } = string.Empty;
}
Registering and Using a Custom Module:
// Create driver
BiDiDriver driver = new BiDiDriver(TimeSpan.FromSeconds(30));
// Register custom module BEFORE starting
// The module constructor calls RegisterObservableEvent for each event it exposes
CustomModule customModule = new CustomModule(driver);
driver.RegisterModule(customModule);
// NOW start the driver
await driver.StartAsync(webSocketUrl);
// Use custom module like built-in modules
CustomCommandParameters parameters = new CustomCommandParameters
{
CustomProperty = "value"
};
CustomCommandResult result = await customModule.MyCustomCommandAsync(parameters);
Important Considerations:
- Custom modules must be registered before
StartAsync() - The protocol method names must match what the browser expects
- JSON serialization must align with the protocol specification
- See Custom Modules Guide for complete details
Custom JSON Type Resolvers (AOT Scenarios)
For ahead-of-time (AOT) compilation scenarios where reflection-based JSON serialization is unavailable, you can register custom IJsonTypeInfoResolver instances.
⚠️ Specialized Feature: This is only needed for:
- Native AOT deployment (e.g., NativeAOT in .NET 7+)
- Custom module types that need explicit serialization metadata
- Environments where reflection is restricted or disabled
Most users can ignore this - the library handles JSON serialization automatically using reflection when available.
Example:
[JsonSourceGenerationOptions(WriteIndented = false)]
[JsonSerializable(typeof(CustomCommandParameters))]
[JsonSerializable(typeof(CustomCommandResult))]
[JsonSerializable(typeof(CustomEventArgs))]
internal partial class CustomJsonContext : JsonSerializerContext
{
}
// Register the custom resolver BEFORE starting
BiDiDriver driver = new BiDiDriver(TimeSpan.FromSeconds(30));
await driver.RegisterTypeInfoResolverAsync(CustomJsonContext.Default);
await driver.StartAsync(webSocketUrl);
When This is Required:
- Publishing with
<PublishAot>true</PublishAot>in your .csproj - Using custom modules with custom parameter/result types
- Running in restricted environments (iOS, WebAssembly, etc.)
When You Don't Need This:
- Regular .NET applications using JIT compilation
- Using only the built-in modules and types
- Any scenario where reflection is available (the default)
See AOT Compatibility Guide for complete details on AOT deployment.
When to Use These Advanced Features
Use this decision tree to determine if you need these advanced features:
Are you building a higher-level framework on top of WebDriverBiDi.NET?
├─ YES → You may use custom modules, custom type resolvers, or one of the advanced capability interfaces internally
└─ NO → Use BiDiDriver directly
Does the built-in library support the protocol feature you need?
├─ YES → Use the built-in modules (Browser, Network, etc.)
└─ NO → You might need a custom module
Are you deploying with Native AOT compilation?
├─ YES → You might need custom type resolvers
└─ NO → Reflection-based serialization works automatically
Are you implementing browser-specific extensions?
├─ YES → You need custom modules and possibly custom events
└─ NO → Use the standard modules
For almost all users: You don't need any of these features. Use BiDiDriver with the built-in modules and you're set.
For framework developers: These features provide the extensibility needed to build rich automation libraries while maintaining type safety and performance.
Next Steps
- Quick Reference: Cheat sheet of common commands
- API Design Guide: Parameter patterns, timeouts, versioning
- Events and Observables: Deep dive into event handling
- Remote Values: Comprehensive guide to JavaScript value handling
- Module Guides: Learn about each module in detail
- Common Scenarios: See practical examples
- Custom Modules Guide: Complete guide to creating custom modules (advanced)
- AOT Compatibility: Native AOT deployment guide (advanced)
Summary
BiDiDriveris the main entry point- Modules organize functionality by area
- Commands use parameters (mutable) and return results (immutable)
- Events require subscription and use the observer pattern
- Browsing contexts represent tabs/windows/iframes
- Remote values represent JavaScript data
- All operations are async
- Protocol failures are thrown as
WebDriverBiDiException; caller mistakes as the usual .NET exceptions