Table of Contents

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 expose Type, As<T>(), TryAs<T>(), and ToLocalValue()
  • ValueHoldingRemoteValue<T> – subclass for values that carry a .NET payload; exposes a typed Value property
  • ObjectReferenceRemoteValue – subclass for JavaScript objects that can be referenced by handle; exposes Handle and InternalId, and ToRemoteObjectReference() to build a reference
  • NodeRemoteValue – derives directly from RemoteValue, exposes a NodeProperties? Value (via ITypeSafeRemoteValue<NodeProperties?>), and implements IObjectReferenceRemoteValue, providing SharedId, ToSharedReference() and ToRemoteObjectReference()

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

  1. Always check result type: Use pattern matching or type checks
  2. Handle null/undefined: Check for these types explicitly
  3. Use appropriate .NET types: long for integers, double for decimals
  4. Cache element references: Store SharedReference for reuse
  5. Return structured data: Use objects to return multiple values
  6. Convert early: Get values into .NET types as soon as possible

Next Steps