Swimburger

Download the right ChromeDriver version & keep it up to date on Windows/Linux/macOS using C# .NET

Niels Swimberghe

Niels Swimberghe - - .NET

Follow me on Twitter, buy me a coffee

C# and Chrome logo surrounding title: Download the right ChromeDriver version using C# .NET

Update (Sep 2023): The old code for downloading the ChromeDriver does not work for versions 115 and newer because Chrome changed their process. You can find the updated C# code for newer versions in the associate GitHub repository.

You can run automated UI browser tests using technologies like Selenium. The UI testing technology will communicate with a "webdriver" which will, in turn, drive around the browser.
ChromeDriver is the webdriver implementation for Google Chrome. ChromeDriver and Selenium work together very well, but given enough time you will run into the following error:

Unhandled exception. System.InvalidOperationException: session not created: This version of ChromeDriver only supports Chrome version 74
  (Driver info: chromedriver=74.0.3729.6 (255758eccf3d244491b8a1317aa76e1ce10d57e9-refs/branch-heads/3729@{#29}),platform=Windows NT 10.0.19042 x86_64) (SessionNotCreated)
   at OpenQA.Selenium.Remote.RemoteWebDriver.UnpackAndThrowOnError(Response errorResponse)
   at OpenQA.Selenium.Remote.RemoteWebDriver.Execute(String driverCommandToExecute, Dictionary`2 parameters)
   at OpenQA.Selenium.Remote.RemoteWebDriver.StartSession(ICapabilities desiredCapabilities)
   at OpenQA.Selenium.Remote.RemoteWebDriver..ctor(ICommandExecutor commandExecutor, ICapabilities desiredCapabilities)
   at OpenQA.Selenium.Chrome.ChromeDriver..ctor(ChromeDriverService service, ChromeOptions options, TimeSpan commandTimeout)
   at OpenQA.Selenium.Chrome.ChromeDriver..ctor(ChromeOptions options)
   at OpenQA.Selenium.Chrome.ChromeDriver..ctor()
   at SeleniumConsole.Program.Main(String[] args) in C:\Users\niels\source\repos\SeleniumConsole\Program.cs:line 10

Google Chrome is very good about updating very frequently, often leaving the ChromeDriver out of date. When the ChromeDriver is incompatible with the installed version of Google Chrome, you will run into the error above.
The fix is pretty simple, go back to the ChromeDriver website and download the most recent version. But doing this manually every time Chrome updates will quickly become unmanageable.
Especially when you run UI tests on multiple servers, on a periodic basis, or inside a continuous integration and deployment pipeline. Even worse, the failure of these tests may be connected to email, SMS, and/or phone alerting systems.

How to download the correct version of Chrome

Luckily, the ChromeDriver website provides a systematic way of downloading the correct version of the ChromeDriver given a specific version of Google Chrome. 
Here are the instructions provided:

  • First, find out which version of Chrome you are using. Let's say you have Chrome 72.0.3626.81.
  • Take the Chrome version number, remove the last part, and append the result to URL "https://chromedriver.storage.googleapis.com/LATEST_RELEASE_". For example, with Chrome version 72.0.3626.81, you'd get a URL "https://chromedriver.storage.googleapis.com/LATEST_RELEASE_72.0.3626".
  • Use the URL created in the last step to retrieve a small file containing the version of ChromeDriver to use. For example, the above URL will get your a file containing "72.0.3626.69". (The actual number may change in the future, of course.)
  • Use the version number retrieved from the previous step to construct the URL to download ChromeDriver. With version 72.0.3626.69, the URL would be "https://chromedriver.storage.googleapis.com/index.html?path=72.0.3626.69/".
  • After the initial download, it is recommended that you occasionally go through the above process again to see if there are any bug fix releases.

In the above steps, one small detail has been omitted. You have to download the correct file which will work on the operating system (OS) you're using. You will have the following three options to download on the URL determined in the steps above:

  • chromedriver_linux64.zip (for Linux)
  • chromedriver_mac64.zip (for macOS)
  • chromedriver_win32.zip (for Windows)

It's self-explanatory which file you need to download depending on your OS which is why it was probably omitted from the steps.  But you will need to keep this in account to automate this process.

Install the correct ChromeDriver using C# .NET

With the instructions provided by Google, you can piece together C# code to automate the installation of the ChromeDriver. Here's an example implementation:

using Microsoft.Win32;
using System;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Net;
using System.Net.Http;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Threading.Tasks;

public class ChromeDriverInstaller
{
    private static readonly HttpClient httpClient = new HttpClient
    {
        BaseAddress = new Uri("https://chromedriver.storage.googleapis.com/")
    };

    public Task Install() => Install(null, false);
    public Task Install(string chromeVersion) => Install(chromeVersion, false);
    public Task Install(bool forceDownload) => Install(null, forceDownload);

    public async Task Install(string chromeVersion, bool forceDownload)
    {
        // Instructions from https://chromedriver.chromium.org/downloads/version-selection
        //   First, find out which version of Chrome you are using. Let's say you have Chrome 72.0.3626.81.
        if (chromeVersion == null)
        {
            chromeVersion = await GetChromeVersion();
        }

        //   Take the Chrome version number, remove the last part, 
        chromeVersion = chromeVersion.Substring(0, chromeVersion.LastIndexOf('.'));

        //   and append the result to URL "https://chromedriver.storage.googleapis.com/LATEST_RELEASE_". 
        //   For example, with Chrome version 72.0.3626.81, you'd get a URL "https://chromedriver.storage.googleapis.com/LATEST_RELEASE_72.0.3626".
        var chromeDriverVersionResponse = await httpClient.GetAsync($"LATEST_RELEASE_{chromeVersion}");
        if (!chromeDriverVersionResponse.IsSuccessStatusCode)
        {
            if (chromeDriverVersionResponse.StatusCode == HttpStatusCode.NotFound)
            {
                throw new Exception($"ChromeDriver version not found for Chrome version {chromeVersion}");
            }
            else
            {
                throw new Exception($"ChromeDriver version request failed with status code: {chromeDriverVersionResponse.StatusCode}, reason phrase: {chromeDriverVersionResponse.ReasonPhrase}");
            }
        }

        var chromeDriverVersion = await chromeDriverVersionResponse.Content.ReadAsStringAsync();

        string zipName;
        string driverName;
        if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
        {
            zipName = "chromedriver_win32.zip";
            driverName = "chromedriver.exe";
        }
        else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
        {
            zipName = "chromedriver_linux64.zip";
            driverName = "chromedriver";
        }
        else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
        {
            zipName = "chromedriver_mac64.zip";
            driverName = "chromedriver";
        }
        else
        {
            throw new PlatformNotSupportedException("Your operating system is not supported.");
        }

        string targetPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
        targetPath = Path.Combine(targetPath, driverName);
        if (!forceDownload && File.Exists(targetPath))
        {
            using var process = Process.Start(
                new ProcessStartInfo
                {
                    FileName = targetPath,
                    ArgumentList = { "--version" },
                    UseShellExecute = false,
                    CreateNoWindow = true,
                    RedirectStandardOutput = true,
                    RedirectStandardError = true,
                }
            );
            string existingChromeDriverVersion = await process.StandardOutput.ReadToEndAsync();
            string error = await process.StandardError.ReadToEndAsync();
            await process.WaitForExitAsync();
            process.Kill(true);

            // expected output is something like "ChromeDriver 88.0.4324.96 (68dba2d8a0b149a1d3afac56fa74648032bcf46b-refs/branch-heads/4324@{#1784})"
            // the following line will extract the version number and leave the rest
            existingChromeDriverVersion = existingChromeDriverVersion.Split(" ")[1];
            if (chromeDriverVersion == existingChromeDriverVersion)
            {
                return;
            }

            if (!string.IsNullOrEmpty(error))
            {
                throw new Exception($"Failed to execute {driverName} --version");
            }
        }

        //   Use the URL created in the last step to retrieve a small file containing the version of ChromeDriver to use. For example, the above URL will get your a file containing "72.0.3626.69". (The actual number may change in the future, of course.)
        //   Use the version number retrieved from the previous step to construct the URL to download ChromeDriver. With version 72.0.3626.69, the URL would be "https://chromedriver.storage.googleapis.com/index.html?path=72.0.3626.69/".
        var driverZipResponse = await httpClient.GetAsync($"{chromeDriverVersion}/{zipName}");
        if (!driverZipResponse.IsSuccessStatusCode)
        {
            throw new Exception($"ChromeDriver download request failed with status code: {driverZipResponse.StatusCode}, reason phrase: {driverZipResponse.ReasonPhrase}");
        }

        // this reads the zipfile as a stream, opens the archive, 
        // and extracts the chromedriver executable to the targetPath without saving any intermediate files to disk
        using (var zipFileStream = await driverZipResponse.Content.ReadAsStreamAsync())
        using (var zipArchive = new ZipArchive(zipFileStream, ZipArchiveMode.Read))
        using (var chromeDriverWriter = new FileStream(targetPath, FileMode.Create))
        {
            var entry = zipArchive.GetEntry(driverName);
            using Stream chromeDriverStream = entry.Open();
            await chromeDriverStream.CopyToAsync(chromeDriverWriter);
        }

        // on Linux/macOS, you need to add the executable permission (+x) to allow the execution of the chromedriver
        if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
        {
            using var process = Process.Start(
                new ProcessStartInfo
                {
                    FileName = "chmod",
                    ArgumentList = { "+x", targetPath },
                    UseShellExecute = false,
                    CreateNoWindow = true,
                    RedirectStandardOutput = true,
                    RedirectStandardError = true,
                }
            );
            string error = await process.StandardError.ReadToEndAsync();
            await process.WaitForExitAsync();
            process.Kill(true);

            if (!string.IsNullOrEmpty(error))
            {
                throw new Exception("Failed to make chromedriver executable");
            }
        }
    }

    public async Task<string> GetChromeVersion()
    {
        if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
        {
            string chromePath = (string)Registry.GetValue("HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\chrome.exe", null, null);
            if (chromePath == null)
            {
                throw new Exception("Google Chrome not found in registry");
            }

            var fileVersionInfo = FileVersionInfo.GetVersionInfo(chromePath);
            return fileVersionInfo.FileVersion;
        }
        else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
        {
            try
            {
                using var process = Process.Start(
                    new ProcessStartInfo
                    {
                        FileName = "google-chrome",
                        ArgumentList = { "--product-version" },
                        UseShellExecute = false,
                        CreateNoWindow = true,
                        RedirectStandardOutput = true,
                        RedirectStandardError = true,
                    }
                );
                string output = await process.StandardOutput.ReadToEndAsync();
                string error = await process.StandardError.ReadToEndAsync();
                await process.WaitForExitAsync();
                process.Kill(true);

                if (!string.IsNullOrEmpty(error))
                {
                    throw new Exception(error);
                }

                return output;
            }
            catch (Exception ex)
            {
                throw new Exception("An error occurred trying to execute 'google-chrome --product-version'", ex);
            }
        }
        else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
        {
            try
            {
                using var process = Process.Start(
                    new ProcessStartInfo
                    {
                        FileName = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
                        ArgumentList = { "--version" },
                        UseShellExecute = false,
                        CreateNoWindow = true,
                        RedirectStandardOutput = true,
                        RedirectStandardError = true,
                    }
                );
                string output = await process.StandardOutput.ReadToEndAsync();
                string error = await process.StandardError.ReadToEndAsync();
                await process.WaitForExitAsync();
                process.Kill(true);

                if (!string.IsNullOrEmpty(error))
                {
                    throw new Exception(error);
                }

                output = output.Replace("Google Chrome ", "");
                return output;
            }
            catch (Exception ex)
            {
                throw new Exception($"An error occurred trying to execute '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome --version'", ex);
            }
        }
        else
        {
            throw new PlatformNotSupportedException("Your operating system is not supported.");
        }
    }
}

This code sample is for .NET (Core), you can find the .NET Framework version later on.

The ChromeDriverInstaller implementation provides two methods:

  • Install: This method installs the ChromeDriver at the specified path. If there's already a ChromeDriver at the existing path, it will be updated only if the version of the driver to be installed doesn't match the existing driver. The method takes two parameters:
    • chromeVersion: A string to specify which version of Chrome you want a ChromeDriver for. If null, GetChromeVersion is used to get the version of Chrome installed on your machine. Defaults to null.
    • forceDownload: Pass in true to force update the ChromeDriver even when the same version of the ChromeDriver is already at the expected location. Defaults to false.
  • GetChromeVersion: This method returns the version of Chrome installed on your machine. If Chrome is not installed, an exception will be thrown.

The Install method has some overloads to make the parameters optional. The ChromeDriverInstaller implementation supports Windows, Linux, and macOS. 
For this code to compile, you will need to install the "Microsoft.Win32.Registry" package. This package is only supported on Windows, but that's okay because the code from this package will only be executed on Windows.

To use the ChromeDriverInstaller, simply create a new instance and call the Install method. The ChromeDriver class will automatically look for the ChromeDriver binary in the executing assembly folder among other locations.
So after the ChromeDriver is installed into the executing assembly folder, you can instantiate your ChromeDriver without specifying the location of the ChromeDriver binary.

For example, here is a small console sample that will do the following:

  • Print the detected Chrome version
  • Install the ChromeDriver
  • Ask you for a URL to check
  • Print the title found when browsing the URL using ChromeDriver and Selenium
using OpenQA.Selenium.Chrome;
using System;
using System.Threading.Tasks;

namespace SeleniumConsole
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            Console.WriteLine("Installing ChromeDriver");

            var chromeDriverInstaller = new ChromeDriverInstaller();

            // not necessary, but added for logging purposes
            var chromeVersion = await chromeDriverInstaller.GetChromeVersion();
            Console.WriteLine($"Chrome version {chromeVersion} detected");

            await chromeDriverInstaller.Install(chromeVersion);
            Console.WriteLine("ChromeDriver installed");

            Console.WriteLine("Enter URL to visit:");
            var url = Console.ReadLine();
            if (string.IsNullOrEmpty(url))
            {
                Console.WriteLine("No URL entered");
                Console.WriteLine("Press any key to exit");
                Console.ReadKey();
                return;
            }

            var chromeOptions = new ChromeOptions();
            chromeOptions.AddArguments("headless");
            using (var chromeDriver = new ChromeDriver(chromeOptions))
            {
                chromeDriver.Navigate().GoToUrl(url);
                Console.WriteLine($"Page title: {chromeDriver.Title}");
            }
            Console.WriteLine("Press any key to exit");
            Console.ReadKey();
        }
    }
}

The output of this console application looks like this:

Installing ChromeDriver
Chrome version 88.0.4324.190 detected
ChromeDriver installed
Enter URL to visit:
https://swimburger.net
Starting ChromeDriver 88.0.4324.96 (68dba2d8a0b149a1d3afac56fa74648032bcf46b-refs/branch-heads/4324@{#1784}) on port 59180
Only local connections are allowed.
Please see https://chromedriver.chromium.org/security-considerations for suggestions on keeping ChromeDriver safe.
ChromeDriver was started successfully.

DevTools listening on ws://127.0.0.1:59183/devtools/browser/d1f2f48b-1dcb-4a74-8e18-57b088b87ae4
[0303/002653.418:INFO:CONSOLE(220)] "[PWA Builder] Service worker has been registered for scope: https://swimburger.net/", source: https://swimburger.net/ (220)
Page title: Swimburger - .NET, Web, Azure, Umbraco, and more
Press any key to exit

The second time you execute this console application, the ChromeDriver will have been download already and the download step will be skipped.
This console application is only one example of how you can use the ChromeDriverInstaller. You can also plug this into your test projects to ensure the latest matching ChromeDriver is always used before running your test suite.
This way you avoid the mismatch error mentioned at the beginning of this blog post. You can find the above sample code on GitHub.

Dissecting the code

Let's dissect the code starting off with the GetChromeVersion method. Depending on the Operating System (OS), the version of Chrome is determined differently. The OS is determined using RuntimeInformation.IsOSPlatform.

public async Task<string> GetChromeVersion()
{
    if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
    {
        string chromePath = (string)Registry.GetValue("HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\chrome.exe", null, null);
        if (chromePath == null)
        {
            throw new Exception("Google Chrome not found in registry");
        }

        var fileVersionInfo = FileVersionInfo.GetVersionInfo(chromePath);
        return fileVersionInfo.FileVersion;
    }

On Windows, the registry is queried to get the path to the Chrome installation. Using this path, you can get the FileVersionInfo which has the version of Chrome stored in the FileVersion property.
If Chrome is not installed, the registry query will return null and an exception will be thrown.

else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
    try
    {
        using var process = Process.Start(
            new ProcessStartInfo
            {
                FileName = "google-chrome",
                ArgumentList = { "--product-version" },
                UseShellExecute = false,
                CreateNoWindow = true,
                RedirectStandardOutput = true,
                RedirectStandardError = true,
            }
        );
        string output = await process.StandardOutput.ReadToEndAsync();
        string error = await process.StandardError.ReadToEndAsync();
        await process.WaitForExitAsync();
        process.Kill(true);

        if (!string.IsNullOrEmpty(error))
        {
            throw new Exception(error);
        }

        return output;
    }
    catch (Exception ex)
    {
        throw new Exception("An error occurred trying to execute 'google-chrome --product-version'", ex);
    }
}

On Linux, the google-chrome command is expected to be available globally, so you can execute it and pass in the --product-version argument to get the version of Chrome returned as output.
The Process APIs are used to execute this command. The version of Chrome will go to the StandardOutput. If there's anything in the StandardError, an exception is thrown.

else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
    try
    {
        using var process = Process.Start(
            new ProcessStartInfo
            {
                FileName = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
                ArgumentList = { "--version" },
                UseShellExecute = false,
                CreateNoWindow = true,
                RedirectStandardOutput = true,
                RedirectStandardError = true,
            }
        );
        string output = await process.StandardOutput.ReadToEndAsync();
        string error = await process.StandardError.ReadToEndAsync();
        await process.WaitForExitAsync();
        process.Kill(true);

        if (!string.IsNullOrEmpty(error))
        {
            throw new Exception(error);
        }

        output = output.Replace("Google Chrome ", "");
        return output;
    }
    catch (Exception ex)
    {
        throw new Exception($"An error occurred trying to execute '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome --version'", ex);
    }
}

On macOS, the application should be available at "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome". For some reason, the --product-version argument is not available on macOS, but you can use the --version argument instead.
The only difference between the two arguments is that the latter will prefix the version with "Google Chrome ".

The Process APIs are used to execute this command. The version of Chrome will go to the StandardOutput with the prefix which is removed from the string later on. If there's anything in the StandardError, an exception is thrown.

    else
    {
        throw new PlatformNotSupportedException("Your operating system is not supported.");
    }
}

Lastly, if the code isn't run on Windows, Linux, or macOS, an exception is thrown.

The GetChromeVersion method will be called from the Install method if no version of Chrome is passed in as a parameter.
Then the last part of the version number is stripped off.

public async Task Install(string chromeVersion, bool forceDownload)
{
    // Instructions from https://chromedriver.chromium.org/downloads/version-selection
    //   First, find out which version of Chrome you are using. Let's say you have Chrome 72.0.3626.81.
    if (chromeVersion == null)
    {
        chromeVersion = await GetChromeVersion();
    }

    //   Take the Chrome version number, remove the last part, 
    chromeVersion = chromeVersion.Substring(0, chromeVersion.LastIndexOf('.'));

The ChromeDriver version is requested using the HttpClient which is defined as a static field at the top of the class.
If the response status code is not 200, an exception is thrown. Otherwise, the response content is read and stored in the chromeDriverVersion variable.

//   and append the result to URL "https://chromedriver.storage.googleapis.com/LATEST_RELEASE_". 
//   For example, with Chrome version 72.0.3626.81, you'd get a URL "https://chromedriver.storage.googleapis.com/LATEST_RELEASE_72.0.3626".
var chromeDriverVersionResponse = await httpClient.GetAsync($"LATEST_RELEASE_{chromeVersion}");
if (!chromeDriverVersionResponse.IsSuccessStatusCode)
{
    if (chromeDriverVersionResponse.StatusCode == HttpStatusCode.NotFound)
    {
        throw new Exception($"ChromeDriver version not found for Chrome version {chromeVersion}");
    }
    else
    {
        throw new Exception($"ChromeDriver version request failed with status code: {chromeDriverVersionResponse.StatusCode}, reason phrase: {chromeDriverVersionResponse.ReasonPhrase}");
    }
}

var chromeDriverVersion = await chromeDriverVersionResponse.Content.ReadAsStringAsync();

Depending on which OS you're on, the correct zip file to download and the name of the file inside the zip will be initialized differently.

string zipName;
string driverName;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
    zipName = "chromedriver_win32.zip";
    driverName = "chromedriver.exe";
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
    zipName = "chromedriver_linux64.zip";
    driverName = "chromedriver";
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
    zipName = "chromedriver_mac64.zip";
    driverName = "chromedriver";
}
else
{
    throw new PlatformNotSupportedException("Your operating system is not supported.");
}

The Install method does not allow you to specify where to place the ChromeDriver binary. It assumes it should go into the folder where the executing assembly is stored.
You could add a targetPath parameter to allow you to specify where the ChromeDriver should be saved. But the current implementation will save the ChromeDriver binary into the folder of the executing assembly:

string targetPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
targetPath = Path.Combine(targetPath, driverName);

This section will check if there's an existing ChromeDriver at the target path. No ChromeDriver will be downloaded if the version to be downloaded matches the version of the ChromeDriver on disk. Instead, it will return to the caller.
If forceDownload is true, this optimization will be skipped entirely.

if (!forceDownload && File.Exists(targetPath))
{
    using var process = Process.Start(
        new ProcessStartInfo
        {
            FileName = targetPath,
            ArgumentList = { "--version" },
            UseShellExecute = false,
            CreateNoWindow = true,
            RedirectStandardOutput = true,
            RedirectStandardError = true,
        }
    );
    string existingChromeDriverVersion = await process.StandardOutput.ReadToEndAsync();
    string error = await process.StandardError.ReadToEndAsync();
    await process.WaitForExitAsync();
    process.Kill(true);

    // expected output is something like "ChromeDriver 88.0.4324.96 (68dba2d8a0b149a1d3afac56fa74648032bcf46b-refs/branch-heads/4324@{#1784})"
    // the following line will extract the version number and leave the rest
    existingChromeDriverVersion = existingChromeDriverVersion.Split(" ")[1];
    if (chromeDriverVersion == existingChromeDriverVersion)
    {
        return;
    }

    if (!string.IsNullOrEmpty(error))
    {
        throw new Exception($"Failed to execute {driverName} --version");
    }
}

The ChromeDriver ZIP file is requested using the HttpClient which is declared as a static field.
The response content is read as a stream and passed to the ZipArchive class. Using the zipArchive the binary inside the ZIP file is also read as a stream and copied to the FileStream used to write the binary to disk.

//   Use the URL created in the last step to retrieve a small file containing the version of ChromeDriver to use. For example, the above URL will get your a file containing "72.0.3626.69". (The actual number may change in the future, of course.)
//   Use the version number retrieved from the previous step to construct the URL to download ChromeDriver. With version 72.0.3626.69, the URL would be "https://chromedriver.storage.googleapis.com/index.html?path=72.0.3626.69/".
var driverZipResponse = await httpClient.GetAsync($"{chromeDriverVersion}/{zipName}");
if (!driverZipResponse.IsSuccessStatusCode)
{
    throw new Exception($"ChromeDriver download request failed with status code: {driverZipResponse.StatusCode}, reason phrase: {driverZipResponse.ReasonPhrase}");
}
        
// this reads the zipfile as a stream, opens the archive, 
// and extracts the chromedriver executable to the targetPath without saving any intermediate files to disk
using (var zipFileStream = await driverZipResponse.Content.ReadAsStreamAsync())
using (var zipArchive = new ZipArchive(zipFileStream, ZipArchiveMode.Read))
using (var chromeDriverWriter = new FileStream(targetPath, FileMode.Create))
{
    var entry = zipArchive.GetEntry(driverName);
    using Stream chromeDriverStream = entry.Open();
    await chromeDriverStream.CopyToAsync(chromeDriverWriter);
}

Lastly, only on Linux and macOS, you have to change the file permissions to allow the ChromeDriver to be executed.

    // on Linux/macOS, you need to add the executable permission (+x) to allow the execution of the chromedriver
    if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
    {
        using var process = Process.Start(
            new ProcessStartInfo
            {
                FileName = "chmod",
                ArgumentList = { "+x", targetPath },
                UseShellExecute = false,
                CreateNoWindow = true,
                RedirectStandardOutput = true,
                RedirectStandardError = true,
            }
        );
        string error = await process.StandardError.ReadToEndAsync();
        await process.WaitForExitAsync();
        process.Kill(true);

        if (!string.IsNullOrEmpty(error))
        {
            throw new Exception("Failed to make chromedriver executable");
        }
    }
}

Download the right ChromeDriver automatically using .NET Framework

The previous code samples work for .NET (Core) on Windows, macOS, and Linux. Unfortunately, .NET Framework does not have all the convenient .NET APIs and C# features used in the above sample. But if you still need to support it, here's the .NET Framework version:

using System;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Net;
using System.Net.Http;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.Win32;

namespace SeleniumConsoleFramework
{
    public class ChromeDriverInstaller
    {
        private static readonly HttpClient httpClient = new HttpClient
        {
            BaseAddress = new Uri("https://chromedriver.storage.googleapis.com/")
        };

        public Task Install() => Install(null, false);
        public Task Install(string chromeVersion) => Install(chromeVersion, false);
        public Task Install(bool forceDownload) => Install(null, forceDownload);

        public async Task Install(string chromeVersion, bool forceDownload)
        {
            // Instructions from https://chromedriver.chromium.org/downloads/version-selection
            //   First, find out which version of Chrome you are using. Let's say you have Chrome 72.0.3626.81.
            if (chromeVersion == null)
            {
                chromeVersion = GetChromeVersion();
            }

            //   Take the Chrome version number, remove the last part, 
            chromeVersion = chromeVersion.Substring(0, chromeVersion.LastIndexOf('.'));

            //   and append the result to URL "https://chromedriver.storage.googleapis.com/LATEST_RELEASE_". 
            //   For example, with Chrome version 72.0.3626.81, you'd get a URL "https://chromedriver.storage.googleapis.com/LATEST_RELEASE_72.0.3626".
            var chromeDriverVersionResponse = await httpClient.GetAsync($"LATEST_RELEASE_{chromeVersion}");
            if (!chromeDriverVersionResponse.IsSuccessStatusCode)
            {
                if (chromeDriverVersionResponse.StatusCode == HttpStatusCode.NotFound)
                {
                    throw new Exception($"ChromeDriver version not found for Chrome version {chromeVersion}");
                }
                else
                {
                    throw new Exception($"ChromeDriver version request failed with status code: {chromeDriverVersionResponse.StatusCode}, reason phrase: {chromeDriverVersionResponse.ReasonPhrase}");
                }
            }

            var chromeDriverVersion = await chromeDriverVersionResponse.Content.ReadAsStringAsync();

            string zipName = "chromedriver_win32.zip";
            string driverName = "chromedriver.exe";

            string targetPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
            targetPath = Path.Combine(targetPath, driverName);
            if (!forceDownload && File.Exists(targetPath))
            {
                using (var process = Process.Start(
                    new ProcessStartInfo
                    {
                        FileName = targetPath,
                        Arguments = "--version",
                        UseShellExecute = false,
                        CreateNoWindow = true,
                        RedirectStandardOutput = true,
                        RedirectStandardError = true,
                    }
                ))
                {
                    string existingChromeDriverVersion = await process.StandardOutput.ReadToEndAsync();
                    string error = await process.StandardError.ReadToEndAsync();
                    process.WaitForExit();
                    process.Kill();

                    // expected output is something like "ChromeDriver 88.0.4324.96 (68dba2d8a0b149a1d3afac56fa74648032bcf46b-refs/branch-heads/4324@{#1784})"
                    // the following line will extract the version number and leave the rest
                    existingChromeDriverVersion = existingChromeDriverVersion.Split(' ')[1];
                    if (chromeDriverVersion == existingChromeDriverVersion)
                    {
                        return;
                    }

                    if (!string.IsNullOrEmpty(error))
                    {
                        throw new Exception($"Failed to execute {driverName} --version");
                    }
                }
            }

            //   Use the URL created in the last step to retrieve a small file containing the version of ChromeDriver to use. For example, the above URL will get your a file containing "72.0.3626.69". (The actual number may change in the future, of course.)
            //   Use the version number retrieved from the previous step to construct the URL to download ChromeDriver. With version 72.0.3626.69, the URL would be "https://chromedriver.storage.googleapis.com/index.html?path=72.0.3626.69/".
            var driverZipResponse = await httpClient.GetAsync($"{chromeDriverVersion}/{zipName}");
            if (!driverZipResponse.IsSuccessStatusCode)
            {
                throw new Exception($"ChromeDriver download request failed with status code: {driverZipResponse.StatusCode}, reason phrase: {driverZipResponse.ReasonPhrase}");
            }

            // this reads the zipfile as a stream, opens the archive, 
            // and extracts the chromedriver executable to the targetPath without saving any intermediate files to disk
            using (var zipFileStream = await driverZipResponse.Content.ReadAsStreamAsync())
            using (var zipArchive = new ZipArchive(zipFileStream, ZipArchiveMode.Read))
            using (var chromeDriverWriter = new FileStream(targetPath, FileMode.Create))
            {
                var entry = zipArchive.GetEntry(driverName);
                using (Stream chromeDriverStream = entry.Open())
                {
                    await chromeDriverStream.CopyToAsync(chromeDriverWriter);
                }
            }
        }

        public string GetChromeVersion()
        {
            string chromePath = (string)Registry.GetValue("HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\chrome.exe", null, null);
            if (chromePath == null)
            {
                throw new Exception("Google Chrome not found in registry");
            }

            var fileVersionInfo = FileVersionInfo.GetVersionInfo(chromePath);
            return fileVersionInfo.FileVersion;
        }
    }
}

The code is a lot smaller because you don't have to consider any operating system but Windows.

Using the .NET Framework version using a console app would look like this:

using System;
using OpenQA.Selenium.Chrome;

namespace SeleniumConsoleFramework
{
    public class Program
    {
        public static void Main(string[] args)
        {
            Console.WriteLine("Installing ChromeDriver");

            var chromeDriverInstaller = new ChromeDriverInstaller();

            // not necessary, but added for logging purposes
            var chromeVersion = chromeDriverInstaller.GetChromeVersion();
            Console.WriteLine($"Chrome version {chromeVersion} detected");

            chromeDriverInstaller.Install(chromeVersion);
            Console.WriteLine("ChromeDriver installed");

            Console.WriteLine("Enter URL to visit:");
            var url = Console.ReadLine();
            if (string.IsNullOrEmpty(url))
            {
                Console.WriteLine("No URL entered");
                Console.WriteLine("Press any key to exit");
                Console.ReadKey();
                return;
            }

            var chromeOptions = new ChromeOptions();
            chromeOptions.AddArguments("headless");
            using (var chromeDriver = new ChromeDriver(chromeOptions))
            {
                chromeDriver.Navigate().GoToUrl(url);
                Console.WriteLine($"Page title: {chromeDriver.Title}");
            }
            Console.WriteLine("Press any key to exit");
            Console.ReadKey();
        }
    }
}

Summary

Google Chrome updates by itself all the time, but the ChromeDriver to run selenium tests does not update automatically alongside it. This leads to incompatibility errors over time.
You can fix this by updating the ChromeDriver manually which is very cumbersome when you have to do this over and over again. Especially when you use a NuGet package or check-in the ChromeDriver file into source-code, keeping your ChromeDriver in sync with the version of Chrome is painful. You can avoid this by automatically downloading the right ChromeDriver at runtime before running your selenium tests. Using the ChromeDriverInstaller class, you can detect the version of Chrome on your machine and automatically download the matching ChromeDriver.

You can find both the .NET (Core) and .NET Framework samples on this GitHub repository.

If you prefer using PowerShell, check out "Download the right ChromeDriver version & keep it up to date on Windows/Linux/macOS using PowerShell".

Related Posts

Related Posts