Repository: SeleniumHQ/selenium
Pull request #15532: [dotnet] [bidi] Add strongly-typed `LocalValue.ConvertFrom` overloads
Author: login: RenderMichael
Labels: name: Review effort 2/5
Created: 2025-03-28T18:34:28Z | Merged: 2025-04-13T15:22:10Z

### **User description**
### Description 
Add strongly-typed `LocalValue.ConvertFrom` overloads

### Motivation and Context

Prevents users from being forced to guess which types we happen to support.

### Types of changes
<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->
- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)

### Checklist
<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->
- [x] I have read the [contributing](https://github.com/SeleniumHQ/selenium/blob/trunk/CONTRIBUTING.md) document.
- [ ] My change requires a change to the documentation.
- [ ] I have updated the documentation accordingly.
- [ ] I have added tests to cover my changes.
- [x] All new and existing tests passed.
<!--- Provide a general summary of your changes in the Title above -->


___

### **PR Type**
Enhancement


___

### **Description**
- Added strongly-typed `LocalValue.ConvertFrom` overloads for better type safety.

- Refactored `ConvertFrom` logic to delegate to specific type overloads.

- Introduced new methods for handling various data structures and primitives.

- Improved code maintainability and readability by reducing duplication.


___



### **Changes walkthrough** 📝
<table><thead><tr><th></th><th align="left">Relevant files</th></tr></thead><tbody><tr><td><strong>Enhancement</strong></td><td><table>
<tr>
  <td>
    <details>
      <summary><strong>LocalValue.cs</strong><dd><code>Refactored and extended `LocalValue.ConvertFrom` functionality</code></dd></summary>
<hr>

dotnet/src/webdriver/BiDi/Modules/Script/LocalValue.cs

<li>Added strongly-typed overloads for <code>ConvertFrom</code> method.<br> <li> Refactored implicit operators to use <code>ConvertFrom</code> methods.<br> <li> Introduced specialized methods for handling primitives and <br>collections.<br> <li> Enhanced maintainability by reducing redundant code and improving <br>structure.


</details>


  </td>
  <td><a href="https://github.com/SeleniumHQ/selenium/pull/15532/files#diff-b2d3bd83230255db2e435ac34700a0af0595469211636058e6eb477107171b22">+219/-63</a></td>

</tr>
</table></td></tr></tr></tbody></table>

___

> <details> <summary>  Need help?</summary><li>Type <code>/help how to ...</code> in the comments thread for any questions about Qodo Merge usage.</li><li>Check out the <a href="https://qodo-merge-docs.qodo.ai/usage-guide/">documentation</a> for more information.</li></details>


Diff:
diff --git a/dotnet/src/webdriver/BiDi/Modules/Script/LocalValue.cs b/dotnet/src/webdriver/BiDi/Modules/Script/LocalValue.cs
index 19be05efaaf18..2e458c0377920 100644
--- a/dotnet/src/webdriver/BiDi/Modules/Script/LocalValue.cs
+++ b/dotnet/src/webdriver/BiDi/Modules/Script/LocalValue.cs
@@ -18,10 +18,12 @@
 // </copyright>
 
 using System;
+using System.Collections;
 using System.Collections.Generic;
 using System.Linq;
 using System.Numerics;
 using System.Text.Json.Serialization;
+using System.Text.RegularExpressions;
 
 namespace OpenQA.Selenium.BiDi.Modules.Script;
 
@@ -41,10 +43,11 @@ namespace OpenQA.Selenium.BiDi.Modules.Script;
 [JsonDerivedType(typeof(SetLocalValue), "set")]
 public abstract record LocalValue
 {
-    public static implicit operator LocalValue(bool? value) { return value is bool b ? new BooleanLocalValue(b) : new NullLocalValue(); }
-    public static implicit operator LocalValue(int? value) { return value is int i ? new NumberLocalValue(i) : new NullLocalValue(); }
-    public static implicit operator LocalValue(double? value) { return value is double d ? new NumberLocalValue(d) : new NullLocalValue(); }
-    public static implicit operator LocalValue(string? value) { return value is null ? new NullLocalValue() : new StringLocalValue(value); }
+    public static implicit operator LocalValue(bool? value) { return ConvertFrom(value); }
+    public static implicit operator LocalValue(int? value) { return ConvertFrom(value); }
+    public static implicit operator LocalValue(double? value) { return ConvertFrom(value); }
+    public static implicit operator LocalValue(string? value) { return ConvertFrom(value); }
+    public static implicit operator LocalValue(DateTimeOffset? value) { return ConvertFrom(value); }
 
     // TODO: Extend converting from types
     public static LocalValue ConvertFrom(object? value)
@@ -58,86 +61,222 @@ public static LocalValue ConvertFrom(object? value)
                 return new NullLocalValue();
 
             case bool b:
-                return new BooleanLocalValue(b);
+                return ConvertFrom(b);
 
             case int i:
-                return new NumberLocalValue(i);
+                return ConvertFrom(i);
 
             case double d:
-                return new NumberLocalValue(d);
+                return ConvertFrom(d);
 
             case long l:
-                return new NumberLocalValue(l);
+                return ConvertFrom(l);
 
-            case DateTime dt:
-                return new DateLocalValue(dt.ToString("o"));
+            case DateTimeOffset dt:
+                return ConvertFrom(dt);
 
             case BigInteger bigInt:
-                return new BigIntLocalValue(bigInt.ToString());
+                return ConvertFrom(bigInt);
 
             case string str:
-                return new StringLocalValue(str);
+                return ConvertFrom(str);
 
-            case IDictionary<string, string?> dictionary:
+            case Regex regex:
+                return ConvertFrom(regex);
+
+            case { } when value.GetType().GetInterfaces()
+                .Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ISet<>)):
                 {
-                    var bidiObject = new List<List<LocalValue>>(dictionary.Count);
-                    foreach (var item in dictionary)
-                    {
-                        bidiObject.Add([new StringLocalValue(item.Key), ConvertFrom(item.Value)]);
-                    }
+                    IEnumerable set = (IEnumerable)value;
 
-                    return new ObjectLocalValue(bidiObject);
-                }
+                    List<LocalValue> setValues = [];
 
-            case IDictionary<string, object?> dictionary:
-                {
-                    var bidiObject = new List<List<LocalValue>>(dictionary.Count);
-                    foreach (var item in dictionary)
+                    foreach (var obj in set)
                     {
-                        bidiObject.Add([new StringLocalValue(item.Key), ConvertFrom(item.Value)]);
+                        setValues.Add(ConvertFrom(obj));
                     }
 
-                    return new ObjectLocalValue(bidiObject);
+                    return new SetLocalValue(setValues);
                 }
 
-            case IDictionary<int, object?> dictionary:
-                {
-                    var bidiObject = new List<List<LocalValue>>(dictionary.Count);
-                    foreach (var item in dictionary)
-                    {
-                        bidiObject.Add([ConvertFrom(item.Key), ConvertFrom(item.Value)]);
-                    }
+            case IDictionary dictionary:
+                return ConvertFrom(dictionary);
 
-                    return new MapLocalValue(bidiObject);
-                }
+            case IEnumerable enumerable:
+                return ConvertFrom(enumerable);
 
-            case IEnumerable<object?> list:
-                return new ArrayLocalValue(list.Select(ConvertFrom).ToList());
+            default:
+                return ReflectionBasedConvertFrom(value);
+        }
+    }
 
-            case object:
-                {
-                    const System.Reflection.BindingFlags Flags = System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance;
+    public static LocalValue ConvertFrom(bool? value)
+    {
+        if (value is bool b)
+        {
+            return new BooleanLocalValue(b);
+        }
 
-                    var properties = value.GetType().GetProperties(Flags);
+        return new NullLocalValue();
+    }
 
-                    var values = new List<List<LocalValue>>(properties.Length);
-                    foreach (var property in properties)
-                    {
-                        object? propertyValue;
-                        try
-                        {
-                            propertyValue = property.GetValue(value);
-                        }
-                        catch (Exception ex)
-                        {
-                            throw new BiDiException($"Could not retrieve property {property.Name} from {property.DeclaringType}", ex);
-                        }
-                        values.Add([property.Name, ConvertFrom(propertyValue)]);
-                    }
+    public static LocalValue ConvertFrom(int? value)
+    {
+        if (value is int b)
+        {
+            return new NumberLocalValue(b);
+        }
 
-                    return new ObjectLocalValue(values);
-                }
+        return new NullLocalValue();
+    }
+
+    public static LocalValue ConvertFrom(double? value)
+    {
+        if (value is double b)
+        {
+            return new NumberLocalValue(b);
+        }
+
+        return new NullLocalValue();
+    }
+
+    public static LocalValue ConvertFrom(long? value)
+    {
+        if (value is long b)
+        {
+            return new NumberLocalValue(b);
+        }
+
+        return new NullLocalValue();
+    }
+
+    public static LocalValue ConvertFrom(string? value)
+    {
+        if (value is not null)
+        {
+            return new StringLocalValue(value);
+        }
+
+        return new NullLocalValue();
+    }
+
+    /// <summary>
+    /// Converts a .NET Regex into a BiDi Regex
+    /// </summary>
+    /// <param name="regex">A .NET Regex.</param>
+    /// <returns>A BiDi Regex.</returns>
+    /// <remarks>
+    /// Note that the .NET regular expression engine does not work the same as the JavaScript engine.
+    /// To minimize the differences between the two engines, it is recommended to enabled the <see cref="RegexOptions.ECMAScript"/> option.
+    /// </remarks>
+    public static LocalValue ConvertFrom(Regex? regex)
+    {
+        if (regex is null)
+        {
+            return new NullLocalValue();
         }
+
+        string? flags = RegExpValue.GetRegExpFlags(regex.Options);
+
+        return new RegExpLocalValue(new RegExpValue(regex.ToString()) { Flags = flags });
+    }
+
+    public static LocalValue ConvertFrom(DateTimeOffset? value)
+    {
+        if (value is null)
+        {
+            return new NullLocalValue();
+        }
+
+        return new DateLocalValue(value.Value.ToString("o"));
+    }
+
+    public static LocalValue ConvertFrom(BigInteger? value)
+    {
+        if (value is not null)
+        {
+            return new BigIntLocalValue(value.Value.ToString());
+        }
+
+        return new NullLocalValue();
+    }
+
+    public static LocalValue ConvertFrom(IEnumerable? value)
+    {
+        if (value is null)
+        {
+            return new NullLocalValue();
+        }
+
+        List<LocalValue> list = [];
+
+        foreach (var element in value)
+        {
+            list.Add(ConvertFrom(element));
+        }
+
+        return new ArrayLocalValue(list);
+    }
+
+    public static LocalValue ConvertFrom(IDictionary? value)
+    {
+        if (value is null)
+        {
+            return new NullLocalValue();
+        }
+
+        var bidiObject = new List<List<LocalValue>>(value.Count);
+
+        foreach (var key in value.Keys)
+        {
+            bidiObject.Add([ConvertFrom(key), ConvertFrom(value[key])]);
+        }
+
+        return new MapLocalValue(bidiObject);
+    }
+
+    public static LocalValue ConvertFrom<T>(ISet<T?>? value)
+    {
+        if (value is null)
+        {
+            return new NullLocalValue();
+        }
+
+        LocalValue[] convertedValues = [.. value.Select(x => ConvertFrom(x))];
+
+        return new SetLocalValue(convertedValues);
+    }
+
+    private static LocalValue ReflectionBasedConvertFrom(object? value)
+    {
+        if (value is null)
+        {
+            return new NullLocalValue();
+        }
+
+        const System.Reflection.BindingFlags Flags = System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance;
+
+        System.Reflection.PropertyInfo[] properties = value.GetType().GetProperties(Flags);
+
+        var values = new List<List<LocalValue>>(properties.Length);
+
+        foreach (System.Reflection.PropertyInfo? property in properties)
+        {
+            object? propertyValue;
+
+            try
+            {
+                propertyValue = property.GetValue(value);
+            }
+            catch (Exception ex)
+            {
+                throw new BiDiException($"Could not retrieve property {property.Name} from {property.DeclaringType}", ex);
+            }
+
+            values.Add([property.Name, ConvertFrom(propertyValue)]);
+        }
+
+        return new ObjectLocalValue(values);
     }
 }
 
