Re-using existing browser session in Selenium using C#

Earlier I wrote an article on how to re-use a session in Selenium using Java. @Jim Hazen asked if the I could provide the implementation of same in C#. So here it is

The first time I worked out this approach was in C# only, but that was back in 2014. That time I copied code from the Selenium source code and modified it. This makes upgrading Selenium version difficult. So I would reattempt to do this again in a better way.

Let’s start

var driver = new ChromeDriver();

As discussed in previous articles. Two things that we need is the Session Id and the Executor url to be able to re-create the driver.

Getting the Session Id

Getting the Session Id is quite simple

Console.WriteLine(driver.SessionId.ToString());

Getting the Executor URL

Getting the Executor URL is not straight forward. So we will see how to get it in multiple steps

If we look at the RemoteWebDriver class source code we will find the executor is stored in a private field

private ICommandExecutor executor;

To get a private field we need to use Reflection concepts in C#

var executorField = driver.GetType().GetField("executor", BindingFlags.NonPublic |BindingFlags.Instance);

The executorField value will come as null. The reason this happens is that we had initiated the driver as a ChromeDriver and the private field is of RemoteWebDriver which means it is not accesible to the ChromeDriver class also. So we need to go to it’s base class to fetch the executor field.

So we update our code as below

var executorField = driver.GetType().GetField("executor", BindingFlags.NonPublic |BindingFlags.Instance);
if (executorField == null )
{
    executorField = driver.GetType().BaseType.GetField("executor", BindingFlags.NonPublic |BindingFlags.Instance);
}

object executor = executorField.GetValue(driver);

Now if we look the executor object value. It is of type OpenQA.Selenium.Remote.DriverServiceCommandExecutor. If we look at the source code of this class

internal class DriverServiceCommandExecutor : ICommandExecutor
{
    private DriverService service;

    private HttpCommandExecutor internalExecutor;

This is a internal class, so we can’t cast this object. Also since the internalExecutor is a private field, we anyways have to use reflection further.

var internalExecutorField = executor.GetType().GetField("internalExecutor", BindingFlags.Instance | BindingFlags.NonPublic);
object internalExecutor = internalExecutorField.GetValue(executor);

The internalExecutor object has a type of OpenQA.Selenium.Remote.HttpCommandExecutor. The source code of the class is as below

internal class HttpCommandExecutor : ICommandExecutor
{
    private const string JsonMimeType = "application/json";

    private const string ContentTypeHeader = "application/json;charset=utf-8";

    private const string RequestAcceptHeader = "application/json, image/png";

    private Uri remoteServerUri;

So there is the field we are interested in remoteServerUri. We can get it the same way we got remoteServerUri.

var remoteServerUriField = internalExecutor.GetType().GetField("remoteServerUri", BindingFlags.Instance | BindingFlags.NonPublic);
var remoteServerUri = remoteServerUriField.GetValue(internalExecutor) as Uri;

The value of remoteServerUri comes out be http://localhost:52600/ in my case. This would change everytime we launch a new driver.

Here is a refactored and more polished version of the code we wrote

public static Uri GetExecutorURLFromDriver(OpenQA.Selenium.Remote.RemoteWebDriver driver)
{
    var executorField = typeof(OpenQA.Selenium.Remote.RemoteWebDriver)
        .GetField("executor",
                  System.Reflection.BindingFlags.NonPublic
                  | System.Reflection.BindingFlags.Instance);
    
    object executor = executorField.GetValue(driver);
    
    var internalExecutorField = executor.GetType()
        .GetField("internalExecutor",
                  System.Reflection.BindingFlags.NonPublic
                  | System.Reflection.BindingFlags.Instance);
    object internalExecutor = internalExecutorField.GetValue(executor);
    
    //executor.CommandInfoRepository
    var remoteServerUriField = internalExecutor.GetType()
        .GetField("remoteServerUri",
                  System.Reflection.BindingFlags.NonPublic
                  | System.Reflection.BindingFlags.Instance);
    var remoteServerUri = remoteServerUriField.GetValue(internalExecutor) as Uri;
    
    return remoteServerUri;
}

So now we have solved the first issue, which is to have the Session Id and Exeuctor URL for us to save before reconstructing the WebDriver next time.

Reconstructing the driver

So we have what we need to re-create the driver, now it is for us to look at what we need from a C# language perspective. We need to create a new class with base class OpenQA.Selenium.Remote.RemoteWebDriver class and override the execute method to change the NewSession command response

public class ReuseRemoteWebDriver: OpenQA.Selenium.Remote.RemoteWebDriver{
    private String _sessionId;
    
    public ReuseRemoteWebDriver(Uri remoteAddress, String sessionId)
        :base( remoteAddress,  new OpenQA.Selenium.Remote.DesiredCapabilities()) {
        this._sessionId = sessionId;
    }
    
