Getting Started with WebDriverBiDi.NET
This guide will walk you through installing WebDriverBiDi.NET and setting up your first browser automation project.
Prerequisites
- Runtime: The package targets .NET Standard 2.0 alongside .NET 8, 9 and 10, so it runs on .NET Framework 4.6.2+ and .NET 8+. Its netstandard2.0 dependencies no longer support .NET Framework 4.6.1 or .NET Core 2.x, and warn when restored for them. Any SDK that can target your chosen runtime is enough to consume the library
- .NET SDK: 7.0 or later to build the samples in this guide as written, because they use C# 11 raw string literals
- IDE: Visual Studio, Visual Studio Code, or JetBrains Rider
- Browser: A browser with WebDriver BiDi support (Chrome, Edge, Firefox)
Installation
Using NuGet Package Manager
Install the WebDriverBiDi package from NuGet:
dotnet add package WebDriverBiDi
Or using the Package Manager Console in Visual Studio:
Install-Package WebDriverBiDi
Pinning an exact version
dotnet add package records the current release automatically, which is the recommended way to add the reference. Because the library is in the 0.x series — where any release, including a patch increment, may change or remove public API — pin an exact version in your .csproj so that an update is always a deliberate choice rather than a wildcard that can pull in a breaking change:
<PackageReference Include="WebDriverBiDi" Version="0.0.63" />
Check NuGet for the latest version, and see API Design and Compatibility for the versioning policy.
Launching Browsers
The WebDriverBiDi package is the protocol client: it connects to a browser that is already running, and this guide starts one by hand. To download and launch browsers from code, see Dramaturge.Browsers, part of the Dramaturge automation library built on WebDriverBiDi.NET.
Optional: Conveniences
The WebDriverBiDi.Extensions package adds one-call extension methods for common commands and an input action builder:
dotnet add package WebDriverBiDi.Extensions
Optional: Roslyn Analyzers
For compile-time help catching common usage errors, add the WebDriverBiDi.Analyzers package. See Roslyn Analyzers for the full list of available analyzers.
Browser Setup
WebDriverBiDi.NET requires a connection that speaks WebDriver BiDi. See the Browser Setup Guide for the full picture; the essentials are below.
Setting Up a Browser
You start the browser (or its driver) yourself and connect to the WebSocket URL it reports.
Chrome, Chromium and Edge
Chrome and Edge do not speak WebDriver BiDi on their --remote-debugging-port endpoint — that endpoint (ws://localhost:9222/devtools/browser/<id>, reported by /json/version) speaks the Chrome DevTools Protocol only, and a BiDiDriver connected to it fails on its first command. Use the browser's driver executable instead: chromedriver (from Chrome for Testing) or msedgedriver.
chromedriver --port=9515
Then create a WebDriver session that asks for a BiDi WebSocket:
curl -X POST http://localhost:9515/session \
-H "Content-Type: application/json" \
-d '{"capabilities":{"alwaysMatch":{"webSocketUrl":true}}}'
The response's value.capabilities.webSocketUrl — for example ws://localhost:9515/session/8a4d1c2e-… — is the URL for BiDiDriver.StartAsync(). chromedriver launches the browser as part of creating the session, so you do not start Chrome yourself; browser flags such as --headless=new go in goog:chromeOptions.args (ms:edgeOptions.args for Edge).
Firefox
Firefox speaks WebDriver BiDi natively. Either launch it directly:
firefox --remote-debugging-port=9222
and connect to ws://localhost:9222/session, or run geckodriver (geckodriver --port 4444) and connect to ws://localhost:4444/session. On both of these endpoints you must call driver.Session.NewSessionAsync(...) after StartAsync, because no session exists yet. (geckodriver also accepts the webSocketUrl: true classic session shown above, in which case the session already exists.)
Getting the WebSocket URL Programmatically
Creating the session from C# is a single HTTP request:
public static async Task<string> CreateBiDiSessionAsync(string driverUrl = "http://localhost:9515")
{
using HttpClient client = new HttpClient();
// Ask the driver for a WebDriver BiDi WebSocket for the session it creates
const string body = """
{ "capabilities": { "alwaysMatch": { "webSocketUrl": true } } }
""";
using StringContent content = new StringContent(body, Encoding.UTF8, "application/json");
try
{
using HttpResponseMessage response = await client.PostAsync($"{driverUrl}/session", content);
string json = await response.Content.ReadAsStringAsync();
response.EnsureSuccessStatusCode();
using JsonDocument doc = JsonDocument.Parse(json);
JsonElement capabilities = doc.RootElement.GetProperty("value").GetProperty("capabilities");
if (capabilities.TryGetProperty("webSocketUrl", out JsonElement urlElement))
{
string? webSocketUrl = urlElement.GetString();
if (!string.IsNullOrEmpty(webSocketUrl))
{
return webSocketUrl;
}
}
throw new Exception("The driver did not return a webSocketUrl; the browser may not support WebDriver BiDi");
}
catch (HttpRequestException ex)
{
throw new Exception($"Failed to reach the driver at {driverUrl}. " +
"Ensure chromedriver (or msedgedriver/geckodriver) is running, e.g. chromedriver --port=9515", ex);
}
}
// Usage
string webSocketUrl = await CreateBiDiSessionAsync("http://localhost:9515");
BiDiDriver driver = new BiDiDriver(TimeSpan.FromSeconds(30));
await driver.StartAsync(webSocketUrl);
// The session already exists; do not call driver.Session.NewSessionAsync
Common Connection String Formats
Session created through chromedriver, msedgedriver or geckodriver (recommended):
ws://localhost:9515/session/<session-id>
The session already exists; do not call NewSessionAsync.
Firefox launched directly, or geckodriver's BiDi-only endpoint:
ws://localhost:9222/session
ws://localhost:4444/session
Call NewSessionAsync after connecting.
Chrome's CDP endpoints (do not use):
ws://localhost:9222/devtools/browser/<browser-id>
ws://localhost:9222/devtools/page/<page-id>
Complete Connection Example
public class BrowserConnection
{
public static async Task<BiDiDriver> ConnectToBrowserAsync(string driverUrl = "http://localhost:9515")
{
// Create a session through the driver executable and get its BiDi endpoint
string webSocketUrl;
try
{
webSocketUrl = await CreateBiDiSessionAsync(driverUrl);
Console.WriteLine($"Session created; WebDriver BiDi endpoint: {webSocketUrl}");
}
catch (Exception ex)
{
throw new InvalidOperationException(
$"Failed to create a WebDriver BiDi session through the driver at {driverUrl}. " +
"For Chrome or Edge, run chromedriver/msedgedriver and request the webSocketUrl capability. " +
"For Firefox, connect to ws://localhost:<port>/session and call Session.NewSessionAsync instead.",
ex);
}
// Create and start driver
BiDiDriver driver = new BiDiDriver(TimeSpan.FromSeconds(30));
try
{
await driver.StartAsync(webSocketUrl);
Console.WriteLine("Connected to browser successfully");
return driver;
}
catch (Exception ex)
{
Console.WriteLine($"Connection failed: {ex.Message}");
throw;
}
}
private static async Task<string> CreateBiDiSessionAsync(string driverUrl)
{
using HttpClient client = new HttpClient();
client.Timeout = TimeSpan.FromSeconds(30);
using StringContent content = new StringContent(
"""{ "capabilities": { "alwaysMatch": { "webSocketUrl": true } } }""",
Encoding.UTF8,
"application/json");
using HttpResponseMessage response = await client.PostAsync($"{driverUrl}/session", content);
response.EnsureSuccessStatusCode();
using JsonDocument doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
return doc.RootElement.GetProperty("value").GetProperty("capabilities").GetProperty("webSocketUrl").GetString()
?? throw new Exception("webSocketUrl is null");
}
}
// Usage
BiDiDriver driver = await BrowserConnection.ConnectToBrowserAsync("http://localhost:9515");
Best Practices:
- Create the session through the driver and use its
webSocketUrl; never a/devtools/…URL - Include fallback logic for connection failures
- Handle HttpClient timeouts appropriately (the driver launches the browser while answering the new-session request)
Connection Methods
WebDriverBiDi.NET supports two ways to connect to browsers:
- WebSocket Connection (used in this guide): a driver executable or Firefox listens on a port, your application connects via WebSocket URL
- Pipe Connection: a Chromium browser communicates via anonymous pipes for lower latency (requires a BiDi-over-CDP mapper; see Browser Setup)
For getting started, WebSocket connections are recommended as they're simpler to configure and supported by most browsers. See Browser Setup for more details about connection methods.
Creating Your First Application
1. Create a New Console Application
dotnet new console -n MyFirstBiDiApp
cd MyFirstBiDiApp
dotnet add package WebDriverBiDi
2. Start Firefox
The application connects to Firefox, which speaks WebDriver BiDi natively, on its remote debugging port. Start it first:
firefox --remote-debugging-port=9222
3. Write the Code
Replace the contents of Program.cs with the code below, adding these using directives at the top of the file:
using WebDriverBiDi;
using WebDriverBiDi.BrowsingContext;
using WebDriverBiDi.Script;
using WebDriverBiDi.Session;
// Create a driver with a 10-second command timeout
BiDiDriver driver = new BiDiDriver(TimeSpan.FromSeconds(10));
try
{
// Connect to Firefox, started with --remote-debugging-port=9222, and create a session
Console.WriteLine("Connecting to browser...");
await driver.StartAsync("ws://localhost:9222/session");
await driver.Session.NewSessionAsync(new NewCommandParameters());
Console.WriteLine("Connected!");
// Get the current browsing contexts (tabs/windows)
GetTreeCommandResult tree = await driver.BrowsingContext.GetTreeAsync(
new GetTreeCommandParameters());
string contextId = tree.ContextTree[0].BrowsingContextId;
Console.WriteLine($"Active context ID: {contextId}");
// Navigate to a webpage
Console.WriteLine("Navigating to example.com...");
NavigateCommandParameters navParams = new NavigateCommandParameters(
contextId,
"https://example.com")
{
Wait = ReadinessState.Complete
};
NavigateCommandResult navResult = await driver.BrowsingContext.NavigateAsync(navParams);
Console.WriteLine($"Navigation complete! URL: {navResult.Url}");
// Execute JavaScript to get the page title
EvaluateCommandParameters evalParams = new EvaluateCommandParameters(
"document.title",
new ContextTarget(contextId),
true);
EvaluateResult scriptResult = await driver.Script.EvaluateAsync(evalParams);
if (scriptResult is EvaluateResultSuccess success &&
success.Result is StringRemoteValue stringValue)
{
string title = stringValue.Value ?? "No title";
Console.WriteLine($"Page title: {title}");
}
Console.WriteLine("Press any key to close...");
Console.ReadKey();
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
finally
{
// Disconnect from the browser
await driver.StopAsync();
Console.WriteLine("Disconnected from browser");
}
4. Run the Application
dotnet run
Understanding the Code
Let's break down what this code does:
Creating the Driver
BiDiDriver driver = new BiDiDriver(TimeSpan.FromSeconds(10));
The BiDiDriver is the main entry point for all WebDriver BiDi operations. The timeout parameter specifies how long to wait for command responses.
Tip: By default, event handler exceptions and problems with messages from the browser never throw. They are reported only through the driver's diagnostic events, which nothing may be watching. During development, set the error behaviors to
TransportErrorBehavior.Terminateso that problems surface as exceptions: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;See Error Handling for a full explanation of the four error behavior properties and the recommended settings for production use.
Connecting to the Browser
await driver.StartAsync("ws://localhost:9222/session");
await driver.Session.NewSessionAsync(new NewCommandParameters());
This connects to the WebDriver BiDi endpoint Firefox serves on its remote debugging port. No session exists there yet, so one is created. (A webSocketUrl returned by a driver executable's new-session request already has a session; see Browser Setup.)
Getting the Browsing Context
GetTreeCommandResult tree = await driver.BrowsingContext.GetTreeAsync(
new GetTreeCommandParameters());
string contextId = tree.ContextTree[0].BrowsingContextId;
return contextId;
A browsing context represents a tab, window, or iframe. You need the context ID to perform operations like navigation or script execution.
Navigating
NavigateCommandParameters navParams = new NavigateCommandParameters(
contextId,
"https://example.com")
{
Wait = ReadinessState.Complete
};
await driver.BrowsingContext.NavigateAsync(navParams);
The Wait property controls when the command returns:
ReadinessState.None: Returns once the navigation is committed, without waiting for the document to loadReadinessState.Interactive: Waits for DOM readyReadinessState.Complete: Waits for page load complete (including images, stylesheets)
Executing JavaScript
EvaluateCommandParameters evalParams = new EvaluateCommandParameters(
"document.title",
new ContextTarget(contextId),
true);
EvaluateResult scriptResult = await driver.Script.EvaluateAsync(evalParams);
The third parameter (true) indicates whether to await promises in the JavaScript code.
Next Steps
Now that you have a working WebDriverBiDi.NET application, explore these topics:
- Core Concepts: Understand modules, commands, and events
- Browser Setup: Learn about connection types and browser configuration
- Architecture: Understand the library's design and connection architecture
- Browser Module: Learn about browser-level operations
- Events and Observables: Handle browser events asynchronously
- Common Scenarios: See practical examples
Troubleshooting
"Could not connect to remote WebSocket server" Error
StartAsync retries a refused or failed connection every 500 milliseconds until the startup timeout (10 seconds by
default) runs out, then throws WebDriverBiDiTimeoutException. When that happens:
- Ensure the driver executable (or Firefox with
--remote-debugging-port) is running and, for a driver, that the session was created - Verify the URL is the session's
webSocketUrl(or Firefox's/session), not a/devtools/…CDP URL - Check that no firewall is blocking the connection
"Timed out executing command" Error
The command did not complete within its timeout, so it failed with WebDriverBiDiTimeoutException.
- Increase the timeout when creating the
BiDiDriver, or override for specific commands using thetimeoutOverrideparameter on module methods (e.g.,NavigateAsync(parameters, TimeSpan.FromSeconds(120))) - Check that the browser is responsive
- Ensure the command parameters are valid
"unknown command" Error
A command the browser does not implement fails with a WebDriverBiDiCommandException whose ErrorCode is
UnknownCommand.
- Verify that your browser supports the specific module
- Some modules are experimental and may require specific browser flags