Working with Remote Values
Remote values represent JavaScript data returned from the browser. This guide explains how to work with them effectively.
Overview
When you execute JavaScript in the browser, the results are returned as RemoteValue objects. These represent JavaScript values in a .NET-friendly way while preserving type information and references to browser objects.
RemoteValue Class Hierarchy
Every RemoteValue has a Type property indicating the JavaScript type. It is a RemoteValueType enumeration value, not a string, so compare it against enum members:
if (remoteValue.Type == RemoteValueType.Node)
{
// ...
}
Each enum member corresponds to the protocol's wire value for that type, which is the member name in lower case (RemoteValueType.HtmlCollection is sent as "htmlcollection"). The tables below list those wire values; the equivalent enum member is the same word in Pascal case.
The library deserializes each value into a concrete subclass rather than a single generic type:
RemoteValue(abstract base) – all remote values exposeType,As<T>(),TryAs<T>(), andToLocalValue()ValueHoldingRemoteValue<T>– subclass for values that carry a .NET payload; exposes a typedValuepropertyObjectReferenceRemoteValue– subclass for JavaScript objects that can be referenced by handle; exposesHandleandInternalId, andToRemoteObjectReference()to build a referenceNodeRemoteValue– derives directly fromRemoteValue, exposes aNodeProperties? Value(viaITypeSafeRemoteValue<NodeProperties?>), and implementsIObjectReferenceRemoteValue, providingSharedId,ToSharedReference()andToRemoteObjectReference()
NullRemoteValue and UndefinedRemoteValue have no Value property — only a Type.
JavaScript Type Mapping
Primitive Types
| JavaScript Type | Protocol type value |
Concrete Class | Value Property Type |
|---|---|---|---|
string |
"string" |
StringRemoteValue |
string |
number |
"number" |
NumberRemoteValue |
double |
boolean |
"boolean" |
BooleanRemoteValue |
bool |
bigint |
"bigint" |
BigIntegerRemoteValue |
BigInteger |
undefined |
"undefined" |
UndefinedRemoteValue |
(none) |
null |
"null" |
NullRemoteValue |
(none) |
Complex Types
| JavaScript Type | Protocol type value |
Concrete Class | Value Property Type |
|---|---|---|---|
Object |
"object" |
KeyValuePairCollectionRemoteValue |
RemoteValueDictionary? |
Map |
"map" |
KeyValuePairCollectionRemoteValue |
RemoteValueDictionary? |
Array |
"array" |
CollectionRemoteValue |
RemoteValueList? |
Set |
"set" |
CollectionRemoteValue |
RemoteValueList? |
NodeList |
"nodelist" |
CollectionRemoteValue |
RemoteValueList? |
HTMLCollection |
"htmlcollection" |
CollectionRemoteValue |
RemoteValueList? |
Date |
"date" |
DateRemoteValue |
DateTime |
RegExp |
"regexp" |
RegExpRemoteValue |
RegularExpressionValue |
DOM Element |
"node" |
NodeRemoteValue |
NodeProperties? |
Window |
"window" |
WindowProxyRemoteValue |
WindowProxyProperties |
Function, Promise, etc. |
various | ObjectReferenceRemoteValue |
(none; use Handle) |
The collection and node Value properties are nullable because the remote end omits a value's contents in three cases. A collection's Value is null when the object depth limit was reached (SerializationOptions.MaxObjectDepth); a collection's or node's Value is null when the same object already appears earlier in the result, the repeat carrying only an InternalId equal to that of the earlier, full serialization; and a platform object, such as navigator or localStorage, is serialized without its contents whatever the depth limit allows. Check for null before reading these properties. For a node, NodeRemoteValue.GetNodeProperties() returns the properties or throws WebDriverBiDiException when they are absent.
RemoteValueDictionary is a read-only dictionary mapping keys to RemoteValue instances. Use dict[key].As<SpecificType>().Value to extract values. String keys are compared by value; keys that are themselves RemoteValue objects (JavaScript Map entries keyed by objects) are compared by reference, because each denotes a distinct object on the remote end even when two serialize identically — enumerate the dictionary to read those entries. RemoteValueList is a read-only collection of RemoteValue instances. Use list[index].As<SpecificType>().Value to extract elements.
Accessing Values
Using As() and Pattern Matching
Remote values are deserialized into their concrete types. Use C# pattern matching to check and cast in one step, or use As<T>() to cast directly (throwing if the type is wrong) and TryAs<T>() for a safe try-pattern:
EvaluateResult result = await driver.Script.EvaluateAsync(
new EvaluateCommandParameters("42", target, true));
if (result is EvaluateResultSuccess success)
{
RemoteValue remoteValue = success.Result;
// Convert to long
long number = remoteValue.As<NumberRemoteValue>();
Console.WriteLine(number); // 42
// Can also convert to double
double doubleNumber = remoteValue.As<NumberRemoteValue>();
Console.WriteLine(doubleNumber); // 42, because a whole double prints without a decimal point
}
String Values
EvaluateResult result = await driver.Script.EvaluateAsync(
new EvaluateCommandParameters("'Hello, World!'", target, true));
if (result is EvaluateResultSuccess success &&
success.Result is StringRemoteValue stringValue)
{
string text = stringValue.Value;
Console.WriteLine(text); // "Hello, World!"
}
Boolean Values
EvaluateResult result = await driver.Script.EvaluateAsync(
new EvaluateCommandParameters("true", target, true));
if (result is EvaluateResultSuccess success &&
success.Result is BooleanRemoteValue booleanValue)
{
bool flag = booleanValue.Value;
Console.WriteLine(flag); // True
}
Null and Undefined
EvaluateResult result = await driver.Script.EvaluateAsync(
new EvaluateCommandParameters("null", target, true));
if (result is EvaluateResultSuccess success)
{
if (success.Result.Type == RemoteValueType.Null || success.Result.Type == RemoteValueType.Undefined)
{
Console.WriteLine("Value is null or undefined");
}
}
Working with Objects
Simple Objects
string script = """
({
name: 'John',
age: 30,
active: true
})
""";
EvaluateResult result = await driver.Script.EvaluateAsync(
new EvaluateCommandParameters(script, target, true));
if (result is EvaluateResultSuccess success)
{
KeyValuePairCollectionRemoteValue obj = success.Result.As<KeyValuePairCollectionRemoteValue>();
// Convert to RemoteValueDictionary; extract values with As<T>(). Value is null when
// the remote end sent the object without its contents, so test before reading it.
if (obj.Value is RemoteValueDictionary dict)
{
Console.WriteLine(dict["name"].As<StringRemoteValue>().Value); // "John"
Console.WriteLine(dict["age"].As<NumberRemoteValue>().Value); // 30
Console.WriteLine(dict["active"].As<BooleanRemoteValue>().Value); // True
}
}
Nested Objects
string script = """
({
user: {
name: 'John',
address: {
city: 'New York',
zip: '10001'
}
}
})
""";
EvaluateResult result = await driver.Script.EvaluateAsync(
new EvaluateCommandParameters(script, target, true));
// Each level is only present if the remote end serialized it, which it stops doing at the
// object depth limit, so each Value is tested as it is unwrapped.
if (result is EvaluateResultSuccess success &&
success.Result is KeyValuePairCollectionRemoteValue dictionaryValue &&
dictionaryValue.Value is RemoteValueDictionary dict &&
dict["user"].As<KeyValuePairCollectionRemoteValue>().Value is RemoteValueDictionary user &&
user["address"].As<KeyValuePairCollectionRemoteValue>().Value is RemoteValueDictionary address)
{
Console.WriteLine(address["city"].As<StringRemoteValue>().Value); // "New York"
}
Working with Arrays
string script = "[1, 2, 3, 4, 5]";
EvaluateResult result = await driver.Script.EvaluateAsync(
new EvaluateCommandParameters(script, target, true));
if (result is EvaluateResultSuccess success &&
success.Result is CollectionRemoteValue listValue &&
listValue.Value is RemoteValueList list)
{
Console.WriteLine($"Length: {list.Count}"); // 5
foreach (RemoteValue item in list)
{
Console.WriteLine(item.As<NumberRemoteValue>().Value);
}
}
Array of Objects
string script = """
[
{ name: 'Alice', age: 25 },
{ name: 'Bob', age: 30 },
{ name: 'Charlie', age: 35 }
]
""";
EvaluateResult result = await driver.Script.EvaluateAsync(
new EvaluateCommandParameters(script, target, true));
if (result is EvaluateResultSuccess success &&
success.Result is CollectionRemoteValue listValue &&
listValue.Value is RemoteValueList list)
{
foreach (RemoteValue item in list)
{
if (item.As<KeyValuePairCollectionRemoteValue>().Value is RemoteValueDictionary person)
{
Console.WriteLine($"{person["name"].As<StringRemoteValue>().Value}, age {person["age"].As<NumberRemoteValue>().Value}");
}
}
}
Converting to Dictionary or List
When you need a flattened Dictionary<string, object> or List<object> (for example, for serialization or interoperability), use a recursive helper:
static object? ToObject(RemoteValue value)
{
return value.Type switch
{
RemoteValueType.String => value.As<StringRemoteValue>().Value,
RemoteValueType.Number => value.As<NumberRemoteValue>().Value,
RemoteValueType.Boolean => value.As<BooleanRemoteValue>().Value,
RemoteValueType.Null or RemoteValueType.Undefined => null,
// Value is null when the remote end sent the object or array by reference, without its
// contents: a platform object, or a value beyond the serialization depth.
RemoteValueType.Object or RemoteValueType.Map => value.As<KeyValuePairCollectionRemoteValue>().Value?
.ToDictionary(kvp => kvp.Key.ToString() ?? "", kvp => ToObject(kvp.Value)),
RemoteValueType.Array or RemoteValueType.Set => value.As<CollectionRemoteValue>().Value?
.Select(ToObject)
.ToList(),
_ => (value as ValueHoldingRemoteValue)?.ValueObject ?? "(object)"
};
}
// Usage: convert RemoteValueDictionary to Dictionary<string, object>
if (success.Result.As<KeyValuePairCollectionRemoteValue>().Value is RemoteValueDictionary dict)
{
Dictionary<string, object?> flat = dict.ToDictionary(
kvp => kvp.Key.ToString() ?? "",
kvp => ToObject(kvp.Value));
}
Working with DOM Elements
DOM elements are special remote values with type "node".
Getting Element Information
EvaluateResult result = await driver.Script.EvaluateAsync(
new EvaluateCommandParameters(
"document.querySelector('button')",
target,
true));
if (result is EvaluateResultSuccess success)
{
RemoteValue elementRemoteValue = success.Result;
if (!elementRemoteValue.TryAs(out NodeRemoteValue? element))
{
return;
}
Console.WriteLine($"Type: {element.Type}"); // Node
Console.WriteLine($"SharedId: {element.SharedId}");
// Get node properties; this throws if the remote end sent the node without them
NodeProperties nodeProps = element.GetNodeProperties();
Console.WriteLine($"Tag: {nodeProps.LocalName}");
Console.WriteLine($"Node Type: {nodeProps.NodeType}");
Console.WriteLine($"Child Count: {nodeProps.ChildNodeCount}");
// Get attributes
if (nodeProps.Attributes != null)
{
foreach (var attr in nodeProps.Attributes)
{
Console.WriteLine($" {attr.Key} = {attr.Value}");
}
}
}
Using Element References
Elements can be passed to subsequent script calls:
// Get element
EvaluateResult getResult = await driver.Script.EvaluateAsync(
new EvaluateCommandParameters(
"document.querySelector('button')",
target,
true));
if (getResult is EvaluateResultSuccess getSuccess)
{
if (!getSuccess.Result.TryAs(out NodeRemoteValue? element))
{
return;
}
// Create a reference
SharedReference elementRef = element.ToSharedReference();
// Use 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.
Primitive Local Values
CallFunctionCommandParameters parameters = new CallFunctionCommandParameters(
"(str, num, bool) => console.log(str, num, bool)",
target,
false);
parameters.Arguments.Add(LocalValue.String("Hello"));
parameters.Arguments.Add(LocalValue.Number(42));
parameters.Arguments.Add(LocalValue.Boolean(true));
await driver.Script.CallFunctionAsync(parameters);
Special Values
parameters.Arguments.Add(LocalValue.Null);
parameters.Arguments.Add(LocalValue.Undefined);
// Special numbers
parameters.Arguments.Add(LocalValue.Number(double.PositiveInfinity));
parameters.Arguments.Add(LocalValue.Number(double.NegativeInfinity));
parameters.Arguments.Add(LocalValue.Number(double.NaN));
Object Local Values
Dictionary<string, LocalValue> obj = new Dictionary<string, LocalValue>
{
{ "name", LocalValue.String("John") },
{ "age", LocalValue.Number(30) },
{ "active", LocalValue.Boolean(true) },
{ "metadata", LocalValue.Object(new Dictionary<string, LocalValue>
{
{ "created", LocalValue.Date(DateTime.Now) }
})
}
};
parameters.Arguments.Add(LocalValue.Object(obj));
Array Local Values
List<LocalValue> array = new List<LocalValue>
{
LocalValue.Number(1),
LocalValue.Number(2),
LocalValue.Number(3)
};
parameters.Arguments.Add(LocalValue.Array(array));
Date Values
parameters.Arguments.Add(LocalValue.Date(DateTime.Now));
parameters.Arguments.Add(LocalValue.Date(new DateTime(2024, 1, 1)));
Regular Expression Values
parameters.Arguments.Add(LocalValue.RegExp("\\d+", "g"));
parameters.Arguments.Add(LocalValue.RegExp("[a-z]+", "i"));
Type Checking Patterns
Safe Type Conversion
RemoteValue value = success.Result;
switch (value.Type)
{
case RemoteValueType.String:
string str = value.As<StringRemoteValue>().Value;
break;
case RemoteValueType.Number:
double num = value.As<NumberRemoteValue>().Value;
break;
case RemoteValueType.Boolean:
bool flag = value.As<BooleanRemoteValue>().Value;
break;
case RemoteValueType.Object:
// Null when the remote end sent the object without its contents
RemoteValueDictionary? obj = value.As<KeyValuePairCollectionRemoteValue>().Value;
break;
case RemoteValueType.Array:
RemoteValueList? list = value.As<CollectionRemoteValue>().Value;
break;
case RemoteValueType.Node:
NodeProperties? node = value.As<NodeRemoteValue>().Value;
break;
case RemoteValueType.Null:
case RemoteValueType.Undefined:
// Handle null/undefined
break;
}
Checking for Specific Types
// TryAs is the type test as well as the conversion; it returns false for any other type.
if (value.TryAs(out NodeRemoteValue? nodeValue))
{
// It's a DOM element
SharedReference elementRef = nodeValue.ToSharedReference();
}
else if (value.TryAs(out CollectionRemoteValue? listValue))
{
// It's an array, whose contents are null if the remote end omitted them
RemoteValueList? list = listValue.Value;
}
Common Patterns
Pattern: Extract Multiple Values
string script = """
({
title: document.title,
url: window.location.href,
linkCount: document.querySelectorAll('a').length,
ready: document.readyState === 'complete'
})
""";
EvaluateResult result = await driver.Script.EvaluateAsync(
new EvaluateCommandParameters(script, target, true));
if (result is EvaluateResultSuccess success &&
success.Result is KeyValuePairCollectionRemoteValue dictionaryValue &&
dictionaryValue.Value is RemoteValueDictionary data)
{
string title = data["title"].As<StringRemoteValue>().Value;
string url = data["url"].As<StringRemoteValue>().Value;
long linkCount = data["linkCount"].As<NumberRemoteValue>();
bool ready = data["ready"].As<BooleanRemoteValue>().Value;
}
Pattern: Element Collection
string script = "Array.from(document.querySelectorAll('a')).map(a => a.href)";
EvaluateResult result = await driver.Script.EvaluateAsync(
new EvaluateCommandParameters(script, target, true));
if (result is EvaluateResultSuccess success &&
success.Result.As<CollectionRemoteValue>().Value is RemoteValueList links)
{
foreach (RemoteValue link in links)
{
Console.WriteLine(link.As<StringRemoteValue>().Value);
}
}
Pattern: Round-trip Element
// Get element
EvaluateResult getResult = await driver.Script.EvaluateAsync(
new EvaluateCommandParameters(
"document.querySelector('.target')",
target,
true));
if (!getResult.As<EvaluateResultSuccess>().Result.TryAs(out NodeRemoteValue? element))
{
return;
}
// Get properties from element
CallFunctionCommandParameters propsParams = new CallFunctionCommandParameters(
"""
(element) => ({
tag: element.tagName,
text: element.textContent,
visible: element.offsetParent !== null
})
""",
target,
false);
propsParams.Arguments.Add(element.ToSharedReference());
EvaluateResult propsResult = await driver.Script.CallFunctionAsync(propsParams);
RemoteValueDictionary? props = propsResult.As<EvaluateResultSuccess>().Result
.As<KeyValuePairCollectionRemoteValue>().Value;
Best Practices
- Always check result type: Use pattern matching or type checks
- Handle null/undefined: Check for these types explicitly
- Use appropriate .NET types:
longfor integers,doublefor decimals - Cache element references: Store
SharedReferencefor reuse - Return structured data: Use objects to return multiple values
- Convert early: Get values into .NET types as soon as possible
Next Steps
- Script Module: Complete script execution guide
- Core Concepts: Understanding commands and responses
- Examples: Practical usage examples