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.in";
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!