API Design Guide
This guide documents the design principles and conventions used in WebDriverBiDi.NET. Understanding these patterns will help you use the library effectively and write idiomatic code.
Overview
WebDriverBiDi.NET is a low-level protocol client. The API design prioritizes:
- Explicit over implicit: Commands that can reset state require explicit parameters so intent is clear
- Consistency: All module commands follow the same pattern for timeouts and cancellation
- Protocol fidelity: The API closely mirrors the WebDriver BiDi protocol structure
Command Parameter Patterns
Required vs Optional Parameters
Module commands fall into two categories based on their CommandParameters:
Commands with optional parameters accept null or omit the parameters object. Use these when the command has no required properties and no "reset" capability:
// All equivalent—parameters optional
GetTreeCommandResult tree1 = await driver.BrowsingContext.GetTreeAsync(null);
GetTreeCommandResult tree2 = await driver.BrowsingContext.GetTreeAsync(new GetTreeCommandParameters());
StatusCommandResult status = await driver.Session.StatusAsync(null);
GetCookiesCommandResult cookies = await driver.Storage.GetCookiesAsync(null);
Commands with required parameters always require a parameters object, because the parameters type has required members. Some of them also expose a static "reset" property that clears a value on the remote end. Passing no parameters would be ambiguous—are you setting or resetting?
// ✅ Explicit reset
await driver.UserAgentClientHints.SetClientHintsOverrideAsync(
SetClientHintsOverrideCommandParameters.ResetClientHintsOverride);
// ✅ Explicit set
SetClientHintsOverrideCommandParameters setParams = new SetClientHintsOverrideCommandParameters();
setParams.ClientHints = new ClientHintsMetadata { /* ... */ };
await driver.UserAgentClientHints.SetClientHintsOverrideAsync(setParams);
Complete Lists
| Optional Parameters | Required Parameters (Reset Property) |
|---|---|
Browser.CloseAsync |
UserAgentClientHints.SetClientHintsOverrideAsync |
Browser.CreateUserContextAsync |
Browser.SetDownloadBehaviorAsync |
Browser.GetClientWindowsAsync |
BrowsingContext.SetBypassCSPAsync |
Browser.GetUserContextsAsync |
BrowsingContext.SetViewportAsync |
BrowsingContext.GetTreeAsync |
Network.SetExtraHeadersAsync |
Script.GetRealmsAsync |
All Emulation Set*OverrideAsync commands |
Emulation.SetNetworkConditionsAsync |
|
Emulation.SetScriptingEnabledAsync |
|
Session.EndAsync |
|
Session.NewSessionAsync |
|
Session.StatusAsync |
|
Storage.DeleteCookiesAsync |
|
Storage.GetCookiesAsync |
Reset Property Variants
There are two distinct patterns for reset helpers on CommandParameters classes. Understanding the difference is important when reading XML documentation and when working with BIDI014.
Command-level reset
The static property returns a pre-configured instance of the CommandParameters class itself. You pass it directly to the command method. This is the most common pattern.
// ResetTimeZoneOverride returns a SetTimeZoneOverrideCommandParameters instance
await driver.Emulation.SetTimeZoneOverrideAsync(
SetTimeZoneOverrideCommandParameters.ResetTimeZoneOverride);
BIDI014 detects when you use new SomeCommandParameters() without setting properties and the class has a command-level reset property, since that is almost certainly a mistake.
Property-level sentinel
The static property returns a typed value to assign to a specific property on the CommandParameters object. When the property is serialized, the sentinel value is written as JSON null, instructing the remote end to reset that individual field.
SetViewportCommandParameters uses this pattern because viewport dimensions and device pixel ratio can each be reset independently:
// Reset viewport only — leave device pixel ratio unchanged
await driver.BrowsingContext.SetViewportAsync(
new SetViewportCommandParameters
{
Viewport = SetViewportCommandParameters.ResetToDefaultViewport,
});
// Reset device pixel ratio only — leave viewport dimensions unchanged
await driver.BrowsingContext.SetViewportAsync(
new SetViewportCommandParameters
{
DevicePixelRatio = SetViewportCommandParameters.ResetToDefaultDevicePixelRatio,
});
Assigning C# null to either property omits it from the JSON payload entirely, leaving the current value on the remote end unchanged. The sentinel is the only way to emit an explicit JSON null for these fields.
BIDI014 does not apply to property-level sentinel classes. Using new SetViewportCommandParameters() without any properties is valid — it sends a command that leaves both viewport and device pixel ratio at their current values.
Optional List Properties
Optional lists follow the cardinality the protocol gives them. The rules apply to every type serialized into a command payload, not only to the CommandParameters roots: a list nested inside a sent object (CapabilitiesRequest.FirstMatch, ScanRecord.UUIDs, ClientHintsMetadata.Brands) follows the same rules as one declared on the parameters type itself.
- Optional lists (
Contexts,UserContexts,StartNodes,Arguments,UrlPatterns,PageRanges, ...): the property is read-only and always initialized (List<string> Contexts { get; }). Populate it with a collection initializer or.Add(). While empty it is omitted from the JSON payload; an empty array is never sent. Where the CDDL is[+x]the browser would reject an empty array; where it is[*x]the specification treats an absent field and an empty array identically. - The exception —
HeadersandCookiesonContinueRequest,ContinueResponseandProvideResponse, andBrandsandFullVersionListonClientHintsMetadata: the property is nullable and settable, because the protocol gives a present-but-empty array a distinct meaning.FormFactorsonClientHintsMetadatashares that shape for parity with its two siblings, although the specification's client hints emulation does not yet use it. For the network lists,[]replaces the headers or cookies with none while omission keeps the originals; for the client hints,[]overrides the browser's own value with an empty one while omission leaves the browser's value in place.nullomits the property; an empty list sends[]. - Required lists (
Phases,Actions,Handles,HeadersonSetExtraHeaders, ...): read-only and always initialized, never settable.
Always check the XML documentation on the property for the rationale. See Core Concepts - Command Parameters for details.
Protocol Extensions via AdditionalData
The WebDriver BiDi protocol allows implementations to support additional command properties beyond the specification. The AdditionalData dictionary on CommandParameters lets you inject these extra fields into the JSON payload while keeping the strongly-typed API for standard parameters.
Use AdditionalData when:
- A browser or driver supports a pre-standard or vendor-specific parameter
- You need to pass extension data that the library does not yet model as a typed property
- You are integrating with a custom BiDi implementation that expects extra fields
Entries in AdditionalData are serialized as additional properties inside the params object of the command message, alongside the command's typed parameters. They do not appear at the envelope level next to id, method, and params. Values must be JSON-serializable (strings, numbers, booleans, null, arrays, or dictionaries).
For example, adding parameters.AdditionalData["customOption"] = "customValue" to a navigate command produces:
{
"id": 1,
"method": "browsingContext.navigate",
"params": {
"context": "...",
"url": "https://example.com",
"customOption": "customValue"
}
}
If you need extra properties at the envelope level (a sibling of id, method, and params), override Transport.CreateCommand in a custom transport and populate Command.AdditionalCommandProperties on the Command it returns. See Custom Modules — Custom Transport for how to supply a custom Transport to BiDiDriver.
An extension entry may not reuse a property name its object already writes. That holds for the envelope, where id, method and params are reserved, for the parameters root, and for every nested object with its own dictionary, such as PartialCookie.AdditionalData or CapabilityRequest.AdditionalCapabilities. It also holds for your own types sent through a registered resolver. Sending such a command throws WebDriverBiDiSerializationException naming the command and the entry, instead of emitting a JSON object with a duplicate property name. Set the typed property instead. The BIDI033 analyzer reports an entry with a constant name that collides this way before the code runs.
NavigateCommandParameters parameters = new NavigateCommandParameters(contextId, "https://example.com");
// Add vendor-specific or pre-standard extension fields
parameters.AdditionalData["customOption"] = "customValue";
parameters.AdditionalData["experimentalFlag"] = true;
await driver.BrowsingContext.NavigateAsync(parameters);
Reading vendor extension data
Received messages mirror the two outbound positions, and add a third for nested objects. Extension properties are exposed where they were found and are never merged across positions:
| Position | Outbound | Command response | Event |
|---|---|---|---|
Envelope — beside type, id/method, result/params |
Command.AdditionalCommandProperties |
CommandResult.AdditionalResponseProperties |
event args AdditionalEventProperties |
Payload root — beside the specified members of result/params |
CommandParameters.AdditionalData |
CommandResult.AdditionalData |
event args AdditionalData |
| Nested object | e.g. CookieFilter.AdditionalData |
e.g. Cookie.AdditionalData |
e.g. RequestData.AdditionalData |
An error response has no result object: its specified members, and any extension members, are all on the envelope. Every extension member of an error therefore appears in ErrorResult.AdditionalResponseProperties, and ErrorResult.AdditionalData is always empty. A consumer reading a vendor member from AdditionalResponseProperties finds it whether the command succeeded or failed.
The envelope and payload-root positions are captured by the transport for every command result and
event — built-in or custom, under reflection or native AOT — with no attribute on the type: any property
of the result/params object that the type does not define is extension data. (A member marked
[JsonIgnore] does not define a wire property, so a same-named property still counts as extension data.)
Chromium, for example, echoes a subscription's goog:channel on the envelope. Nested objects that the
protocol marks Extensible capture their own: Cookie, CapabilitiesResult (as AdditionalCapabilities) and the
ProxyConfigurationResult it carries as Proxy, the storage partition types, and SharedReferenceInfo (the element of input.fileDialogOpened, whose
ToSharedReference() carries the properties back to the remote end). RequestData and ResponseData capture
theirs too, although the specification does not mark them Extensible, because Chromium places members there in
practice: goog:postData, goog:hasPostData, goog:resourceType, goog:resourceInitiator and
goog:securityDetails.
Values are exposed as ReceivedDataDictionary entries: strings, bool, long or double numbers, nested
ReceivedDataDictionary objects and ReceivedDataList arrays, or null.
// Chromium adds goog:-prefixed properties inside the request and response objects.
using EventObserver<BeforeRequestSentEventArgs> observer = driver.Network.OnBeforeRequestSent.AddObserver(e =>
{
if (e.Request.AdditionalData.TryGetValue("goog:resourceType", out object? resourceType))
{
Console.WriteLine($"Resource type: {resourceType}");
}
});
// Every result exposes the two positions separately: properties inside the result object
// (AdditionalData) and properties on the response envelope (AdditionalResponseProperties).
ReleaseActionsCommandResult result = await driver.Input.ReleaseActionsAsync(new ReleaseActionsCommandParameters(contextId));
foreach (KeyValuePair<string, object?> extension in result.AdditionalData)
{
Console.WriteLine($"result.{extension.Key} = {extension.Value}");
}
if (result.AdditionalResponseProperties.TryGetValue("goog:channel", out object? channel))
{
Console.WriteLine($"Envelope channel: {channel}");
}
Note: The remote end must support the extension fields you send. Sending unknown properties may be ignored or cause an error depending on the implementation. Consult the protocol specification or your browser/driver documentation for supported extensions.
AOT and trimming: Because AdditionalData is typed as Dictionary<string, object?>, values stored in it are serialized through reflection-based JsonSerializer overloads rather than the source-generated context. This is not compatible with native AOT or IL trimming unless every value's runtime type is registered via BiDiDriver.RegisterTypeInfoResolverAsync before the command is sent. The BIDI022 analyzer flags every write to AdditionalData as a reminder. See AOT Compatibility for the pattern.
Timeout and Cancellation
Every module command accepts two optional parameters. This is the preferred way to set per-command timeouts when using the module API (e.g., driver.BrowsingContext.NavigateAsync). Prefer this over ExecuteCommandAsync when you need per-command timeout control:
Task<T> CommandAsync(
CommandParameters? parameters,
TimeSpan? timeoutOverride = null,
CancellationToken cancellationToken = default)
timeoutOverride: Whennull, the driver usesBiDiDriver.DefaultCommandTimeout, the timeout the driver was constructed with, which isBiDiDriver.DefaultCommandWaitTimeout(60 seconds) when none was given. Pass a value to override for long-running or quick-fail scenarios.cancellationToken: Propagates cancellation. Use for cooperative cancellation (e.g., user cancel, test timeout).
// Use default timeout
await driver.BrowsingContext.NavigateAsync(navParams);
// Override timeout for quick operation
await driver.Session.StatusAsync(null, timeoutOverride: TimeSpan.FromSeconds(5));
// With cancellation
using CancellationTokenSource cts = new(TimeSpan.FromSeconds(30));
await driver.BrowsingContext.NavigateAsync(navParams, cancellationToken: cts.Token);
For timeout patterns (e.g., returning null instead of throwing) and connection-level timeout configuration, see Error Handling - Timeout Handling.
Error Handling Configuration
The library uses TransportErrorBehavior (Ignore, Collect, Terminate) to control how transport-level errors are handled. Four properties on ITransportConfiguration, reached through BiDiDriver.TransportConfiguration, provide fine-grained control:
| Property | Default | Controls |
|---|---|---|
EventHandlerExceptionBehavior |
Ignore | Exceptions thrown by event handlers |
ProtocolErrorBehavior |
Ignore | An error response or registered event whose payload cannot be deserialized; an unexpected failure while processing a message |
UnknownMessageBehavior |
Ignore | A message that is not valid JSON, or not a command response, error response or registered event |
UnexpectedErrorBehavior |
Ignore | Error response with no corresponding command |
See Error Handling for detailed guidance on when to use each mode.
Versioning and Compatibility
Package Versioning
WebDriverBiDi.NET uses Semantic Versioning (SemVer) version numbers, and is currently in the 0.x series. SemVer makes no compatibility promise for major version zero, and this project does not make one either: while the major version is 0, any release — including a patch increment — may change or remove public API. Removals have already shipped in patch releases (for example, analyzer rule BIDI018 was removed in 0.0.48 and BIDI011/BIDI019 in 0.0.51). Pin an exact package version, and review what changed before updating. The project publishes no GitHub releases and keeps no changelog file, so the commit history and the pull request descriptions are the record.
Once the package reaches 1.0, the usual SemVer contract applies:
- Major: Breaking API changes
- Minor: New features, backward compatible
- Patch: Bug fixes, backward compatible
Framework Support
The main library multi-targets netstandard2.0, net8.0, net9.0, and net10.0. The .NET Standard 2.0 target ensures compatibility with:
- .NET Framework 4.6.2+
- .NET 8, 9, 10
Projects targeting .NET 8 or later bind to the corresponding assembly, which is the build marked IsAotCompatible and therefore the one that supports trimming and native AOT publishing. The published API reference is generated from the netstandard2.0 assembly.
Protocol Compatibility
The WebDriver BiDi protocol is evolving. The library defaults to TransportErrorBehavior.Ignore for protocol errors and unknown messages to support:
- Forward compatibility: Older library versions working with newer browsers that send new message types
- Graceful degradation: Automation continuing when protocol versions diverge slightly
When strict conformance is required (e.g., production with known protocol versions), consider ProtocolErrorBehavior.Terminate and UnknownMessageBehavior.Terminate.
Breaking Changes
While the major version is 0, a breaking change may appear in any release, and there is no separate document announcing it: compare the commit history between the version you are on and the one you are moving to, and read the pull request descriptions. Look in particular for:
- Removed or renamed types and members
- Changed method signatures
- Changed default behavior
- Removed analyzer rules, which stop reporting rather than failing the build
IObservable<T> Integration
Any ObservableEvent<T> can be adapted to the standard BCL IObservable<T> interface via the ToObservable() extension method. This enables integration with Reactive Extensions (Rx) operators and any code that consumes IObservable<T>/IObserver<T>.
IDisposable subscription = driver.Network.OnBeforeRequestSent
.ToObservable()
.Subscribe(new MyObserver());
The adapter only partially satisfies the full Rx push-stream contract. Be aware of these differences:
OnCompletedis called only after the subscription handle returned bySubscribeis disposed and the internal buffer drains. It is not called when theBiDiDriveris stopped or disposed — dispose the subscription handle explicitly to trigger completion.OnErroris called ifOnNextthrows. It is not called for transport errors or exceptions thrown by other observers on the same event.- Each
Subscribecall creates an independent buffered subscription that counts as one observer againstObservableEvent<T>.MaxObserverCount. Dispose the returned handle when done to avoid resource leaks. The handle is anObservableEventSubscription<T>, whoseCompletionTaskcompletes once delivery to the observer has ended.
For full details, code samples, and Rx operator usage, see Events and Observables — IObservable<T> Support.
Related Documentation
- Core Concepts: Command parameters, events, lifecycle
- Error Handling: TransportErrorBehavior, exception handling, timeout patterns, troubleshooting
- Architecture: Transport, connection, error configuration
- Events and Observables: Observer pattern, data collectors, IObservable<T> integration
- Quick Reference: Common commands at a glance