diff --git a/dotnet/src/webdriver/BiDi/Modules/Script/RegExpValue.cs b/dotnet/src/webdriver/BiDi/Modules/Script/RegExpValue.cs
index 47a2239ab093d..eb25c091e8f91 100644
--- a/dotnet/src/webdriver/BiDi/Modules/Script/RegExpValue.cs
+++ b/dotnet/src/webdriver/BiDi/Modules/Script/RegExpValue.cs
@@ -17,9 +17,62 @@
 // under the License.
 // </copyright>
 
+using System;
+using System.Diagnostics;
+using System.Text.RegularExpressions;
+
 namespace OpenQA.Selenium.BiDi.Modules.Script;
 
 public record RegExpValue(string Pattern)
 {
     public string? Flags { get; set; }
+
+    internal static string? GetRegExpFlags(RegexOptions options)
+    {
+        if (options == RegexOptions.None)
+        {
+            return null;
+        }
+
+        string flags = string.Empty;
+        const RegexOptions NonBacktracking = (RegexOptions)1024;
+#if NET8_0_OR_GREATER
+        Debug.Assert(NonBacktracking == RegexOptions.NonBacktracking);
+#endif
+        const RegexOptions NonApplicableOptions = RegexOptions.Compiled | NonBacktracking;
+
+        const RegexOptions UnsupportedOptions =
+            RegexOptions.ExplicitCapture |
+            RegexOptions.IgnorePatternWhitespace |
+            RegexOptions.RightToLeft |
+            RegexOptions.CultureInvariant;
+
+        options &= ~NonApplicableOptions;
+        if ((options & UnsupportedOptions) != 0)
+        {
+            throw new NotSupportedException($"The selected RegEx options are not supported in BiDi: {options & UnsupportedOptions}");
+        }
+
+        if ((options & RegexOptions.IgnoreCase) != 0)
+        {
+            flags += "i";
+            options = options & ~RegexOptions.IgnoreCase;
+        }
+
+        if ((options & RegexOptions.Multiline) != 0)
+        {
+            options = options & ~RegexOptions.Multiline;
+            flags += "m";
+        }
+
+        if ((options & RegexOptions.Singleline) != 0)
+        {
+            options = options & ~RegexOptions.Singleline;
+            flags += "s";
+        }
+
+        Debug.Assert(options == RegexOptions.None);
+
+        return flags;
+    }
 }
diff --git a/dotnet/test/common/BiDi/Script/LocalValueConversionTests.cs b/dotnet/test/common/BiDi/Script/LocalValueConversionTests.cs
index c0a1058a55ef5..0ef090b7a0bcd 100644
--- a/dotnet/test/common/BiDi/Script/LocalValueConversionTests.cs
+++ b/dotnet/test/common/BiDi/Script/LocalValueConversionTests.cs
@@ -19,6 +19,10 @@
 
 using NUnit.Framework;
 using OpenQA.Selenium.BiDi.Modules.Script;
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
 
 namespace OpenQA.Selenium.BiDi.Script;
 
@@ -28,72 +32,211 @@ class LocalValueConversionTests
     public void CanConvertNullBoolToLocalValue()
     {
         bool? arg = null;
-        LocalValue result = arg;
-        Assert.That(result, Is.TypeOf<NullLocalValue>());
+
+        AssertValue(arg);
+        AssertValue(LocalValue.ConvertFrom(arg));
+
+        static void AssertValue(LocalValue value)
+        {
+            Assert.That(value, Is.TypeOf<NullLocalValue>());
+        }
     }
 
     [Test]
     public void CanConvertTrueToLocalValue()
     {
-        LocalValue result = true;
-        Assert.That(result, Is.TypeOf<BooleanLocalValue>());
-        Assert.That((result as BooleanLocalValue).Value, Is.True);
+        AssertValue(true);
+
+        AssertValue(LocalValue.ConvertFrom(true));
+
+        static void AssertValue(LocalValue value)
+        {
+            Assert.That(value, Is.TypeOf<BooleanLocalValue>());
+            Assert.That((value as BooleanLocalValue).Value, Is.True);
+        }
     }
 
     [Test]
     public void CanConvertFalseToLocalValue()
     {
-        LocalValue result = false;
-        Assert.That(result, Is.TypeOf<BooleanLocalValue>());
-        Assert.That((result as BooleanLocalValue).Value, Is.False);
+        AssertValue(false);
+
+        AssertValue(LocalValue.ConvertFrom(false));
+
+        static void AssertValue(LocalValue value)
+        {
+            Assert.That(value, Is.TypeOf<BooleanLocalValue>());
+            Assert.That((value as BooleanLocalValue).Value, Is.False);
+        }
     }
 
     [Test]
     public void CanConvertNullIntToLocalValue()
     {
         int? arg = null;
-        LocalValue result = arg;
-        Assert.That(result, Is.TypeOf<NullLocalValue>());
+
+        AssertValue(arg);
+
+        AssertValue(LocalValue.ConvertFrom(arg));
+
+        static void AssertValue(LocalValue value)
+        {
+            Assert.That(value, Is.TypeOf<NullLocalValue>());
+        }
     }
 
     [Test]
     public void CanConvertZeroIntToLocalValue()
     {
-        LocalValue result = 0;
-        Assert.That(result, Is.TypeOf<NumberLocalValue>());
-        Assert.That((result as NumberLocalValue).Value, Is.Zero);
+        int arg = 0;
+
+        AssertValue(arg);
+
+        AssertValue(LocalValue.ConvertFrom(arg));
+
+        static void AssertValue(LocalValue value)
+        {
+            Assert.That(value, Is.TypeOf<NumberLocalValue>());
+            Assert.That((value as NumberLocalValue).Value, Is.Zero);
+        }
     }
 
     [Test]
     public void CanConvertNullDoubleToLocalValue()
     {
         double? arg = null;
-        LocalValue result = arg;
-        Assert.That(result, Is.TypeOf<NullLocalValue>());
+
+        AssertValue(arg);
+
+        AssertValue(LocalValue.ConvertFrom(arg));
+
+        static void AssertValue(LocalValue value)
+        {
+            Assert.That(value, Is.TypeOf<NullLocalValue>());
+        }
     }
 
     [Test]
     public void CanConvertZeroDoubleToLocalValue()
     {
         double arg = 0;
-        LocalValue result = arg;
-        Assert.That(result, Is.TypeOf<NumberLocalValue>());
-        Assert.That((result as NumberLocalValue).Value, Is.Zero);
+
+        AssertValue(arg);
+
+        AssertValue(LocalValue.ConvertFrom(arg));
+
+        static void AssertValue(LocalValue value)
+        {
+            Assert.That(value, Is.TypeOf<NumberLocalValue>());
+            Assert.That((value as NumberLocalValue).Value, Is.Zero);
+        }
     }
 
     [Test]
     public void CanConvertNullStringToLocalValue()
     {
         string arg = null;
-        LocalValue result = arg;
-        Assert.That(result, Is.TypeOf<NullLocalValue>());
+
+        AssertValue(arg);
+
+        AssertValue(LocalValue.ConvertFrom(arg));
+
+        static void AssertValue(LocalValue value)
+        {
+            Assert.That(value, Is.TypeOf<NullLocalValue>());
+        }
     }
 
     [Test]
     public void CanConvertStringToLocalValue()
     {
-        LocalValue result = "value";
-        Assert.That(result, Is.TypeOf<StringLocalValue>());
-        Assert.That((result as StringLocalValue).Value, Is.EqualTo("value"));
+        AssertValue("value");
+
+        AssertValue(LocalValue.ConvertFrom("value"));
+
+        static void AssertValue(LocalValue value)
+        {
+            Assert.That(value, Is.TypeOf<StringLocalValue>());
+            Assert.That((value as StringLocalValue).Value, Is.EqualTo("value"));
+        }
+    }
+
+    [Test]
+    public void CanConvertDateTimeOffsetToLocalValue()
+    {
+        var date = new DateTimeOffset(2025, 4, 13, 5, 40, 20, 123, 456, TimeSpan.FromHours(+3));
+
+        AssertValue(date);
+
+        AssertValue(LocalValue.ConvertFrom(date));
+
+        static void AssertValue(LocalValue value)
+        {
+            Assert.That(value, Is.TypeOf<DateLocalValue>());
+            Assert.That((value as DateLocalValue).Value, Is.EqualTo("2025-04-13T05:40:20.1234560+03:00"));
+        }
+    }
+
+    [Test]
+    public void CanConvertArrayToLocalValue()
+    {
+        AssertValue(LocalValue.ConvertFrom(new List<int> { 1, 2 }));
+
+        AssertValue(LocalValue.ConvertFrom(new string[] { "a", "b" }));
+
+        static void AssertValue(LocalValue value)
+        {
+            Assert.That(value, Is.TypeOf<ArrayLocalValue>());
+            Assert.That((value as ArrayLocalValue).Value.Count, Is.EqualTo(2));
+        }
+    }
+
+    [Test]
+    public void CanConvertMapToLocalValue()
+    {
+        AssertValue(LocalValue.ConvertFrom(new Dictionary<int, string> { { 1, "a" }, { 2, "b" } }));
+
+        static void AssertValue(LocalValue value)
+        {
+            Assert.That(value, Is.TypeOf<MapLocalValue>());
+            Assert.That((value as MapLocalValue).Value.Count, Is.EqualTo(2));
+        }
+    }
+
+    [Test]
+    public void CanConvertSetToLocalValue()
+    {
+        AssertValue(LocalValue.ConvertFrom(new HashSet<int> { 1, 2 }));
+
+        AssertValue(LocalValue.ConvertFrom(ImmutableHashSet.CreateRange([1, 2])));
+
+        static void AssertValue(LocalValue value)
+        {
+            Assert.That(value, Is.TypeOf<SetLocalValue>());
+            Assert.That((value as SetLocalValue).Value.Count, Is.EqualTo(2));
+        }
+    }
+
+    [Test]
+    public void CanConvertObjectValue()
+    {
+        var arg = new
+        {
+            UIntNumber = 5u,
+            Array = new int[] { 1, 2 },
+            List = new List<string> { "a", "b" },
+            Dictionary = new Dictionary<string, object> { { "a", 1 }, { "b", 2 } },
+            Set = new HashSet<string> { "a", "b" }
+        };
+
+        var value = LocalValue.ConvertFrom(arg);
+
+        Console.WriteLine(value);
+
+        Assert.That(value, Is.TypeOf<ObjectLocalValue>());
+
+        var objValue = value as ObjectLocalValue;
+
+        Assert.That(objValue.Value, Has.Exactly(5).Count);
     }
 }


