Repository: SeleniumHQ/selenium (language: python)

Issue:
[🚀 Feature]: .NET: Support nullable reference types
### Feature and motivation

Annotate the code for nullable reference type support. This would be massively beneficial for clarity and to prevent null reference exceptions, both within Selenium and for users.

### Usage example

![image](https://github.com/user-attachments/assets/44811002-17d2-424f-921a-c9ab62fb04ee)
![image](https://github.com/user-attachments/assets/1d5d7127-1873-4d57-8ec4-fb7d70eec19f)

## Affected namespaces

- [ ] `OpenQA.Selenium`
- [x] `OpenQA.Selenium.BiDi`
- [x] `OpenQA.Selenium.BiDi.Communication`
  - [x] Nested namespaces
- [x] `OpenQA.Selenium.BiDi.Modules`
  - [x] Nested namespaces
- [ ] `OpenQA.Selenium.Chrome`
- [ ] `OpenQA.Selenium.Chromium`
- [ ] `OpenQA.Selenium.DevTools`
  - [ ] Nested namespaces
- [ ] `OpenQA.Selenium.Edge`
- [ ] `OpenQA.Selenium.Firefox`
- [ ] `OpenQA.Selenium.Firefox.Internal`
- [ ] `OpenQA.Selenium.IE`
- [ ] `OpenQA.Selenium.Interactions`
- [ ] `OpenQA.Selenium.Interactions.Internal`
- [ ] `OpenQA.Selenium.Internal`
- [ ] `OpenQA.Selenium.Internal.Logging`
- [ ] `OpenQA.Selenium.Remote`
- [ ] `OpenQA.Selenium.Safari`
- [ ] `OpenQA.Selenium.Support.UI`
- [ ] `OpenQA.Selenium.VirtualAuth`


Discussion / hints:


@RenderMichael, thank you for creating this issue. We will troubleshoot it as soon as we can.

---
<details>
  <summary>Info for maintainers</summary>
  <div>
    <br>
    <p>
      Triage this issue by using labels.
    </p>
    <p>
      If information is missing, add a helpful comment and then <code>I-issue-template</code> label.
    </p>
    <p>
      If the issue is a question, add the <code>I-question</code> label.
    </p>
    <p>
      If the issue is valid but there is no time to troubleshoot it, consider adding the <code>help wanted</code> label.
    </p>
    <p>
      If the issue requires changes or fixes from an external project (e.g., ChromeDriver, GeckoDriver, MSEdgeDriver, W3C),
     add the applicable <code>G-*</code> label, and it will provide the correct link and auto-close the
     issue.
    </p>
    <p>
      After troubleshooting the issue, please add the <code>R-awaiting answer</code> label.
    </p>
    <p>
      Thank you!
    </p>
  </div>
</details>



If Selenium is interested in accepting NRT, I'll gladly take up the task
It would be great, and it would be challenging as for you as for us :) Only one request: try keeping PRs small.
@RenderMichael do you mind to join [Selenium Slack channel](https://join.slack.com/t/seleniumhq/shared_invite/zt-2stlcmc6b-Hww~3r3yAzquhSq4riOFxA) for messaging instantly?
Hi Michael, do you think it makes sense to create something to track how far we are from final goal? Probably simple list by namespaces how many warnings we had/have.
I can make a list by namespace, but the PRs will likely not be one-to-one with this list. Still, a good heuristic.
Not even mention PRs, something like?:

- Selenium.Firefox **14** left out 20
- Selenium.Chrome.Something **2** left out of 40

where in case of Firefox: `20` how many warnings we had before (at the beginning of this journey), and `14` how many we have at this time. This list is editable by you (in your original post?).

Just to understand where we are.
I made a checkbox list of namespaces, and I'll populate the list with values as I test things out.
Thank you so much, it will help us to measure our power and abilities.

Fix patch:
diff --git a/dotnet/src/webdriver/VirtualAuth/Credential.cs b/dotnet/src/webdriver/VirtualAuth/Credential.cs
index 170677b12e035..57dbcbe76ff45 100644
--- a/dotnet/src/webdriver/VirtualAuth/Credential.cs
+++ b/dotnet/src/webdriver/VirtualAuth/Credential.cs
@@ -18,31 +18,30 @@
 // </copyright>
 
 using OpenQA.Selenium.Internal;
+using System;
 using System.Collections.Generic;
 
+#nullable enable
+
 namespace OpenQA.Selenium.VirtualAuth
 {
     /// <summary>
     /// A credential stored in a virtual authenticator.
-    /// Refer https://w3c.github.io/webauthn/#credential-parameters
+    /// Refer <see href="https://w3c.github.io/webauthn/#credential-parameters"/>
     /// </summary>
-    public class Credential
+    public sealed class Credential
     {
         private readonly byte[] id;
-        private readonly bool isResidentCredential;
-        private readonly string rpId;
-        private readonly string privateKey;
-        private readonly byte[] userHandle;
-        private readonly int signCount;
+        private readonly byte[]? userHandle;
 
-        private Credential(byte[] id, bool isResidentCredential, string rpId, string privateKey, byte[] userHandle, int signCount)
+        private Credential(byte[] id, bool isResidentCredential, string rpId, string privateKey, byte[]? userHandle, int signCount)
         {
-            this.id = id;
-            this.isResidentCredential = isResidentCredential;
-            this.rpId = rpId;
-            this.privateKey = privateKey;
+            this.id = id ?? throw new ArgumentNullException(nameof(id));
+            this.IsResidentCredential = isResidentCredential;
+            this.RpId = rpId ?? throw new ArgumentNullException(nameof(rpId));
+            this.PrivateKey = privateKey ?? throw new ArgumentNullException(nameof(privateKey));
             this.userHandle = userHandle;
-            this.signCount = signCount;
+            this.SignCount = signCount;
         }
 
         /// <summary>
@@ -53,6 +52,7 @@ private Credential(byte[] id, bool isResidentCredential, string rpId, string pri
         /// <param name="privateKey">The private Key for the credentials.</param>
         /// <param name="signCount">The signature counter for the credentials.</param>
         /// <returns>The created instance of the Credential class.</returns>
+        /// <exception cref="ArgumentNullException">If <paramref name="id"/>, <paramref name="rpId"/>, or <paramref name="privateKey"/> are <see langword="null"/>.</exception>
         public static Credential CreateNonResidentCredential(byte[] id, string rpId, string privateKey, int signCount)
         {
             return new Credential(id, false, rpId, privateKey, null, signCount);
@@ -67,6 +67,7 @@ public static Credential CreateNonResidentCredential(byte[] id, string rpId, str
         /// <param name="userHandle">The user handle associated to the credential.</param>
         /// <param name="signCount">The signature counter for the credentials.</param>
         /// <returns>The created instance of the Credential class.</returns>
+        /// <exception cref="ArgumentNullException">If <paramref name="id"/>, <paramref name="rpId"/>, or <paramref name="privateKey"/> are <see langword="null"/>.</exception>
         public static Credential CreateResidentCredential(byte[] id, string rpId, string privateKey, byte[] userHandle, int signCount)
         {
             return new Credential(id, true, rpId, privateKey, userHandle, signCount);
@@ -75,50 +76,32 @@ public static Credential CreateResidentCredential(byte[] id, string rpId, string
         /// <summary>
         /// Gets the byte array of the ID of the credential.
         /// </summary>
-        public byte[] Id
-        {
-            get { return (byte[])id.Clone(); }
-        }
+        public byte[] Id => (byte[])id.Clone();
 
         /// <summary>
         /// Gets a value indicating whether this Credential is a resident credential.
         /// </summary>
-        public bool IsResidentCredential
-        {
-            get { return this.isResidentCredential; }
-        }
+        public bool IsResidentCredential { get; }
 
         /// <summary>
         /// Gets the ID of the relying party of this credential.
         /// </summary>
-        public string RpId
-        {
-            get { return this.rpId; }
-        }
+        public string RpId { get; }
 
         /// <summary>
         /// Gets the private key of the credential.
         /// </summary>
-        public string PrivateKey
-        {
-            get { return this.privateKey; }
-        }
+        public string PrivateKey { get; }
 
         /// <summary>
         /// Gets the user handle of the credential.
         /// </summary>
-        public byte[] UserHandle
-        {
-            get { return userHandle == null ? null : (byte[])userHandle.Clone(); }
-        }
+        public byte[]? UserHandle => (byte[]?)userHandle?.Clone();
 
         /// <summary>
         /// Gets the signature counter associated to the public key credential source.
         /// </summary>
-        public int SignCount
-        {
-            get { return this.signCount; }
-        }
+        public int SignCount { get; }
 
         /// <summary>
         /// Creates a Credential instance from a dictionary of values.
@@ -127,13 +110,14 @@ public int SignCount
         /// <returns>The created instance of the Credential.</returns>
         public static Credential FromDictionary(Dictionary<string, object> dictionary)
         {
-            return new Credential(
-                Base64UrlEncoder.DecodeBytes((string)dictionary["credentialId"]),
-                (bool)dictionary["isResidentCredential"],
-                dictionary.ContainsKey("rpId") ? (string)dictionary["rpId"] : null,
-                (string)dictionary["privateKey"],
-                dictionary.ContainsKey("userHandle") ? Base64UrlEncoder.DecodeBytes((string)dictionary["userHandle"]) : null,
-                (int)((long)dictionary["signCount"]));
+            byte[] id = Base64UrlEncoder.DecodeBytes((string)dictionary["credentialId"]);
+            bool isResidentCredential = (bool)dictionary["isResidentCredential"];
+            string? rpId = dictionary.TryGetValue("rpId", out object? r) ? (string)r : null;
+            string privateKey = (string)dictionary["privateKey"];
+            byte[]? userHandle = dictionary.TryGetValue("userHandle", out object? u) ? Base64UrlEncoder.DecodeBytes((string)u) : null;
+            int signCount = (int)(long)dictionary["signCount"];
+
+            return new Credential(id, isResidentCredential, rpId, privateKey, userHandle, signCount);
         }
 
         /// <summary>
@@ -145,11 +129,11 @@ public Dictionary<string, object> ToDictionary()
             Dictionary<string, object> toReturn = new Dictionary<string, object>();
 
             toReturn["credentialId"] = Base64UrlEncoder.Encode(this.id);
-            toReturn["isResidentCredential"] = this.isResidentCredential;
-            toReturn["rpId"] = this.rpId;
-            toReturn["privateKey"] = this.privateKey;
-            toReturn["signCount"] = this.signCount;
-            if (this.userHandle != null)
+            toReturn["isResidentCredential"] = this.IsResidentCredential;
+            toReturn["rpId"] = this.RpId;
+            toReturn["privateKey"] = this.PrivateKey;
+            toReturn["signCount"] = this.SignCount;
+            if (this.userHandle is not null)
             {
                 toReturn["userHandle"] = Base64UrlEncoder.Encode(this.userHandle);
             }
diff --git a/dotnet/src/webdriver/VirtualAuth/IHasVirtualAuthenticator.cs b/dotnet/src/webdriver/VirtualAuth/IHasVirtualAuthenticator.cs
index 92619ded68acb..2d8616416f90e 100644
--- a/dotnet/src/webdriver/VirtualAuth/IHasVirtualAuthenticator.cs
+++ b/dotnet/src/webdriver/VirtualAuth/IHasVirtualAuthenticator.cs
@@ -17,8 +17,11 @@
 // under the License.
 // </copyright>
 
+using System;
 using System.Collections.Generic;
 
+#nullable enable
+
 namespace OpenQA.Selenium.VirtualAuth
 {
     /// <summary>
@@ -31,18 +34,23 @@ public interface IHasVirtualAuthenticator
         /// </summary>
         /// <param name="options">The VirtualAuthenticatorOptions to use in creating the authenticator.</param>
         /// <returns>The ID of the added virtual authenticator.</returns>
+        /// <exception cref="ArgumentNullException">If <paramref name="options"/> is <see langword="null"/>.</exception>
         string AddVirtualAuthenticator(VirtualAuthenticatorOptions options);
 
         /// <summary>
         /// Removes a virtual authenticator.
         /// </summary>
         /// <param name="id">The ID of the virtual authenticator to remove.</param>
+        /// <exception cref="ArgumentNullException">If <paramref name="id"/> is <see langword="null"/>.</exception>
+        /// <exception cref="WebDriverArgumentException">If the specified virtual authenticator does not exist.</exception>
         void RemoveVirtualAuthenticator(string id);
 
         /// <summary>
         /// Adds a credential to the virtual authenticator.
         /// </summary>
         /// <param name="credential">The credential to add to the authenticator.</param>
+        /// <exception cref="ArgumentNullException">If <paramref name="credential"/> is <see langword="null"/>.</exception>
+        /// <exception cref="InvalidOperationException">If a Virtual Authenticator has not been added yet.</exception>
         void AddCredential(Credential credential);
 
         /// <summary>
@@ -55,23 +63,29 @@ public interface IHasVirtualAuthenticator
         /// Removes a credential from the virtual authenticator.
         /// </summary>
         /// <param name="credentialId">A byte array representing the ID of the credential to remove.</param>
+        /// <exception cref="ArgumentNullException">If <paramref name="credentialId"/> is <see langword="null"/>.</exception>
+        /// <exception cref="InvalidOperationException">If a Virtual Authenticator has not been added yet.</exception>
         void RemoveCredential(byte[] credentialId);
 
         /// <summary>
         /// Removes a credential from the virtual authenticator.
         /// </summary>
         /// <param name="credentialId">A string representing the ID of the credential to remove.</param>
+        /// <exception cref="ArgumentNullException">If <paramref name="credentialId"/> is <see langword="null"/>.</exception>
+        /// <exception cref="InvalidOperationException">If a Virtual Authenticator has not been added yet.</exception>
         void RemoveCredential(string credentialId);
 
         /// <summary>
         /// Removes all credentials registered to this virtual authenticator.
         /// </summary>
+        /// <exception cref="InvalidOperationException">If a Virtual Authenticator has not been added yet.</exception>
         void RemoveAllCredentials();
 
         /// <summary>
         /// Sets whether or not a user is verified in this virtual authenticator.
         /// </summary>
         /// <param name="verified"><see langword="true"/> if the user is verified; otherwise <see langword="false"/>.</param>
+        /// <exception cref="InvalidOperationException">If a Virtual Authenticator has not been added yet.</exception>
         void SetUserVerified(bool verified);
     }
 }
diff --git a/dotnet/src/webdriver/VirtualAuth/VirtualAuthenticatorOptions.cs b/dotnet/src/webdriver/VirtualAuth/VirtualAuthenticatorOptions.cs
index 6e118df89e037..502d1368f9d0f 100644
--- a/dotnet/src/webdriver/VirtualAuth/VirtualAuthenticatorOptions.cs
+++ b/dotnet/src/webdriver/VirtualAuth/VirtualAuthenticatorOptions.cs
@@ -20,6 +20,8 @@
 using System;
 using System.Collections.Generic;
 
+#nullable enable
+
 namespace OpenQA.Selenium.VirtualAuth
 {
     /// <summary>
@@ -78,10 +80,13 @@ public static class Transport
         private bool isUserVerified = false;
 
         /// <summary>
-        /// Sets the protocol the Virtual Authenticator speaks
+        /// Sets the Client to Authenticator Protocol (CTAP) this <see href="https://www.w3.org/TR/webauthn-2/#sctn-automation-virtual-authenticators">Virtual Authenticator</see> speaks.
         /// </summary>
-        /// <param name="protocol">Valid protocol value</param>
-        /// <returns>VirtualAuthenticatorOptions</returns>
+        /// <param name="protocol">The CTAP protocol identifier.</param>
+        /// <returns>This options instance for chaining.</returns>
+        /// <remarks>Valid protocols are available on the <see cref="Protocol"/> type.</remarks>
+        /// <exception cref="ArgumentException">If <paramref name="protocol"/> is not a supported protocol value.</exception>
+        /// <completionlist cref="Protocol"/>
         public VirtualAuthenticatorOptions SetProtocol(string protocol)
         {
             if (string.Equals(Protocol.CTAP2, protocol) || string.Equals(Protocol.U2F, protocol))
@@ -92,15 +97,19 @@ public VirtualAuthenticatorOptions SetProtocol(string protocol)
             else
             {
                 throw new ArgumentException("Enter a valid protocol value." +
-                "Refer to https://www.w3.org/TR/webauthn-2/#sctn-automation-virtual-authenticators for supported protocols.");
+                    "Refer to https://www.w3.org/TR/webauthn-2/#sctn-automation-virtual-authenticators for supported protocols.");
             }
         }
 
         /// <summary>
-        /// Sets the transport authenticator needs to implement to communicate with clients
+        /// Sets the <see href="https://www.w3.org/TR/webauthn-2/#enum-transport">Authenticator Transport</see> this <see href="https://www.w3.org/TR/webauthn-2/#sctn-automation-virtual-authenticators">Virtual Authenticator</see> needs to implement, to communicate with clients.
         /// </summary>
-        /// <param name="transport">Valid transport value</param>
-        /// <returns>VirtualAuthenticatorOptions</returns>
+        /// <param name="transport">Valid transport value.
+        /// </param>
+        /// <returns>This options instance for chaining.</returns>
+        /// <remarks>Valid protocols are available on the <see cref="Transport"/> type.</remarks>
+        /// <exception cref="ArgumentException">If <paramref name="transport"/> is not a supported transport value.</exception>
+        /// <completionlist cref="Transport"/>
         public VirtualAuthenticatorOptions SetTransport(string transport)
         {
             if (Transport.BLE == transport || Transport.INTERNAL == transport || Transport.NFC == transport || Transport.USB == transport)
@@ -111,16 +120,15 @@ public VirtualAuthenticatorOptions SetTransport(string transport)
             else
             {
                 throw new ArgumentException("Enter a valid transport value." +
-                "Refer to https://www.w3.org/TR/webauthn-2/#enum-transport for supported transport values.");
+                    "Refer to https://www.w3.org/TR/webauthn-2/#enum-transport for supported transport values.");
             }
         }
 
         /// <summary>
-        /// If set to true the authenticator will support client-side discoverable credentials.
-        /// Refer https://w3c.github.io/webauthn/#client-side-discoverable-credential
+        /// If set to <see langword="true"/>, the authenticator will support <see href="https://w3c.github.io/webauthn/#client-side-discoverable-credential">Client-side discoverable Credentials</see>.
         /// </summary>
-        /// <param name="hasResidentKey">boolean value to set</param>
-        /// <returns>VirtualAuthenticatorOptions</returns>
+        /// <param name="hasResidentKey">Whether authenticator will support client-side discoverable credentials.</param>
+        /// <returns>This options instance for chaining.</returns>
         public VirtualAuthenticatorOptions SetHasResidentKey(bool hasResidentKey)
         {
             this.hasResidentKey = hasResidentKey;
@@ -128,11 +136,10 @@ public VirtualAuthenticatorOptions SetHasResidentKey(bool hasResidentKey)
         }
 
         /// <summary>
-        /// If set to true, the authenticator supports user verification.
-        /// Refer https://w3c.github.io/webauthn/#user-verification.
+        /// If set to <see langword="true"/>, the authenticator will support <see href="https://w3c.github.io/webauthn/#user-verification">User Verification</see>.
         /// </summary>
-        /// <param name="hasUserVerification">boolean value to set</param>
-        /// <returns></returns>
+        /// <param name="hasUserVerification">Whether the authenticator supports user verification.</param>
+        /// <returns>This options instance for chaining.</returns>
         public VirtualAuthenticatorOptions SetHasUserVerification(bool hasUserVerification)
         {
             this.hasUserVerification = hasUserVerification;
@@ -140,11 +147,10 @@ public VirtualAuthenticatorOptions SetHasUserVerification(bool hasUserVerificati
         }
 
         /// <summary>
-        /// If set to true, a user consent will always be granted.
-        /// Refer https://w3c.github.io/webauthn/#user-consent
+        /// If set to <see langword="true"/>, a <see href="https://w3c.github.io/webauthn/#user-consent">User Consent</see> will always be granted.
         /// </summary>
-        /// <param name="isUserConsenting">boolean value to set</param>
-        /// <returns>VirtualAuthenticatorOptions</returns>
+        /// <param name="isUserConsenting">Whether a user consent will always be granted.</param>
+        /// <returns>This options instance for chaining.</returns>
         public VirtualAuthenticatorOptions SetIsUserConsenting(bool isUserConsenting)
         {
             this.isUserConsenting = isUserConsenting;
@@ -152,11 +158,10 @@ public VirtualAuthenticatorOptions SetIsUserConsenting(bool isUserConsenting)
         }
 
         /// <summary>
-        /// If set to true, User Verification will always succeed.
-        /// Refer https://w3c.github.io/webauthn/#user-verification
+        /// If set to <see langword="true"/>, <see href="https://w3c.github.io/webauthn/#user-verification">User Verification</see> will always succeed.
         /// </summary>
-        /// <param name="isUserVerified">boolean value to set</param>
-        /// <returns>VirtualAuthenticatorOptions</returns>
+        /// <param name="isUserVerified">Whether User Verification will always succeed.</param>
+        /// <returns>This options instance for chaining.</returns>
         public VirtualAuthenticatorOptions SetIsUserVerified(bool isUserVerified)
         {
             this.isUserVerified = isUserVerified;
@@ -164,7 +169,7 @@ public VirtualAuthenticatorOptions SetIsUserVerified(bool isUserVerified)
         }
 
         /// <summary>
-        /// Serializes this set of options to a dictionary of key-value pairs.
+        /// Serializes this set of options into a dictionary of key-value pairs.
         /// </summary>
         /// <returns>The dictionary containing the values of this set of options.</returns>
         public Dictionary<string, object> ToDictionary()
diff --git a/dotnet/src/webdriver/WebDriver.cs b/dotnet/src/webdriver/WebDriver.cs
index d9960ea97b235..9bcca8b28f8b3 100644
--- a/dotnet/src/webdriver/WebDriver.cs
+++ b/dotnet/src/webdriver/WebDriver.cs
@@ -1036,44 +1036,68 @@ private object ParseJavaScriptReturnValue(object responseValue)
             return returnValue;
         }
 
+#nullable enable
+
         /// <summary>
         /// Creates a Virtual Authenticator.
         /// </summary>
-        /// <param name="options"> VirtualAuthenticator Options (https://w3c.github.io/webauthn/#sctn-automation-virtual-authenticators)</param>
+        /// <param name="options"><see href="https://w3c.github.io/webauthn/#sctn-automation-virtual-authenticators">Virtual Authenticator Options</see>.</param>
         /// <returns> Authenticator id as string </returns>
+        /// <exception cref="ArgumentNullException">If <paramref name="options"/> is <see langword="null"/>.</exception>
         public string AddVirtualAuthenticator(VirtualAuthenticatorOptions options)
         {
+            if (options is null)
+            {
+                throw new ArgumentNullException(nameof(options));
+            }
+
             Response commandResponse = this.Execute(DriverCommand.AddVirtualAuthenticator, options.ToDictionary());
-            string id = commandResponse.Value.ToString();
+            string id = (string)commandResponse.Value!;
             this.AuthenticatorId = id;
-            return this.AuthenticatorId;
+            return id;
         }
 
         /// <summary>
         /// Removes the Virtual Authenticator
         /// </summary>
-        /// <param name="authenticatorId"> Id as string that uniquely identifies a Virtual Authenticator</param>
+        /// <param name="authenticatorId">Id as string that uniquely identifies a Virtual Authenticator.</param>
+        /// <exception cref="ArgumentNullException">If <paramref name="authenticatorId"/> is <see langword="null"/>.</exception>
         public void RemoveVirtualAuthenticator(string authenticatorId)
         {
+            if (authenticatorId is null)
+            {
+                throw new ArgumentNullException(nameof(authenticatorId));
+            }
+
             Dictionary<string, object> parameters = new Dictionary<string, object>();
-            parameters.Add("authenticatorId", this.AuthenticatorId);
+            parameters.Add("authenticatorId", authenticatorId);
+
             this.Execute(DriverCommand.RemoveVirtualAuthenticator, parameters);
             this.AuthenticatorId = null;
         }
 
         /// <summary>
-        /// Gets the virtual authenticator ID for this WebDriver instance.
+        /// Gets the cached virtual authenticator ID, or <see langword="null"/> if no authenticator ID is set.
         /// </summary>
-        public string AuthenticatorId { get; private set; }
+        public string? AuthenticatorId { get; private set; }
 
         /// <summary>
         /// Add a credential to the Virtual Authenticator/
         /// </summary>
         /// <param name="credential"> The credential to be stored in the Virtual Authenticator</param>
+        /// <exception cref="ArgumentNullException">If <paramref name="credential"/> is <see langword="null"/>.</exception>
+        /// <exception cref="InvalidOperationException">If a Virtual Authenticator has not been added yet.</exception>
         public void AddCredential(Credential credential)
         {
+            if (credential is null)
+            {
+                throw new ArgumentNullException(nameof(credential));
+            }
+
+            string authenticatorId = this.AuthenticatorId ?? throw new InvalidOperationException("Virtual Authenticator needs to be added before it can perform operations");
+
             Dictionary<string, object> parameters = new Dictionary<string, object>(credential.ToDictionary());
-            parameters.Add("authenticatorId", this.AuthenticatorId);
+            parameters.Add("authenticatorId", authenticatorId);
 
             this.Execute(driverCommandToExecute: DriverCommand.AddCredential, parameters);
         }
@@ -1082,18 +1106,25 @@ public void AddCredential(Credential credential)
         /// Retrieves all the credentials stored in the Virtual Authenticator
         /// </summary>
         /// <returns> List of credentials </returns>
+        /// <exception cref="InvalidOperationException">If a Virtual Authenticator has not been added yet.</exception>
         public List<Credential> GetCredentials()
         {
+            string authenticatorId = this.AuthenticatorId ?? throw new InvalidOperationException("Virtual Authenticator needs to be added before it can perform operations");
+
             Dictionary<string, object> parameters = new Dictionary<string, object>();
-            parameters.Add("authenticatorId", this.AuthenticatorId);
+            parameters.Add("authenticatorId", authenticatorId);
 
-            object[] commandResponse = (object[])this.Execute(driverCommandToExecute: DriverCommand.GetCredentials, parameters).Value;
+            Response getCredentialsResponse = this.Execute(driverCommandToExecute: DriverCommand.GetCredentials, parameters);
 
-            List<Credential> credentials = new List<Credential>();
+            if (getCredentialsResponse.Value is not object?[] credentialsList)
+            {
+                throw new WebDriverException($"Get credentials call succeeded, but the response was not a list of credentials: {getCredentialsResponse.Value}");
+            }
 
-            foreach (object dictionary in commandResponse)
+            List<Credential> credentials = new List<Credential>(credentialsList.Length);
+            foreach (object? dictionary in credentialsList)
             {
-                Credential credential = Credential.FromDictionary((Dictionary<string, object>)dictionary);
+                Credential credential = Credential.FromDictionary((Dictionary<string, object>)dictionary!);
                 credentials.Add(credential);
             }
 
@@ -1104,6 +1135,8 @@ public List<Credential> GetCredentials()
         /// Removes the credential identified by the credentialId from the Virtual Authenticator.
         /// </summary>
         /// <param name="credentialId"> The id as byte array that uniquely identifies a credential </param>
+        /// <exception cref="ArgumentNullException">If <paramref name="credentialId"/> is <see langword="null"/>.</exception>
+        /// <exception cref="InvalidOperationException">If a Virtual Authenticator has not been added yet.</exception>
         public void RemoveCredential(byte[] credentialId)
         {
             RemoveCredential(Base64UrlEncoder.Encode(credentialId));
@@ -1113,10 +1146,19 @@ public void RemoveCredential(byte[] credentialId)
         /// Removes the credential identified by the credentialId from the Virtual Authenticator.
         /// </summary>
         /// <param name="credentialId"> The id as string that uniquely identifies a credential </param>
+        /// <exception cref="ArgumentNullException">If <paramref name="credentialId"/> is <see langword="null"/>.</exception>
+        /// <exception cref="InvalidOperationException">If a Virtual Authenticator has not been added yet.</exception>
         public void RemoveCredential(string credentialId)
         {
+            if (credentialId is null)
+            {
+                throw new ArgumentNullException(nameof(credentialId));
+            }
+
+            string authenticatorId = this.AuthenticatorId ?? throw new InvalidOperationException("Virtual Authenticator needs to be added before it can perform operations");
+
             Dictionary<string, object> parameters = new Dictionary<string, object>();
-            parameters.Add("authenticatorId", this.AuthenticatorId);
+            parameters.Add("authenticatorId", authenticatorId);
             parameters.Add("credentialId", credentialId);
 
             this.Execute(driverCommandToExecute: DriverCommand.RemoveCredential, parameters);
@@ -1125,10 +1167,13 @@ public void RemoveCredential(string credentialId)
         /// <summary>
         /// Removes all the credentials stored in the Virtual Authenticator.
         /// </summary>
+        /// <exception cref="InvalidOperationException">If a Virtual Authenticator has not been added yet.</exception>
         public void RemoveAllCredentials()
         {
+            string authenticatorId = this.AuthenticatorId ?? throw new InvalidOperationException("Virtual Authenticator needs to be added before it can perform operations");
+
             Dictionary<string, object> parameters = new Dictionary<string, object>();
-            parameters.Add("authenticatorId", this.AuthenticatorId);
+            parameters.Add("authenticatorId", authenticatorId);
 
             this.Execute(driverCommandToExecute: DriverCommand.RemoveAllCredentials, parameters);
         }
@@ -1139,8 +1184,10 @@ public void RemoveAllCredentials()
         /// <param name="verified">The boolean value representing value to be set </param>
         public void SetUserVerified(bool verified)
         {
+            string authenticatorId = this.AuthenticatorId ?? throw new InvalidOperationException("Virtual Authenticator needs to be added before it can perform operations");
+
             Dictionary<string, object> parameters = new Dictionary<string, object>();
-            parameters.Add("authenticatorId", this.AuthenticatorId);
+            parameters.Add("authenticatorId", authenticatorId);
             parameters.Add("isUserVerified", verified);
 
             this.Execute(driverCommandToExecute: DriverCommand.SetUserVerified, parameters);


Test patch:
diff --git a/dotnet/test/common/VirtualAuthn/VirtualAuthenticatorTest.cs b/dotnet/test/common/VirtualAuthn/VirtualAuthenticatorTest.cs
index e6c16e11b673a..c46d541dcb548 100644
--- a/dotnet/test/common/VirtualAuthn/VirtualAuthenticatorTest.cs
+++ b/dotnet/test/common/VirtualAuthn/VirtualAuthenticatorTest.cs
@@ -194,6 +194,26 @@ public void ShouldRemoveAuthenticator()
             Assert.IsNull(webDriver.AuthenticatorId);
         }
 
+        [Test]
+        [NeedsFreshDriver(IsCreatedAfterTest = true)]
+        [IgnoreBrowser(Selenium.Browser.IE, "IE does not support Virtual Authenticator")]
+        [IgnoreBrowser(Selenium.Browser.Firefox, "Firefox does not support Virtual Authenticator")]
+        [IgnoreBrowser(Selenium.Browser.Safari, "Safari does not support Virtual Authenticator")]
+        public void ShouldSupportMultipleVirtualAuthenticatorsAtOnce()
+        {
+            VirtualAuthenticatorOptions options = new VirtualAuthenticatorOptions();
+
+            string authenticatorId1 = webDriver.AddVirtualAuthenticator(options);
+            Assert.That(webDriver.AuthenticatorId, Is.EqualTo(authenticatorId1));
+
+            string authenticatorId2 = webDriver.AddVirtualAuthenticator(options);
+
+            webDriver.RemoveVirtualAuthenticator(authenticatorId1);
+            webDriver.RemoveVirtualAuthenticator(authenticatorId2);
+
+            Assert.IsNull(webDriver.AuthenticatorId);
+        }
+
         [Test]
         [NeedsFreshDriver(IsCreatedAfterTest = true)]
         [IgnoreBrowser(Selenium.Browser.IE, "IE does not support Virtual Authenticator")]
@@ -302,7 +322,9 @@ public void AddResidentCredentialNotSupportedWhenAuthenticatorUsesU2FProtocol()
             byte[] userHandle = { 1 };
             Credential credential = Credential.CreateResidentCredential(
               credentialId, "localhost", base64EncodedEC256PK, userHandle, /*signCount=*/0);
-            Assert.Throws<WebDriverArgumentException>(() => webDriver.AddCredential(credential));
+            Assert.That(
+                () => webDriver.AddCredential(credential),
+                Throws.TypeOf<WebDriverArgumentException>().With.Message.Contains("The Authenticator does not support Resident Credentials."));
         }
 
         [Test]
@@ -492,5 +514,37 @@ public void testSetUserVerified()
 
             Assert.True(error.StartsWith("NotAllowedError"));
         }
+
+        [Test]
+        [NeedsFreshDriver(IsCreatedAfterTest = true)]
+        [IgnoreBrowser(Selenium.Browser.IE, "IE does not support Virtual Authenticator")]
+        [IgnoreBrowser(Selenium.Browser.Firefox, "Firefox does not support Virtual Authenticator")]
+        [IgnoreBrowser(Selenium.Browser.Safari, "Safari does not support Virtual Authenticator")]
+        public void ShouldThrowOnInvalidArguments()
+        {
+            Assert.That(
+                () => webDriver.AddVirtualAuthenticator(null),
+                Throws.ArgumentNullException);
+
+            Assert.That(
+                () => webDriver.RemoveVirtualAuthenticator(null),
+                Throws.ArgumentNullException);
+
+            Assert.That(
+                () => webDriver.AddCredential(null),
+                Throws.ArgumentNullException);
+
+            Assert.That(
+               () => webDriver.RemoveCredential((byte[])null),
+               Throws.ArgumentNullException);
+
+            Assert.That(
+                () => webDriver.RemoveCredential((string)null),
+                Throws.ArgumentNullException);
+
+            Assert.That(
+               () => webDriver.RemoveVirtualAuthenticator("non-existant"),
+               Throws.TypeOf<WebDriverArgumentException>());
+        }
     }
 }