    protected override OpenQA.Selenium.Remote.Response 
        Execute(string driverCommandToExecute, System.Collections.Generic.Dictionary<string, object> parameters)
    {
        if (driverCommandToExecute == OpenQA.Selenium.Remote.DriverCommand.NewSession)
        {
            var resp =  new OpenQA.Selenium.Remote.Response();
            resp.Status = OpenQA.Selenium.WebDriverResult.Success;
            resp.SessionId = this._sessionId;
            resp.Value = new System.Collections.Generic.Dictionary<String, Object>();
            return resp;
        }
        var respBase = base.Execute(driverCommandToExecute, parameters);
        return respBase;
    }
}

This code creates a the driver successfully. Now let’s test it.

var driverReUse = new ReuseRemoteWebDriver(remoteUri, driverChrome.SessionId.ToString());
driverReUse.Url = "http://tarunlalwani.com";

Console.WriteLine(driverReUse.Url);

The line of code to set Url of the driver doesn’t do anything, but doesn’t error too. On printing the Url a exception is raised

OpenQA.Selenium.WebDriverException: no such session
  (Driver info: chromedriver=2.30.477700 (0057494ad8732195794a7b32078424f92a5fce41),platform=Windows NT 6.1.7601 SP1 x86_64)

   at OpenQA.Selenium.Remote.RemoteWebDriver.UnpackAndThrowOnError(Response errorResponse)
   at OpenQA.Selenium.Remote.RemoteWebDriver.Execute(String driverCommandToExecute, Dictionary`2 parameters)
   at SeleniumReuseSession.ReuseRemoteWebDriver.Execute(String driverCommandToExecute, Dictionary`2 parameters) in c:\Users\tarun\Documents\SharpDevelop Projects\SeleniumReuseSession\SeleniumReuseSession\Program.cs:line 40
   at OpenQA.Selenium.Remote.RemoteWebDriver.get_Url()
   at SeleniumReuseSession.Program.Main(String[] args) in c:\Users\tarun\Documents\SharpDevelop Projects\SeleniumReuseSession\SeleniumReuseSession\Program.cs:line 83

After debugging the code, I found the issue. The problem is that the base class constructor is called first and then the line this._sessionId = sessionId. The base constructor calls the Execute method. So when we set the sessionId in the dummy response, the ID is null as our constructor code has not been called yet.

Now this is the way constructor are suppose to work and there is no workaround to this behavior. So we need to dig into the constructor to find our workaround

The constructor calls the StartSession method which in turn executes the NewSession command and then save the session in sessionId private field.

//https://github.com/SeleniumHQ/selenium/blob/master/dotnet/src/webdriver/Remote/RemoteWebDriver.cs#L1100

protected void StartSession(ICapabilities desiredCapabilities)
{
    Dictionary<string, object> parameters = new Dictionary<string, object>();
    parameters.Add("desiredCapabilities", this.GetLegacyCapabilitiesDictionary(desiredCapabilities));

    Dictionary<string, object> firstMatchCapabilities = this.GetCapabilitiesDictionary(desiredCapabilities);

    List<object> firstMatchCapabilitiesList = new List<object>();
    firstMatchCapabilitiesList.Add(firstMatchCapabilities);

    Dictionary<string, object> specCompliantCapabilities = new Dictionary<string, object>();
    specCompliantCapabilities["firstMatch"] = firstMatchCapabilitiesList;
    parameters.Add("capabilities", specCompliantCapabilities);

    Response response = this.Execute(DriverCommand.NewSession, parameters);

    Dictionary<string, object> rawCapabilities = (Dictionary<string, object>)response.Value;
    DesiredCapabilities returnedCapabilities = new DesiredCapabilities(rawCapabilities);
    this.capabilities = returnedCapabilities;
    this.sessionId = new SessionId(response.SessionId);
}

So basically when our constructor gets called and the sessionId on our base class is set to null because of our overriden response. The fix is to set the sessionId in our constructor. So if update the constructor as below

public ReuseRemoteWebDriver(Uri remoteAddress, String sessionId)
    :base( remoteAddress,  new OpenQA.Selenium.Remote.DesiredCapabilities()) {
    this._sessionId = sessionId;
    var sessionIdBase = this.GetType()
        .BaseType
        .GetField("sessionId",
                  System.Reflection.BindingFlags.Instance |
                  System.Reflection.BindingFlags.NonPublic);
    sessionIdBase.SetValue(this, new OpenQA.Selenium.Remote.SessionId(sessionId));
}

The finally updated code for our class is as below

public class ReuseRemoteWebDriver: OpenQA.Selenium.Remote.RemoteWebDriver{
    private String _sessionId;
    
    public ReuseRemoteWebDriver(Uri remoteAddress, String sessionId)
        :base( remoteAddress,  new OpenQA.Selenium.Remote.DesiredCapabilities()) {
        this._sessionId = sessionId;
        var sessionIdBase = this.GetType()
            .BaseType
            .GetField("sessionId",
                      System.Reflection.BindingFlags.Instance |
                      System.Reflection.BindingFlags.NonPublic);
        sessionIdBase.SetValue(this, new OpenQA.Selenium.Remote.SessionId(sessionId));
    }
    
    protected override OpenQA.Selenium.Remote.Response 
        Execute(string driverCommandToExecute, System.Collections.Generic.Dictionary<string, object> parameters)
    {
        if (driverCommandToExecute == OpenQA.Selenium.Remote.DriverCommand.NewSession)
        {
            var resp =  new OpenQA.Selenium.Remote.Response();
            resp.Status = OpenQA.Selenium.WebDriverResult.Success;
            resp.SessionId = this._sessionId;
            resp.Value = new System.Collections.Generic.Dictionary<String, Object>();
            return resp;
        }
        var respBase = base.Execute(driverCommandToExecute, parameters);
        return respBase;
    }
}

And this version of code works great. Enjoy!