Reviews:
user: login: nvborisenko
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-29T09:54:32Z
user: login: nvborisenko
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-12T10:53:46Z
user: login: nvborisenko
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-12T12:51:23Z
user: login: nvborisenko
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-12T13:03:30Z
user: login: nvborisenko
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-12T13:04:52Z
user: login: nvborisenko
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-12T13:19:11Z
user: login: nvborisenko
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-12T13:23:07Z
user: login: nvborisenko
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-12T13:52:31Z
user: login: nvborisenko
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-12T14:05:37Z
user: login: nvborisenko
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-12T16:00:06Z
user: login: nvborisenko
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-12T19:31:44Z
user: login: nvborisenko
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-12T19:47:23Z
user: login: nvborisenko
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-12T21:10:50Z
user: login: RenderMichael
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-13T04:15:00Z
user: login: RenderMichael
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-13T04:16:06Z
user: login: RenderMichael
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-13T04:26:53Z
user: login: nvborisenko
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-13T14:58:35Z
user: login: nvborisenko
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-13T15:00:45Z
user: login: nvborisenko
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-13T15:05:00Z

Review comments:
diff_hunk: @@ -41,10 +41,10 @@ namespace OpenQA.Selenium.BiDi.Modules.Script;
 [JsonDerivedType(typeof(SetLocalValue), "set")]
 public abstract record LocalValue
 {
-    public static implicit operator LocalValue(bool? value) { return value is bool b ? new BooleanLocalValue(b) : new NullLocalValue(); }
-    public static implicit operator LocalValue(int? value) { return value is int i ? new NumberLocalValue(i) : new NullLocalValue(); }
-    public static implicit operator LocalValue(double? value) { return value is double d ? new NumberLocalValue(d) : new NullLocalValue(); }
-    public static implicit operator LocalValue(string? value) { return value is null ? new NullLocalValue() : new StringLocalValue(value); }
+    public static implicit operator LocalValue(bool? value) { return ConvertFrom(value); }
+    public static implicit operator LocalValue(int? value) { return ConvertFrom(value); }
+    public static implicit operator LocalValue(double? value) { return ConvertFrom(value); }
+    public static implicit operator LocalValue(string? value) { return ConvertFrom(value); }
 
     // TODO: Extend converting from types
     public static LocalValue ConvertFrom(object? value)
path: dotnet/src/webdriver/BiDi/Modules/Script/LocalValue.cs
user: login: nvborisenko
body: We already have unit tests for this method. I propose to create some universal test to demonstrate the power of conversion. Where user is able to properly convert any "free-style object" to `LocalValue` (including all supported complex types). The idea behind it is:
- double-check for us
- see all supported types in one place
- easily understand what we are missing
created_at: 2025-03-29T09:54:32Z
author_association: MEMBER
diff_hunk: @@ -58,86 +58,210 @@ public static LocalValue ConvertFrom(object? value)
                 return new NullLocalValue();
 
             case bool b:
-                return new BooleanLocalValue(b);
+                return ConvertFrom(b);
 
             case int i:
-                return new NumberLocalValue(i);
+                return ConvertFrom(i);
 
             case double d:
-                return new NumberLocalValue(d);
+                return ConvertFrom(d);
 
             case long l:
-                return new NumberLocalValue(l);
+                return ConvertFrom(l);
 
             case DateTime dt:
-                return new DateLocalValue(dt.ToString("o"));
+                return ConvertFrom(dt);
 
             case BigInteger bigInt:
-                return new BigIntLocalValue(bigInt.ToString());
+                return ConvertFrom(bigInt);
 
             case string str:
-                return new StringLocalValue(str);
+                return ConvertFrom(str);
 
             case IDictionary<string, string?> dictionary:
-                {
-                    var bidiObject = new List<List<LocalValue>>(dictionary.Count);
-                    foreach (var item in dictionary)
-                    {
-                        bidiObject.Add([new StringLocalValue(item.Key), ConvertFrom(item.Value)]);
-                    }
-
-                    return new ObjectLocalValue(bidiObject);
-                }
+                return ConvertFrom(dictionary);
 
             case IDictionary<string, object?> dictionary:
-                {
-                    var bidiObject = new List<List<LocalValue>>(dictionary.Count);
-                    foreach (var item in dictionary)
-                    {
-                        bidiObject.Add([new StringLocalValue(item.Key), ConvertFrom(item.Value)]);
-                    }
-
-                    return new ObjectLocalValue(bidiObject);
-                }
+                return ConvertFrom(dictionary);
 
             case IDictionary<int, object?> dictionary:
-                {
-                    var bidiObject = new List<List<LocalValue>>(dictionary.Count);
-                    foreach (var item in dictionary)
-                    {
-                        bidiObject.Add([ConvertFrom(item.Key), ConvertFrom(item.Value)]);
-                    }
+                return ConvertFrom(dictionary);
+
+            case ISet<object?> set:
+                return ConvertFrom(set);
 
-                    return new MapLocalValue(bidiObject);
-                }
+            case IList<object?> set:
+                return ConvertFrom(set);
 
             case IEnumerable<object?> list:
-                return new ArrayLocalValue(list.Select(ConvertFrom).ToList());
-
-            case object:
-                {
-                    const System.Reflection.BindingFlags Flags = System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance;
-
-                    var properties = value.GetType().GetProperties(Flags);
-
-                    var values = new List<List<LocalValue>>(properties.Length);
-                    foreach (var property in properties)
-                    {
-                        object? propertyValue;
-                        try
-                        {
-                            propertyValue = property.GetValue(value);
-                        }
-                        catch (Exception ex)
-                        {
-                            throw new BiDiException($"Could not retrieve property {property.Name} from {property.DeclaringType}", ex);
-                        }
-                        values.Add([property.Name, ConvertFrom(propertyValue)]);
-                    }
-
-                    return new ObjectLocalValue(values);
-                }
+                return ConvertFrom(list);
+
+            default:
+                return ReflectionBasedConvertFrom(value);
+        }
+    }
+
+    public static LocalValue ConvertFrom(bool? value)
+    {
+        if (value is bool b)
+        {
+            return new BooleanLocalValue(b);
+        }
+
+        return new NullLocalValue();
+    }
+
+    public static LocalValue ConvertFrom(int? value)
+    {
+        if (value is int b)
+        {
+            return new NumberLocalValue(b);
+        }
+
+        return new NullLocalValue();
+    }
+
+    public static LocalValue ConvertFrom(double? value)
+    {
+        if (value is double b)
+        {
+            return new NumberLocalValue(b);
         }
+
+        return new NullLocalValue();
+    }
+
+    public static LocalValue ConvertFrom(long? value)
+    {
+        if (value is long b)
+        {
+            return new NumberLocalValue(b);
+        }
+
+        return new NullLocalValue();
+    }
+
+    public static LocalValue ConvertFrom(string? value)
+    {
+        if (value is not null)
+        {
+            return new StringLocalValue(value);
+        }
+
+        return new NullLocalValue();
+    }
+
+    public static LocalValue ConvertFrom(DateTime? value)
+    {
+        if (value is null)
+        {
+            return new NullLocalValue();
+        }
+
+        return new DateLocalValue(value.Value.ToString("o"));
+    }
+
+    public static LocalValue ConvertFrom(BigInteger? value)
+    {
+        if (value is not null)
+        {
+            return new BigIntLocalValue(value.Value.ToString());
+        }
+
+        return new NullLocalValue();
+    }
+
+    public static LocalValue ConvertFrom(IEnumerable<object?>? value)
path: dotnet/src/webdriver/BiDi/Modules/Script/LocalValue.cs
user: login: nvborisenko
body: Should be non-generic `IEnumerable`, then conversion will happen for `object` flow (via reflection). The same for `IDictionary` and `ISet`.
created_at: 2025-04-12T10:53:46Z
author_association: MEMBER
diff_hunk: @@ -58,86 +58,210 @@ public static LocalValue ConvertFrom(object? value)
                 return new NullLocalValue();
 
             case bool b:
-                return new BooleanLocalValue(b);
+                return ConvertFrom(b);
 
             case int i:
-                return new NumberLocalValue(i);
+                return ConvertFrom(i);
 
             case double d:
-                return new NumberLocalValue(d);
+                return ConvertFrom(d);
 
             case long l:
-                return new NumberLocalValue(l);
+                return ConvertFrom(l);
 
             case DateTime dt:
-                return new DateLocalValue(dt.ToString("o"));
+                return ConvertFrom(dt);
 
             case BigInteger bigInt:
-                return new BigIntLocalValue(bigInt.ToString());
+                return ConvertFrom(bigInt);
 
             case string str:
-                return new StringLocalValue(str);
+                return ConvertFrom(str);
 
             case IDictionary<string, string?> dictionary:
-                {
-                    var bidiObject = new List<List<LocalValue>>(dictionary.Count);
-                    foreach (var item in dictionary)
-                    {
-                        bidiObject.Add([new StringLocalValue(item.Key), ConvertFrom(item.Value)]);
-                    }
-
-                    return new ObjectLocalValue(bidiObject);
-                }
+                return ConvertFrom(dictionary);
 
             case IDictionary<string, object?> dictionary:
-                {
-                    var bidiObject = new List<List<LocalValue>>(dictionary.Count);
-                    foreach (var item in dictionary)
-                    {
-                        bidiObject.Add([new StringLocalValue(item.Key), ConvertFrom(item.Value)]);
-                    }
-
-                    return new ObjectLocalValue(bidiObject);
-                }
+                return ConvertFrom(dictionary);
 
             case IDictionary<int, object?> dictionary:
-                {
-                    var bidiObject = new List<List<LocalValue>>(dictionary.Count);
-                    foreach (var item in dictionary)
-                    {
-                        bidiObject.Add([ConvertFrom(item.Key), ConvertFrom(item.Value)]);
-                    }
+                return ConvertFrom(dictionary);
+
+            case ISet<object?> set:
+                return ConvertFrom(set);
 
-                    return new MapLocalValue(bidiObject);
-                }
+            case IList<object?> set:
+                return ConvertFrom(set);
 
             case IEnumerable<object?> list:
-                return new ArrayLocalValue(list.Select(ConvertFrom).ToList());
-
-            case object:
-                {
-                    const System.Reflection.BindingFlags Flags = System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance;
-
-                    var properties = value.GetType().GetProperties(Flags);
-
-                    var values = new List<List<LocalValue>>(properties.Length);
-                    foreach (var property in properties)
-                    {
-                        object? propertyValue;
-                        try
-                        {
-                            propertyValue = property.GetValue(value);
-                        }
-                        catch (Exception ex)
-                        {
-                            throw new BiDiException($"Could not retrieve property {property.Name} from {property.DeclaringType}", ex);
-                        }
-                        values.Add([property.Name, ConvertFrom(propertyValue)]);
-                    }
-
-                    return new ObjectLocalValue(values);
-                }
+                return ConvertFrom(list);
+
+            default:
+                return ReflectionBasedConvertFrom(value);
+        }
+    }
+
+    public static LocalValue ConvertFrom(bool? value)
+    {
+        if (value is bool b)
+        {
+            return new BooleanLocalValue(b);
+        }
+
+        return new NullLocalValue();
+    }
+
+    public static LocalValue ConvertFrom(int? value)
+    {
+        if (value is int b)
+        {
+            return new NumberLocalValue(b);
+        }
+
+        return new NullLocalValue();
+    }
+
+    public static LocalValue ConvertFrom(double? value)
+    {
+        if (value is double b)
+        {
+            return new NumberLocalValue(b);
         }
+
+        return new NullLocalValue();
+    }
+
+    public static LocalValue ConvertFrom(long? value)
+    {
+        if (value is long b)
+        {
+            return new NumberLocalValue(b);
+        }
+
+        return new NullLocalValue();
+    }
+
+    public static LocalValue ConvertFrom(string? value)
+    {
+        if (value is not null)
+        {
+            return new StringLocalValue(value);
+        }
+
+        return new NullLocalValue();
+    }
+
+    public static LocalValue ConvertFrom(DateTime? value)
+    {
+        if (value is null)
+        {
+            return new NullLocalValue();
+        }
+
+        return new DateLocalValue(value.Value.ToString("o"));
+    }
+
+    public static LocalValue ConvertFrom(BigInteger? value)
+    {
+        if (value is not null)
+        {
+            return new BigIntLocalValue(value.Value.ToString());
+        }
+
+        return new NullLocalValue();
+    }
+
+    public static LocalValue ConvertFrom(IEnumerable<object?>? value)
path: dotnet/src/webdriver/BiDi/Modules/Script/LocalValue.cs
user: login: nvborisenko
body: Resolved, but there is no non-generic `ISet`. This is why we still see `public static LocalValue ConvertFrom<T>(ISet<T?>? value)`.
created_at: 2025-04-12T12:51:23Z
author_association: MEMBER
diff_hunk: @@ -58,86 +58,210 @@ public static LocalValue ConvertFrom(object? value)
                 return new NullLocalValue();
 
             case bool b:
-                return new BooleanLocalValue(b);
+                return ConvertFrom(b);
 
             case int i:
-                return new NumberLocalValue(i);
+                return ConvertFrom(i);
 
             case double d:
-                return new NumberLocalValue(d);
+                return ConvertFrom(d);
 
             case long l:
-                return new NumberLocalValue(l);
+                return ConvertFrom(l);
 
             case DateTime dt:
-                return new DateLocalValue(dt.ToString("o"));
+                return ConvertFrom(dt);
 
             case BigInteger bigInt:
-                return new BigIntLocalValue(bigInt.ToString());
+                return ConvertFrom(bigInt);
 
             case string str:
-                return new StringLocalValue(str);
+                return ConvertFrom(str);
 
             case IDictionary<string, string?> dictionary:
-                {
-                    var bidiObject = new List<List<LocalValue>>(dictionary.Count);
-                    foreach (var item in dictionary)
-                    {
-                        bidiObject.Add([new StringLocalValue(item.Key), ConvertFrom(item.Value)]);
-                    }
-
-                    return new ObjectLocalValue(bidiObject);
-                }
+                return ConvertFrom(dictionary);
 
             case IDictionary<string, object?> dictionary:
-                {
-                    var bidiObject = new List<List<LocalValue>>(dictionary.Count);
-                    foreach (var item in dictionary)
-                    {
-                        bidiObject.Add([new StringLocalValue(item.Key), ConvertFrom(item.Value)]);
-                    }
-
-                    return new ObjectLocalValue(bidiObject);
-                }
+                return ConvertFrom(dictionary);
 
             case IDictionary<int, object?> dictionary:
-                {
-                    var bidiObject = new List<List<LocalValue>>(dictionary.Count);
-                    foreach (var item in dictionary)
-                    {
-                        bidiObject.Add([ConvertFrom(item.Key), ConvertFrom(item.Value)]);
-                    }
+                return ConvertFrom(dictionary);
+
+            case ISet<object?> set:
+                return ConvertFrom(set);
 
-                    return new MapLocalValue(bidiObject);
-                }
+            case IList<object?> set:
+                return ConvertFrom(set);
 
             case IEnumerable<object?> list:
-                return new ArrayLocalValue(list.Select(ConvertFrom).ToList());
-
-            case object:
-                {
-                    const System.Reflection.BindingFlags Flags = System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance;
-
-                    var properties = value.GetType().GetProperties(Flags);
-
-                    var values = new List<List<LocalValue>>(properties.Length);
-                    foreach (var property in properties)
-                    {
-                        object? propertyValue;
-                        try
-                        {
-                            propertyValue = property.GetValue(value);
-                        }
-                        catch (Exception ex)
-                        {
-                            throw new BiDiException($"Could not retrieve property {property.Name} from {property.DeclaringType}", ex);
-                        }
-                        values.Add([property.Name, ConvertFrom(propertyValue)]);
-                    }
-
-                    return new ObjectLocalValue(values);
-                }
+                return ConvertFrom(list);
+
+            default:
+                return ReflectionBasedConvertFrom(value);
+        }
+    }
+
+    public static LocalValue ConvertFrom(bool? value)
+    {
+        if (value is bool b)
+        {
+            return new BooleanLocalValue(b);
+        }
+
+        return new NullLocalValue();
+    }
+
+    public static LocalValue ConvertFrom(int? value)
+    {
+        if (value is int b)
+        {
+            return new NumberLocalValue(b);
+        }
+
+        return new NullLocalValue();
+    }
+
+    public static LocalValue ConvertFrom(double? value)
+    {
+        if (value is double b)
+        {
+            return new NumberLocalValue(b);
         }
+
+        return new NullLocalValue();
+    }
+
+    public static LocalValue ConvertFrom(long? value)
+    {
+        if (value is long b)
+        {
+            return new NumberLocalValue(b);
+        }
+
+        return new NullLocalValue();
+    }
+
+    public static LocalValue ConvertFrom(string? value)
+    {
+        if (value is not null)
+        {
+            return new StringLocalValue(value);
+        }
+
+        return new NullLocalValue();
+    }
+
+    public static LocalValue ConvertFrom(DateTime? value)
+    {
+        if (value is null)
+        {
+            return new NullLocalValue();
+        }
+
+        return new DateLocalValue(value.Value.ToString("o"));
+    }
+
+    public static LocalValue ConvertFrom(BigInteger? value)
+    {
+        if (value is not null)
+        {
+            return new BigIntLocalValue(value.Value.ToString());
+        }
+
+        return new NullLocalValue();
+    }
+
+    public static LocalValue ConvertFrom(IEnumerable<object?>? value)
path: dotnet/src/webdriver/BiDi/Modules/Script/LocalValue.cs
user: login: nvborisenko
body: It seems that this method is unnecessary since we have `LocalValue.ConvertFrom(object)`, but from user's point of view it is good to know that `LocalValue.ConvertFrom(new HashSet<string> { });` is supported.
created_at: 2025-04-12T13:03:30Z
author_association: MEMBER
diff_hunk: @@ -41,10 +41,10 @@ namespace OpenQA.Selenium.BiDi.Modules.Script;
 [JsonDerivedType(typeof(SetLocalValue), "set")]
 public abstract record LocalValue
 {
-    public static implicit operator LocalValue(bool? value) { return value is bool b ? new BooleanLocalValue(b) : new NullLocalValue(); }
-    public static implicit operator LocalValue(int? value) { return value is int i ? new NumberLocalValue(i) : new NullLocalValue(); }
-    public static implicit operator LocalValue(double? value) { return value is double d ? new NumberLocalValue(d) : new NullLocalValue(); }
-    public static implicit operator LocalValue(string? value) { return value is null ? new NullLocalValue() : new StringLocalValue(value); }
+    public static implicit operator LocalValue(bool? value) { return ConvertFrom(value); }
+    public static implicit operator LocalValue(int? value) { return ConvertFrom(value); }
+    public static implicit operator LocalValue(double? value) { return ConvertFrom(value); }
+    public static implicit operator LocalValue(string? value) { return ConvertFrom(value); }
 
     // TODO: Extend converting from types
     public static LocalValue ConvertFrom(object? value)
path: dotnet/src/webdriver/BiDi/Modules/Script/LocalValue.cs
user: login: nvborisenko
body: I fixed the conversion, including for `Set`. Now it is time to prepare tests.
created_at: 2025-04-12T13:04:51Z
author_association: MEMBER
diff_hunk: @@ -58,86 +59,198 @@ public static LocalValue ConvertFrom(object? value)
                 return new NullLocalValue();
 
             case bool b:
-                return new BooleanLocalValue(b);
+                return ConvertFrom(b);
 
             case int i:
-                return new NumberLocalValue(i);
+                return ConvertFrom(i);
 
             case double d:
-                return new NumberLocalValue(d);
+                return ConvertFrom(d);
 
             case long l:
-                return new NumberLocalValue(l);
+                return ConvertFrom(l);
 
             case DateTime dt:
-                return new DateLocalValue(dt.ToString("o"));
+                return ConvertFrom(dt);
 
             case BigInteger bigInt:
-                return new BigIntLocalValue(bigInt.ToString());
+                return ConvertFrom(bigInt);
 
             case string str:
-                return new StringLocalValue(str);
+                return ConvertFrom(str);
 
-            case IDictionary<string, string?> dictionary:
+            case { } when value.GetType().GetInterfaces()
+                .Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ISet<>)):
                 {
-                    var bidiObject = new List<List<LocalValue>>(dictionary.Count);
-                    foreach (var item in dictionary)
-                    {
-                        bidiObject.Add([new StringLocalValue(item.Key), ConvertFrom(item.Value)]);
-                    }
+                    IEnumerable set = (IEnumerable)value;
 
-                    return new ObjectLocalValue(bidiObject);
-                }
+                    List<LocalValue> setValues = [];
 
-            case IDictionary<string, object?> dictionary:
-                {
-                    var bidiObject = new List<List<LocalValue>>(dictionary.Count);
-                    foreach (var item in dictionary)
+                    foreach (var obj in set)
                     {
-                        bidiObject.Add([new StringLocalValue(item.Key), ConvertFrom(item.Value)]);
+                        setValues.Add(ConvertFrom(obj));
                     }
 
-                    return new ObjectLocalValue(bidiObject);
+                    return new SetLocalValue(setValues);
                 }
 
-            case IDictionary<int, object?> dictionary:
-                {
-                    var bidiObject = new List<List<LocalValue>>(dictionary.Count);
-                    foreach (var item in dictionary)
-                    {
-                        bidiObject.Add([ConvertFrom(item.Key), ConvertFrom(item.Value)]);
-                    }
+            case IDictionary dictionary:
+                return ConvertFrom(dictionary);
 
-                    return new MapLocalValue(bidiObject);
-                }
+            case IEnumerable enumerable:
+                return ConvertFrom(enumerable);
 
-            case IEnumerable<object?> list:
-                return new ArrayLocalValue(list.Select(ConvertFrom).ToList());
+            default:
path: dotnet/src/webdriver/BiDi/Modules/Script/LocalValue.cs
user: login: nvborisenko
body: We are missing `RegEx` type, let's add it, seems not a big deal.
created_at: 2025-04-12T13:19:11Z
author_association: MEMBER
diff_hunk: @@ -41,10 +42,10 @@ namespace OpenQA.Selenium.BiDi.Modules.Script;
 [JsonDerivedType(typeof(SetLocalValue), "set")]
 public abstract record LocalValue
