Table of Contents

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.Terminate so 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.

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 load
  • ReadinessState.Interactive: Waits for DOM ready
  • ReadinessState.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:

  1. Core Concepts: Understand modules, commands, and events
  2. Browser Setup: Learn about connection types and browser configuration
  3. Architecture: Understand the library's design and connection architecture
  4. Browser Module: Learn about browser-level operations
  5. Events and Observables: Handle browser events asynchronously
  6. 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 the timeoutOverride parameter 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

Additional Resources