path: dotnet/src/webdriver/BiDi/Modules/Script/LocalValue.cs
user: login: nvborisenko
body: We are missing `script.RemoteReference` type (https://w3c.github.io/webdriver-bidi/#type-script-LocalValue)
created_at: 2025-04-12T13:23:07Z
author_association: MEMBER
diff_hunk: @@ -41,10 +41,10 @@ namespace OpenQA.Selenium.BiDi.Modules.Script;
 [JsonDerivedType(typeof(SetLocalValue), "set")]
 public abstract record LocalValue
 {
-    public static implicit operator LocalValue(bool? value) { return value is bool b ? new BooleanLocalValue(b) : new NullLocalValue(); }
-    public static implicit operator LocalValue(int? value) { return value is int i ? new NumberLocalValue(i) : new NullLocalValue(); }
-    public static implicit operator LocalValue(double? value) { return value is double d ? new NumberLocalValue(d) : new NullLocalValue(); }
-    public static implicit operator LocalValue(string? value) { return value is null ? new NullLocalValue() : new StringLocalValue(value); }
+    public static implicit operator LocalValue(bool? value) { return ConvertFrom(value); }
+    public static implicit operator LocalValue(int? value) { return ConvertFrom(value); }
+    public static implicit operator LocalValue(double? value) { return ConvertFrom(value); }
+    public static implicit operator LocalValue(string? value) { return ConvertFrom(value); }
 
     // TODO: Extend converting from types
     public static LocalValue ConvertFrom(object? value)
path: dotnet/src/webdriver/BiDi/Modules/Script/LocalValue.cs
user: login: nvborisenko
body: Added minimal tests, let's go.
created_at: 2025-04-12T13:52:31Z
author_association: MEMBER
diff_hunk: @@ -58,86 +59,198 @@ public static LocalValue ConvertFrom(object? value)
                 return new NullLocalValue();
 
             case bool b:
-                return new BooleanLocalValue(b);
+                return ConvertFrom(b);
 
             case int i:
-                return new NumberLocalValue(i);
+                return ConvertFrom(i);
 
             case double d:
-                return new NumberLocalValue(d);
+                return ConvertFrom(d);
 
             case long l:
-                return new NumberLocalValue(l);
+                return ConvertFrom(l);
 
             case DateTime dt:
-                return new DateLocalValue(dt.ToString("o"));
+                return ConvertFrom(dt);
 
             case BigInteger bigInt:
-                return new BigIntLocalValue(bigInt.ToString());
+                return ConvertFrom(bigInt);
 
             case string str:
-                return new StringLocalValue(str);
+                return ConvertFrom(str);
 
-            case IDictionary<string, string?> dictionary:
+            case { } when value.GetType().GetInterfaces()
+                .Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ISet<>)):
                 {
-                    var bidiObject = new List<List<LocalValue>>(dictionary.Count);
-                    foreach (var item in dictionary)
-                    {
-                        bidiObject.Add([new StringLocalValue(item.Key), ConvertFrom(item.Value)]);
-                    }
+                    IEnumerable set = (IEnumerable)value;
 
-                    return new ObjectLocalValue(bidiObject);
-                }
+                    List<LocalValue> setValues = [];
 
-            case IDictionary<string, object?> dictionary:
-                {
-                    var bidiObject = new List<List<LocalValue>>(dictionary.Count);
-                    foreach (var item in dictionary)
+                    foreach (var obj in set)
                     {
-                        bidiObject.Add([new StringLocalValue(item.Key), ConvertFrom(item.Value)]);
+                        setValues.Add(ConvertFrom(obj));
                     }
 
-                    return new ObjectLocalValue(bidiObject);
+                    return new SetLocalValue(setValues);
                 }
 
-            case IDictionary<int, object?> dictionary:
-                {
-                    var bidiObject = new List<List<LocalValue>>(dictionary.Count);
-                    foreach (var item in dictionary)
-                    {
-                        bidiObject.Add([ConvertFrom(item.Key), ConvertFrom(item.Value)]);
-                    }
+            case IDictionary dictionary:
+                return ConvertFrom(dictionary);
 
-                    return new MapLocalValue(bidiObject);
-                }
+            case IEnumerable enumerable:
+                return ConvertFrom(enumerable);
 
-            case IEnumerable<object?> list:
-                return new ArrayLocalValue(list.Select(ConvertFrom).ToList());
+            default:
+                return ReflectionBasedConvertFrom(value);
+        }
+    }
 
-            case object:
-                {
-                    const System.Reflection.BindingFlags Flags = System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance;
+    public static LocalValue ConvertFrom(bool? value)
+    {
+        if (value is bool b)
+        {
+            return new BooleanLocalValue(b);
+        }
 
-                    var properties = value.GetType().GetProperties(Flags);
+        return new NullLocalValue();
+    }
 
-                    var values = new List<List<LocalValue>>(properties.Length);
-                    foreach (var property in properties)
-                    {
-                        object? propertyValue;
-                        try
-                        {
-                            propertyValue = property.GetValue(value);
-                        }
-                        catch (Exception ex)
-                        {
-                            throw new BiDiException($"Could not retrieve property {property.Name} from {property.DeclaringType}", ex);
-                        }
-                        values.Add([property.Name, ConvertFrom(propertyValue)]);
-                    }
+    public static LocalValue ConvertFrom(int? value)
+    {
+        if (value is int b)
+        {
+            return new NumberLocalValue(b);
+        }
 
-                    return new ObjectLocalValue(values);
-                }
+        return new NullLocalValue();
+    }
+
+    public static LocalValue ConvertFrom(double? value)
+    {
+        if (value is double b)
+        {
+            return new NumberLocalValue(b);
+        }
+
+        return new NullLocalValue();
+    }
+
+    public static LocalValue ConvertFrom(long? value)
+    {
+        if (value is long b)
+        {
+            return new NumberLocalValue(b);
+        }
+
+        return new NullLocalValue();
+    }
+
+    public static LocalValue ConvertFrom(string? value)
+    {
+        if (value is not null)
+        {
+            return new StringLocalValue(value);
         }
+
+        return new NullLocalValue();
+    }
+
+    public static LocalValue ConvertFrom(DateTime? value)
path: dotnet/src/webdriver/BiDi/Modules/Script/LocalValue.cs
user: login: nvborisenko
body: `DateTimeOffset`
created_at: 2025-04-12T14:05:37Z
author_association: MEMBER
diff_hunk: @@ -41,10 +42,10 @@ namespace OpenQA.Selenium.BiDi.Modules.Script;
 [JsonDerivedType(typeof(SetLocalValue), "set")]
 public abstract record LocalValue
path: dotnet/src/webdriver/BiDi/Modules/Script/LocalValue.cs
user: login: nvborisenko
body: Will be resolved in https://github.com/SeleniumHQ/selenium/issues/15621
created_at: 2025-04-12T16:00:06Z
author_association: MEMBER
diff_hunk: @@ -58,86 +59,198 @@ public static LocalValue ConvertFrom(object? value)
                 return new NullLocalValue();
 
             case bool b:
-                return new BooleanLocalValue(b);
+                return ConvertFrom(b);
 
             case int i:
-                return new NumberLocalValue(i);
+                return ConvertFrom(i);
 
             case double d:
-                return new NumberLocalValue(d);
+                return ConvertFrom(d);
 
             case long l:
-                return new NumberLocalValue(l);
+                return ConvertFrom(l);
 
             case DateTime dt:
-                return new DateLocalValue(dt.ToString("o"));
+                return ConvertFrom(dt);
 
             case BigInteger bigInt:
-                return new BigIntLocalValue(bigInt.ToString());
+                return ConvertFrom(bigInt);
 
             case string str:
-                return new StringLocalValue(str);
+                return ConvertFrom(str);
 
-            case IDictionary<string, string?> dictionary:
+            case { } when value.GetType().GetInterfaces()
+                .Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ISet<>)):
                 {
-                    var bidiObject = new List<List<LocalValue>>(dictionary.Count);
-                    foreach (var item in dictionary)
-                    {
-                        bidiObject.Add([new StringLocalValue(item.Key), ConvertFrom(item.Value)]);
-                    }
+                    IEnumerable set = (IEnumerable)value;
 
-                    return new ObjectLocalValue(bidiObject);
-                }
+                    List<LocalValue> setValues = [];
 
-            case IDictionary<string, object?> dictionary:
-                {
-                    var bidiObject = new List<List<LocalValue>>(dictionary.Count);
-                    foreach (var item in dictionary)
+                    foreach (var obj in set)
                     {
-                        bidiObject.Add([new StringLocalValue(item.Key), ConvertFrom(item.Value)]);
+                        setValues.Add(ConvertFrom(obj));
                     }
 
-                    return new ObjectLocalValue(bidiObject);
+                    return new SetLocalValue(setValues);
                 }
 
-            case IDictionary<int, object?> dictionary:
-                {
-                    var bidiObject = new List<List<LocalValue>>(dictionary.Count);
-                    foreach (var item in dictionary)
-                    {
-                        bidiObject.Add([ConvertFrom(item.Key), ConvertFrom(item.Value)]);
-                    }
+            case IDictionary dictionary:
+                return ConvertFrom(dictionary);
 
-                    return new MapLocalValue(bidiObject);
-                }
+            case IEnumerable enumerable:
+                return ConvertFrom(enumerable);
 
-            case IEnumerable<object?> list:
-                return new ArrayLocalValue(list.Select(ConvertFrom).ToList());
+            default:
+                return ReflectionBasedConvertFrom(value);
+        }
+    }
 
-            case object:
-                {
-                    const System.Reflection.BindingFlags Flags = System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance;
+    public static LocalValue ConvertFrom(bool? value)
+    {
+        if (value is bool b)
+        {
+            return new BooleanLocalValue(b);
+        }
 
-                    var properties = value.GetType().GetProperties(Flags);
+        return new NullLocalValue();
+    }
 
-                    var values = new List<List<LocalValue>>(properties.Length);
-                    foreach (var property in properties)
-                    {
-                        object? propertyValue;
-                        try
-                        {
-                            propertyValue = property.GetValue(value);
-                        }
-                        catch (Exception ex)
-                        {
-                            throw new BiDiException($"Could not retrieve property {property.Name} from {property.DeclaringType}", ex);
-                        }
-                        values.Add([property.Name, ConvertFrom(propertyValue)]);
-                    }
+    public static LocalValue ConvertFrom(int? value)
+    {
+        if (value is int b)
+        {
+            return new NumberLocalValue(b);
+        }
 
-                    return new ObjectLocalValue(values);
-                }
+        return new NullLocalValue();
+    }
+
+    public static LocalValue ConvertFrom(double? value)
+    {
+        if (value is double b)
+        {
+            return new NumberLocalValue(b);
+        }
+
+        return new NullLocalValue();
+    }
+
+    public static LocalValue ConvertFrom(long? value)
+    {
+        if (value is long b)
+        {
+            return new NumberLocalValue(b);
+        }
+
+        return new NullLocalValue();
+    }
+
+    public static LocalValue ConvertFrom(string? value)
+    {
+        if (value is not null)
+        {
+            return new StringLocalValue(value);
         }
+
+        return new NullLocalValue();
+    }
+
+    public static LocalValue ConvertFrom(DateTime? value)
path: dotnet/src/webdriver/BiDi/Modules/Script/LocalValue.cs
user: login: nvborisenko
body: Played around it and understood we have to know a conversion from `DateTime/DateTimeOffset` to JS string.  
created_at: 2025-04-12T19:31:44Z
author_association: MEMBER
diff_hunk: @@ -58,86 +59,198 @@ public static LocalValue ConvertFrom(object? value)
                 return new NullLocalValue();
 
             case bool b:
-                return new BooleanLocalValue(b);
+                return ConvertFrom(b);
 
             case int i:
-                return new NumberLocalValue(i);
+                return ConvertFrom(i);
 
             case double d:
-                return new NumberLocalValue(d);
+                return ConvertFrom(d);
 
             case long l:
-                return new NumberLocalValue(l);
+                return ConvertFrom(l);
 
             case DateTime dt:
-                return new DateLocalValue(dt.ToString("o"));
+                return ConvertFrom(dt);
 
             case BigInteger bigInt:
-                return new BigIntLocalValue(bigInt.ToString());
+                return ConvertFrom(bigInt);
 
             case string str:
-                return new StringLocalValue(str);
+                return ConvertFrom(str);
 
-            case IDictionary<string, string?> dictionary:
+            case { } when value.GetType().GetInterfaces()
+                .Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ISet<>)):
                 {
-                    var bidiObject = new List<List<LocalValue>>(dictionary.Count);
-                    foreach (var item in dictionary)
-                    {
-                        bidiObject.Add([new StringLocalValue(item.Key), ConvertFrom(item.Value)]);
-                    }
+                    IEnumerable set = (IEnumerable)value;
 
-                    return new ObjectLocalValue(bidiObject);
-                }
+                    List<LocalValue> setValues = [];
 
-            case IDictionary<string, object?> dictionary:
-                {
-                    var bidiObject = new List<List<LocalValue>>(dictionary.Count);
-                    foreach (var item in dictionary)
+                    foreach (var obj in set)
                     {
-                        bidiObject.Add([new StringLocalValue(item.Key), ConvertFrom(item.Value)]);
+                        setValues.Add(ConvertFrom(obj));
                     }
 
-                    return new ObjectLocalValue(bidiObject);
+                    return new SetLocalValue(setValues);
                 }
 
-            case IDictionary<int, object?> dictionary:
-                {
-                    var bidiObject = new List<List<LocalValue>>(dictionary.Count);
-                    foreach (var item in dictionary)
-                    {
-                        bidiObject.Add([ConvertFrom(item.Key), ConvertFrom(item.Value)]);
-                    }
+            case IDictionary dictionary:
+                return ConvertFrom(dictionary);
 
-                    return new MapLocalValue(bidiObject);
-                }
+            case IEnumerable enumerable:
+                return ConvertFrom(enumerable);
 
-            case IEnumerable<object?> list:
-                return new ArrayLocalValue(list.Select(ConvertFrom).ToList());
+            default:
+                return ReflectionBasedConvertFrom(value);
+        }
+    }
 
-            case object:
-                {
-                    const System.Reflection.BindingFlags Flags = System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance;
+    public static LocalValue ConvertFrom(bool? value)
+    {
+        if (value is bool b)
+        {
+            return new BooleanLocalValue(b);
+        }
 
-                    var properties = value.GetType().GetProperties(Flags);
+        return new NullLocalValue();
+    }
 
-                    var values = new List<List<LocalValue>>(properties.Length);
-                    foreach (var property in properties)
-                    {
-                        object? propertyValue;
-                        try
-                        {
-                            propertyValue = property.GetValue(value);
-                        }
-                        catch (Exception ex)
-                        {
-                            throw new BiDiException($"Could not retrieve property {property.Name} from {property.DeclaringType}", ex);
-                        }
-                        values.Add([property.Name, ConvertFrom(propertyValue)]);
-                    }
+    public static LocalValue ConvertFrom(int? value)
+    {
+        if (value is int b)
+        {
+            return new NumberLocalValue(b);
+        }
 
-                    return new ObjectLocalValue(values);
-                }
+        return new NullLocalValue();
+    }
+
+    public static LocalValue ConvertFrom(double? value)
+    {
+        if (value is double b)
+        {
+            return new NumberLocalValue(b);
+        }
+
+        return new NullLocalValue();
+    }
+
+    public static LocalValue ConvertFrom(long? value)
+    {
+        if (value is long b)
+        {
+            return new NumberLocalValue(b);
+        }
+
+        return new NullLocalValue();
+    }
+
+    public static LocalValue ConvertFrom(string? value)
+    {
+        if (value is not null)
+        {
+            return new StringLocalValue(value);
         }
+
+        return new NullLocalValue();
+    }
+
+    public static LocalValue ConvertFrom(DateTime? value)
path: dotnet/src/webdriver/BiDi/Modules/Script/LocalValue.cs
user: login: nvborisenko
body: ```csharp
await context.Script.CallFunctionAsync("() => { return Date.now(); }", false)
```

returns `EvaluateResultSuccess { Result = NumberRemoteValue { Value = 1744486840254 }, Realm = OpenQA.Selenium.BiDi.Modules.Script.Realm }`

unix epoch?

But `DateLocalValue` expects `string`. No ideas, we need help.
created_at: 2025-04-12T19:47:23Z
author_association: MEMBER
diff_hunk: @@ -58,86 +59,198 @@ public static LocalValue ConvertFrom(object? value)
                 return new NullLocalValue();
 
             case bool b:
-                return new BooleanLocalValue(b);
+                return ConvertFrom(b);
 
             case int i:
-                return new NumberLocalValue(i);
+                return ConvertFrom(i);
 
             case double d:
-                return new NumberLocalValue(d);
+                return ConvertFrom(d);
 
             case long l:
-                return new NumberLocalValue(l);
+                return ConvertFrom(l);
 
             case DateTime dt:
-                return new DateLocalValue(dt.ToString("o"));
+                return ConvertFrom(dt);
 
             case BigInteger bigInt:
-                return new BigIntLocalValue(bigInt.ToString());
+                return ConvertFrom(bigInt);
 
             case string str:
-                return new StringLocalValue(str);
+                return ConvertFrom(str);
 
-            case IDictionary<string, string?> dictionary:
+            case { } when value.GetType().GetInterfaces()
+                .Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ISet<>)):
                 {
-                    var bidiObject = new List<List<LocalValue>>(dictionary.Count);
-                    foreach (var item in dictionary)
-                    {
-                        bidiObject.Add([new StringLocalValue(item.Key), ConvertFrom(item.Value)]);
-                    }
+                    IEnumerable set = (IEnumerable)value;
 
-                    return new ObjectLocalValue(bidiObject);
-                }
+                    List<LocalValue> setValues = [];
 
-            case IDictionary<string, object?> dictionary:
-                {
-                    var bidiObject = new List<List<LocalValue>>(dictionary.Count);
-                    foreach (var item in dictionary)
+                    foreach (var obj in set)
                     {
-                        bidiObject.Add([new StringLocalValue(item.Key), ConvertFrom(item.Value)]);
+                        setValues.Add(ConvertFrom(obj));
                     }
 
-                    return new ObjectLocalValue(bidiObject);
+                    return new SetLocalValue(setValues);
                 }
 
-            case IDictionary<int, object?> dictionary:
-                {
-                    var bidiObject = new List<List<LocalValue>>(dictionary.Count);
-                    foreach (var item in dictionary)
-                    {
-                        bidiObject.Add([ConvertFrom(item.Key), ConvertFrom(item.Value)]);
-                    }
+            case IDictionary dictionary:
+                return ConvertFrom(dictionary);
 
-                    return new MapLocalValue(bidiObject);
-                }
+            case IEnumerable enumerable:
+                return ConvertFrom(enumerable);
 
-            case IEnumerable<object?> list:
-                return new ArrayLocalValue(list.Select(ConvertFrom).ToList());
+            default:
+                return ReflectionBasedConvertFrom(value);
+        }
+    }
 
-            case object:
-                {
-                    const System.Reflection.BindingFlags Flags = System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance;
+    public static LocalValue ConvertFrom(bool? value)
+    {
+        if (value is bool b)
+        {
+            return new BooleanLocalValue(b);
+        }
 
-                    var properties = value.GetType().GetProperties(Flags);
+        return new NullLocalValue();
+    }
 
-                    var values = new List<List<LocalValue>>(properties.Length);
-                    foreach (var property in properties)
-                    {
-                        object? propertyValue;
-                        try
-                        {
-                            propertyValue = property.GetValue(value);
-                        }
-                        catch (Exception ex)
-                        {
-                            throw new BiDiException($"Could not retrieve property {property.Name} from {property.DeclaringType}", ex);
-                        }
-                        values.Add([property.Name, ConvertFrom(propertyValue)]);
-                    }
+    public static LocalValue ConvertFrom(int? value)
+    {
+        if (value is int b)
+        {
+            return new NumberLocalValue(b);
+        }
 
-                    return new ObjectLocalValue(values);
-                }
+        return new NullLocalValue();
+    }
+
+    public static LocalValue ConvertFrom(double? value)
+    {
+        if (value is double b)
+        {
+            return new NumberLocalValue(b);
+        }
+
+        return new NullLocalValue();
+    }
+
+    public static LocalValue ConvertFrom(long? value)
+    {
+        if (value is long b)
+        {
+            return new NumberLocalValue(b);
+        }
+
+        return new NullLocalValue();
+    }
+
+    public static LocalValue ConvertFrom(string? value)
+    {
+        if (value is not null)
+        {
+            return new StringLocalValue(value);
         }
+
+        return new NullLocalValue();
+    }
+
+    public static LocalValue ConvertFrom(DateTime? value)
path: dotnet/src/webdriver/BiDi/Modules/Script/LocalValue.cs
user: login: nvborisenko
body: https://tc39.es/ecma262/#sec-date-time-string-format
created_at: 2025-04-12T21:10:49Z
author_association: MEMBER
diff_hunk: @@ -58,86 +59,198 @@ public static LocalValue ConvertFrom(object? value)
                 return new NullLocalValue();
 
             case bool b:
-                return new BooleanLocalValue(b);
+                return ConvertFrom(b);
 
             case int i:
-                return new NumberLocalValue(i);
+                return ConvertFrom(i);
 
             case double d:
-                return new NumberLocalValue(d);
+                return ConvertFrom(d);
 
             case long l:
-                return new NumberLocalValue(l);
+                return ConvertFrom(l);
 
             case DateTime dt:
-                return new DateLocalValue(dt.ToString("o"));
+                return ConvertFrom(dt);
 
             case BigInteger bigInt:
-                return new BigIntLocalValue(bigInt.ToString());
+                return ConvertFrom(bigInt);
 
             case string str:
-                return new StringLocalValue(str);
+                return ConvertFrom(str);
 
-            case IDictionary<string, string?> dictionary:
+            case { } when value.GetType().GetInterfaces()
+                .Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ISet<>)):
                 {
-                    var bidiObject = new List<List<LocalValue>>(dictionary.Count);
-                    foreach (var item in dictionary)
-                    {
-                        bidiObject.Add([new StringLocalValue(item.Key), ConvertFrom(item.Value)]);
-                    }
+                    IEnumerable set = (IEnumerable)value;
 
-                    return new ObjectLocalValue(bidiObject);
-                }
+                    List<LocalValue> setValues = [];
 
-            case IDictionary<string, object?> dictionary:
-                {
-                    var bidiObject = new List<List<LocalValue>>(dictionary.Count);
-                    foreach (var item in dictionary)
+                    foreach (var obj in set)
                     {
-                        bidiObject.Add([new StringLocalValue(item.Key), ConvertFrom(item.Value)]);
+                        setValues.Add(ConvertFrom(obj));
                     }
 
-                    return new ObjectLocalValue(bidiObject);
+                    return new SetLocalValue(setValues);
                 }
 
-            case IDictionary<int, object?> dictionary:
-                {
-                    var bidiObject = new List<List<LocalValue>>(dictionary.Count);
-                    foreach (var item in dictionary)
-                    {
-                        bidiObject.Add([ConvertFrom(item.Key), ConvertFrom(item.Value)]);
-                    }
+            case IDictionary dictionary:
+                return ConvertFrom(dictionary);
 
-                    return new MapLocalValue(bidiObject);
-                }
+            case IEnumerable enumerable:
+                return ConvertFrom(enumerable);
 
-            case IEnumerable<object?> list:
-                return new ArrayLocalValue(list.Select(ConvertFrom).ToList());
+            default:
+                return ReflectionBasedConvertFrom(value);
+        }
+    }
 
-            case object:
-                {
-                    const System.Reflection.BindingFlags Flags = System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance;
+    public static LocalValue ConvertFrom(bool? value)
+    {
+        if (value is bool b)
+        {
+            return new BooleanLocalValue(b);
+        }
 
-                    var properties = value.GetType().GetProperties(Flags);
+        return new NullLocalValue();
+    }
 
-                    var values = new List<List<LocalValue>>(properties.Length);
-                    foreach (var property in properties)
-                    {
-                        object? propertyValue;
-                        try
-                        {
-                            propertyValue = property.GetValue(value);
-                        }
-                        catch (Exception ex)
-                        {
-                            throw new BiDiException($"Could not retrieve property {property.Name} from {property.DeclaringType}", ex);
-                        }
-                        values.Add([property.Name, ConvertFrom(propertyValue)]);
-                    }
+    public static LocalValue ConvertFrom(int? value)
+    {
+        if (value is int b)
+        {
+            return new NumberLocalValue(b);
+        }
 
-                    return new ObjectLocalValue(values);
-                }
+        return new NullLocalValue();
+    }
+
+    public static LocalValue ConvertFrom(double? value)
+    {
+        if (value is double b)
+        {
+            return new NumberLocalValue(b);
+        }
+
+        return new NullLocalValue();
+    }
+
+    public static LocalValue ConvertFrom(long? value)
+    {
+        if (value is long b)
+        {
+            return new NumberLocalValue(b);
+        }
+
+        return new NullLocalValue();
+    }
+
+    public static LocalValue ConvertFrom(string? value)
+    {
+        if (value is not null)
+        {
+            return new StringLocalValue(value);
         }
+
+        return new NullLocalValue();
+    }
+
+    public static LocalValue ConvertFrom(DateTime? value)
path: dotnet/src/webdriver/BiDi/Modules/Script/LocalValue.cs
user: login: RenderMichael
body: `Date.now()` returns a number, you have to do `new Date()` to get the current date as a `Date` type.

https://github.com/SeleniumHQ/selenium/blob/12a1593042c3cd9c40de46cfdd1513ecfcfcd20a/dotnet/test/common/BiDi/Script/CallFunctionRemoteValueTest.cs#L81-L101

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#the_epoch_timestamps_and_invalid_date

created_at: 2025-04-13T04:14:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -58,86 +59,198 @@ public static LocalValue ConvertFrom(object? value)
                 return new NullLocalValue();
 
             case bool b:
-                return new BooleanLocalValue(b);
+                return ConvertFrom(b);
 
             case int i:
-                return new NumberLocalValue(i);
+                return ConvertFrom(i);
 
             case double d:
-                return new NumberLocalValue(d);
+                return ConvertFrom(d);
 
             case long l:
-                return new NumberLocalValue(l);
+                return ConvertFrom(l);
 
             case DateTime dt:
-                return new DateLocalValue(dt.ToString("o"));
+                return ConvertFrom(dt);
 
             case BigInteger bigInt:
-                return new BigIntLocalValue(bigInt.ToString());
+                return ConvertFrom(bigInt);
 
             case string str:
-                return new StringLocalValue(str);
+                return ConvertFrom(str);
 
-            case IDictionary<string, string?> dictionary:
+            case { } when value.GetType().GetInterfaces()
+                .Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ISet<>)):
                 {
-                    var bidiObject = new List<List<LocalValue>>(dictionary.Count);
-                    foreach (var item in dictionary)
-                    {
-                        bidiObject.Add([new StringLocalValue(item.Key), ConvertFrom(item.Value)]);
-                    }
+                    IEnumerable set = (IEnumerable)value;
 
-                    return new ObjectLocalValue(bidiObject);
-                }
+                    List<LocalValue> setValues = [];
 
-            case IDictionary<string, object?> dictionary:
-                {
-                    var bidiObject = new List<List<LocalValue>>(dictionary.Count);
-                    foreach (var item in dictionary)
+                    foreach (var obj in set)
                     {
-                        bidiObject.Add([new StringLocalValue(item.Key), ConvertFrom(item.Value)]);
+                        setValues.Add(ConvertFrom(obj));
                     }
 
-                    return new ObjectLocalValue(bidiObject);
+                    return new SetLocalValue(setValues);
                 }
 
-            case IDictionary<int, object?> dictionary:
-                {
-                    var bidiObject = new List<List<LocalValue>>(dictionary.Count);
-                    foreach (var item in dictionary)
-                    {
-                        bidiObject.Add([ConvertFrom(item.Key), ConvertFrom(item.Value)]);
-                    }
+            case IDictionary dictionary:
+                return ConvertFrom(dictionary);
 
-                    return new MapLocalValue(bidiObject);
-                }
+            case IEnumerable enumerable:
+                return ConvertFrom(enumerable);
 
-            case IEnumerable<object?> list:
-                return new ArrayLocalValue(list.Select(ConvertFrom).ToList());
+            default:
+                return ReflectionBasedConvertFrom(value);
+        }
+    }
 
-            case object:
-                {
-                    const System.Reflection.BindingFlags Flags = System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance;
+    public static LocalValue ConvertFrom(bool? value)
+    {
+        if (value is bool b)
+        {
+            return new BooleanLocalValue(b);
+        }
 
-                    var properties = value.GetType().GetProperties(Flags);
+        return new NullLocalValue();
+    }
 
-                    var values = new List<List<LocalValue>>(properties.Length);
-                    foreach (var property in properties)
-                    {
-                        object? propertyValue;
-                        try
-                        {
-                            propertyValue = property.GetValue(value);
-                        }
-                        catch (Exception ex)
-                        {
-                            throw new BiDiException($"Could not retrieve property {property.Name} from {property.DeclaringType}", ex);
-                        }
-                        values.Add([property.Name, ConvertFrom(propertyValue)]);
-                    }
+    public static LocalValue ConvertFrom(int? value)
+    {
+        if (value is int b)
+        {
+            return new NumberLocalValue(b);
+        }
 
-                    return new ObjectLocalValue(values);
-                }
+        return new NullLocalValue();
+    }
+
+    public static LocalValue ConvertFrom(double? value)
+    {
+        if (value is double b)
+        {
+            return new NumberLocalValue(b);
+        }
+
+        return new NullLocalValue();
+    }
+
+    public static LocalValue ConvertFrom(long? value)
+    {
+        if (value is long b)
+        {
+            return new NumberLocalValue(b);
+        }
+
+        return new NullLocalValue();
+    }
+
+    public static LocalValue ConvertFrom(string? value)
+    {
+        if (value is not null)
+        {
+            return new StringLocalValue(value);
         }
+
+        return new NullLocalValue();
+    }
+
+    public static LocalValue ConvertFrom(DateTime? value)
path: dotnet/src/webdriver/BiDi/Modules/Script/LocalValue.cs
user: login: RenderMichael
body: I prefer to avoid an implementation for `DateTimeOffset`, because I don't think we can fully represent it as `DateLocalValue`. For the same reason, casting `DateTimeOffset` -> `DateTime` requires an explicit cast, because you lose the offset/timezone info. Users should take care of that,
created_at: 2025-04-13T04:16:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -58,86 +59,198 @@ public static LocalValue ConvertFrom(object? value)
                 return new NullLocalValue();
 
             case bool b:
-                return new BooleanLocalValue(b);
+                return ConvertFrom(b);
 
             case int i:
-                return new NumberLocalValue(i);
+                return ConvertFrom(i);
 
             case double d:
-                return new NumberLocalValue(d);
+                return ConvertFrom(d);
 
             case long l:
-                return new NumberLocalValue(l);
+                return ConvertFrom(l);
 
             case DateTime dt:
-                return new DateLocalValue(dt.ToString("o"));
+                return ConvertFrom(dt);
 
             case BigInteger bigInt:
-                return new BigIntLocalValue(bigInt.ToString());
+                return ConvertFrom(bigInt);
 
             case string str:
-                return new StringLocalValue(str);
+                return ConvertFrom(str);
 
-            case IDictionary<string, string?> dictionary:
+            case { } when value.GetType().GetInterfaces()
+                .Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ISet<>)):
                 {
-                    var bidiObject = new List<List<LocalValue>>(dictionary.Count);
-                    foreach (var item in dictionary)
-                    {
-                        bidiObject.Add([new StringLocalValue(item.Key), ConvertFrom(item.Value)]);
-                    }
+                    IEnumerable set = (IEnumerable)value;
 
-                    return new ObjectLocalValue(bidiObject);
-                }
+                    List<LocalValue> setValues = [];
 
-            case IDictionary<string, object?> dictionary:
-                {
-                    var bidiObject = new List<List<LocalValue>>(dictionary.Count);
-                    foreach (var item in dictionary)
+                    foreach (var obj in set)
                     {
-                        bidiObject.Add([new StringLocalValue(item.Key), ConvertFrom(item.Value)]);
+                        setValues.Add(ConvertFrom(obj));
                     }
 
-                    return new ObjectLocalValue(bidiObject);
+                    return new SetLocalValue(setValues);
                 }
 
-            case IDictionary<int, object?> dictionary:
-                {
-                    var bidiObject = new List<List<LocalValue>>(dictionary.Count);
-                    foreach (var item in dictionary)
-                    {
-                        bidiObject.Add([ConvertFrom(item.Key), ConvertFrom(item.Value)]);
-                    }
+            case IDictionary dictionary:
+                return ConvertFrom(dictionary);
 
-                    return new MapLocalValue(bidiObject);
-                }
+            case IEnumerable enumerable:
+                return ConvertFrom(enumerable);
 
-            case IEnumerable<object?> list:
-                return new ArrayLocalValue(list.Select(ConvertFrom).ToList());
+            default:
path: dotnet/src/webdriver/BiDi/Modules/Script/LocalValue.cs
user: login: RenderMichael
body: Added `Regex` support. It is not trivial because of flags, I did the best I could mapping one-to-one. 
created_at: 2025-04-13T04:26:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -58,86 +59,198 @@ public static LocalValue ConvertFrom(object? value)
                 return new NullLocalValue();
 
             case bool b:
-                return new BooleanLocalValue(b);
+                return ConvertFrom(b);
 
             case int i:
-                return new NumberLocalValue(i);
+                return ConvertFrom(i);
 
             case double d:
-                return new NumberLocalValue(d);
+                return ConvertFrom(d);
 
             case long l:
-                return new NumberLocalValue(l);
+                return ConvertFrom(l);
 
             case DateTime dt:
-                return new DateLocalValue(dt.ToString("o"));
+                return ConvertFrom(dt);
 
             case BigInteger bigInt:
-                return new BigIntLocalValue(bigInt.ToString());
+                return ConvertFrom(bigInt);
 
             case string str:
-                return new StringLocalValue(str);
+                return ConvertFrom(str);
 
-            case IDictionary<string, string?> dictionary:
+            case { } when value.GetType().GetInterfaces()
+                .Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ISet<>)):
                 {
-                    var bidiObject = new List<List<LocalValue>>(dictionary.Count);
-                    foreach (var item in dictionary)
-                    {
-                        bidiObject.Add([new StringLocalValue(item.Key), ConvertFrom(item.Value)]);
-                    }
+                    IEnumerable set = (IEnumerable)value;
 
-                    return new ObjectLocalValue(bidiObject);
-                }
+                    List<LocalValue> setValues = [];
 
-            case IDictionary<string, object?> dictionary:
-                {
-                    var bidiObject = new List<List<LocalValue>>(dictionary.Count);
-                    foreach (var item in dictionary)
+                    foreach (var obj in set)
                     {
-                        bidiObject.Add([new StringLocalValue(item.Key), ConvertFrom(item.Value)]);
+                        setValues.Add(ConvertFrom(obj));
                     }
 
-                    return new ObjectLocalValue(bidiObject);
+                    return new SetLocalValue(setValues);
                 }
 
-            case IDictionary<int, object?> dictionary:
-                {
-                    var bidiObject = new List<List<LocalValue>>(dictionary.Count);
-                    foreach (var item in dictionary)
-                    {
-                        bidiObject.Add([ConvertFrom(item.Key), ConvertFrom(item.Value)]);
-                    }
+            case IDictionary dictionary:
+                return ConvertFrom(dictionary);
 
-                    return new MapLocalValue(bidiObject);
-                }
+            case IEnumerable enumerable:
+                return ConvertFrom(enumerable);
 
-            case IEnumerable<object?> list:
-                return new ArrayLocalValue(list.Select(ConvertFrom).ToList());
+            default:
+                return ReflectionBasedConvertFrom(value);
+        }
+    }
 
-            case object:
-                {
-                    const System.Reflection.BindingFlags Flags = System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance;
+    public static LocalValue ConvertFrom(bool? value)
+    {
+        if (value is bool b)
+        {
+            return new BooleanLocalValue(b);
+        }
 
-                    var properties = value.GetType().GetProperties(Flags);
+        return new NullLocalValue();
+    }
 
-                    var values = new List<List<LocalValue>>(properties.Length);
-                    foreach (var property in properties)
-                    {
-                        object? propertyValue;
-                        try
-                        {
-                            propertyValue = property.GetValue(value);
-                        }
-                        catch (Exception ex)
-                        {
-                            throw new BiDiException($"Could not retrieve property {property.Name} from {property.DeclaringType}", ex);
-                        }
-                        values.Add([property.Name, ConvertFrom(propertyValue)]);
-                    }
+    public static LocalValue ConvertFrom(int? value)
+    {
+        if (value is int b)
+        {
+            return new NumberLocalValue(b);
+        }
 
-                    return new ObjectLocalValue(values);
-                }
+        return new NullLocalValue();
+    }
+
+    public static LocalValue ConvertFrom(double? value)
+    {
+        if (value is double b)
+        {
+            return new NumberLocalValue(b);
+        }
+
+        return new NullLocalValue();
+    }
+
+    public static LocalValue ConvertFrom(long? value)
+    {
+        if (value is long b)
+        {
+            return new NumberLocalValue(b);
+        }
+
+        return new NullLocalValue();
+    }
+
+    public static LocalValue ConvertFrom(string? value)
+    {
+        if (value is not null)
+        {
+            return new StringLocalValue(value);
         }
+
+        return new NullLocalValue();
+    }
+
+    public static LocalValue ConvertFrom(DateTime? value)
path: dotnet/src/webdriver/BiDi/Modules/Script/LocalValue.cs
user: login: nvborisenko
body: I understood we can use any .net type for `LocalValue`, even `DateOnly`. In future we can easily add supporting of all o them, just to serialize it to `string` properly. Let's land with `DateTimeOfffset` for now, because of I used it in another BiDi places, not only for `LocalValue`.

Fixed. We always can improve later.
created_at: 2025-04-13T14:58:35Z
author_association: MEMBER
diff_hunk: @@ -58,86 +59,198 @@ public static LocalValue ConvertFrom(object? value)
                 return new NullLocalValue();
 
             case bool b:
-                return new BooleanLocalValue(b);
+                return ConvertFrom(b);
 
             case int i:
-                return new NumberLocalValue(i);
+                return ConvertFrom(i);
 
             case double d:
-                return new NumberLocalValue(d);
+                return ConvertFrom(d);
 
             case long l:
-                return new NumberLocalValue(l);
+                return ConvertFrom(l);
 
             case DateTime dt:
-                return new DateLocalValue(dt.ToString("o"));
+                return ConvertFrom(dt);
 
             case BigInteger bigInt:
-                return new BigIntLocalValue(bigInt.ToString());
+                return ConvertFrom(bigInt);
 
             case string str:
-                return new StringLocalValue(str);
+                return ConvertFrom(str);
 
-            case IDictionary<string, string?> dictionary:
+            case { } when value.GetType().GetInterfaces()
+                .Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ISet<>)):
                 {
-                    var bidiObject = new List<List<LocalValue>>(dictionary.Count);
-                    foreach (var item in dictionary)
-                    {
-                        bidiObject.Add([new StringLocalValue(item.Key), ConvertFrom(item.Value)]);
-                    }
+                    IEnumerable set = (IEnumerable)value;
 
-                    return new ObjectLocalValue(bidiObject);
-                }
+                    List<LocalValue> setValues = [];
 
-            case IDictionary<string, object?> dictionary:
-                {
-                    var bidiObject = new List<List<LocalValue>>(dictionary.Count);
-                    foreach (var item in dictionary)
+                    foreach (var obj in set)
                     {
-                        bidiObject.Add([new StringLocalValue(item.Key), ConvertFrom(item.Value)]);
+                        setValues.Add(ConvertFrom(obj));
                     }
 
-                    return new ObjectLocalValue(bidiObject);
+                    return new SetLocalValue(setValues);
                 }
 
-            case IDictionary<int, object?> dictionary:
-                {
-                    var bidiObject = new List<List<LocalValue>>(dictionary.Count);
-                    foreach (var item in dictionary)
-                    {
-                        bidiObject.Add([ConvertFrom(item.Key), ConvertFrom(item.Value)]);
-                    }
+            case IDictionary dictionary:
+                return ConvertFrom(dictionary);
 
-                    return new MapLocalValue(bidiObject);
-                }
+            case IEnumerable enumerable:
+                return ConvertFrom(enumerable);
 
-            case IEnumerable<object?> list:
-                return new ArrayLocalValue(list.Select(ConvertFrom).ToList());
+            default:
path: dotnet/src/webdriver/BiDi/Modules/Script/LocalValue.cs
user: login: nvborisenko
body: Thank you, the conversion code looks hard, but I trust you. Minor requested feature - we will add tests later. 
created_at: 2025-04-13T15:00:45Z
author_association: MEMBER

PR comments:
user: login: qodo-code-review[bot]
created_at: 2025-03-28T18:34:46Z
body: ## PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

<table>
<tr><td>⏱️&nbsp;<strong>Estimated effort to review</strong>: 2 🔵🔵⚪⚪⚪</td></tr>
<tr><td>🧪&nbsp;<strong>No relevant tests</strong></td></tr>
<tr><td>🔒&nbsp;<strong>No security concerns identified</strong></td></tr>
<tr><td>⚡&nbsp;<strong>Recommended focus areas for review</strong><br><br>

<details><summary><a href='https://github.com/SeleniumHQ/selenium/pull/15532/files#diff-b2d3bd83230255db2e435ac34700a0af0595469211636058e6eb477107171b22R207-R208'><strong>Redundant Code</strong></a>

The `convertedArray` variable on line 207 is created but then not used in the return statement on line 208, which creates a new array conversion instead.
</summary>

```c#
LocalValue[] convertedArray = Array.ConvertAll(values, ConvertFrom);
return new ArrayLocalValue(Array.ConvertAll(values, ConvertFrom));
```

</details>

<details><summary><a href='https://github.com/SeleniumHQ/selenium/pull/15532/files#diff-b2d3bd83230255db2e435ac34700a0af0595469211636058e6eb477107171b22R103-R156'><strong>Parameter Naming</strong></a>

In several nullable type conversion methods (lines 103-156), the parameter name 'b' is used for the unwrapped value, which is inconsistent with the parameter type (should use more descriptive names like 'intValue' instead of 'b' for integers).
</summary>

```c#
public static LocalValue ConvertFrom(bool? value)
{
    if (value is bool b)
    {
        return new BooleanLocalValue(b);
    }

    return new NullLocalValue();
}

public static LocalValue ConvertFrom(int value)
{
    return new NumberLocalValue(value);
}

public static LocalValue ConvertFrom(int? value)
{
    if (value is int b)
    {
        return new NumberLocalValue(b);
    }

    return new NullLocalValue();
}

public static LocalValue ConvertFrom(double value)
{
    return new NumberLocalValue(value);
}

public static LocalValue ConvertFrom(double? value)
{
    if (value is double b)
    {
        return new NumberLocalValue(b);
    }

    return new NullLocalValue();
}

public static LocalValue ConvertFrom(long value)
{
    return new NumberLocalValue(value);
}

public static LocalValue ConvertFrom(long? value)
{
    if (value is long b)
    {
        return new NumberLocalValue(b);
    }

    return new NullLocalValue();
}
```

</details>

</td></tr>
</table>

author_association: CONTRIBUTOR
user: login: qodo-code-review[bot]
created_at: 2025-03-28T18:35:08Z
body: ## PR Code Suggestions ✨

<!-- 742b386 -->

Explore these optional code suggestions:

<table><thead><tr><td><strong>Category</strong></td><td align=left><strong>Suggestion&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </strong></td><td align=center><strong>Impact</strong></td></tr><tbody><tr><td rowspan=1>General</td>
<td>



<details><summary>✅ 
Remove redundant array conversion
</summary>

___

<details><summary><b>Suggestion Impact:</b></summary>The commit completely removed the problematic method ConvertFrom(object?[]?) that contained the redundant array conversion. Instead of fixing the specific issue, the commit refactored the code to use generic methods, eliminating the need for multiple specialized methods including the one with the redundant conversion.

code diff:

```diff
-    public static LocalValue ConvertFrom(object?[]? values)
-    {
-        if (values is null)
-        {
-            return new NullLocalValue();
-        }
-
-        LocalValue[] convertedArray = Array.ConvertAll(values, ConvertFrom);
-        return new ArrayLocalValue(Array.ConvertAll(values, ConvertFrom));
-    }
```

</details>


___


**The method is converting the array twice unnecessarily. The <code>convertedArray</code> <br>variable is created but never used, and then the conversion is performed again <br>in the return statement.**

[dotnet/src/webdriver/BiDi/Modules/Script/LocalValue.cs [200-209]](https://github.com/SeleniumHQ/selenium/pull/15532/files#diff-b2d3bd83230255db2e435ac34700a0af0595469211636058e6eb477107171b22R200-R209)

```diff
 public static LocalValue ConvertFrom(object?[]? values)
 {
     if (values is null)
     {
         return new NullLocalValue();
     }
 
     LocalValue[] convertedArray = Array.ConvertAll(values, ConvertFrom);
-    return new ArrayLocalValue(Array.ConvertAll(values, ConvertFrom));
+    return new ArrayLocalValue(convertedArray);
 }
```

`[Suggestion has been applied]`

<details><summary>Suggestion importance[1-10]: 9</summary>

__

Why: The suggestion correctly identifies a significant bug where the array is converted twice unnecessarily. The code creates a `convertedArray` variable but then ignores it, performing the conversion again in the return statement. This wastes CPU cycles and memory, potentially impacting performance for large arrays.

</details></details></td><td align=center>High

</td></tr>
<tr><td align="center" colspan="2">

- [ ] Update <!-- /improve --more_suggestions=true -->

</td><td></td></tr></tbody></table>
author_association: CONTRIBUTOR
user: login: nvborisenko
created_at: 2025-04-13T15:04:49Z
body: Our big concern here is whether we should support nullable primitive types. I guess it is more philosophic question, from implementation perspective for us it doesn't matter. For end users it is also kind of "doesn't matter". 
author_association: MEMBER
user: login: nvborisenko
created_at: 2025-04-13T15:06:40Z
body: Thank you, Mike! Let's move on and improve later when necessary.
author_association: MEMBER