From 898d65dd1c706b428bd540c5b0abf93e926e4952 Mon Sep 17 00:00:00 2001 From: Nikolay Borisenko <22616990+nvborisenko@users.noreply.github.com> Date: Fri, 20 Feb 2026 18:01:13 +0300 Subject: [PATCH 1/6] [dotnet] Any webdriver can be disposed asynchronously --- dotnet/src/support/BUILD.bazel | 5 ++ .../support/Events/EventFiringWebDriver.cs | 27 +++++++ dotnet/src/webdriver/IWebDriver.cs | 2 +- dotnet/src/webdriver/WebDriver.cs | 72 +++++++++++++++---- dotnet/test/common/StubDriver.cs | 10 +++ 5 files changed, 102 insertions(+), 14 deletions(-) diff --git a/dotnet/src/support/BUILD.bazel b/dotnet/src/support/BUILD.bazel index b4251681c8452..19d8a5d896384 100644 --- a/dotnet/src/support/BUILD.bazel +++ b/dotnet/src/support/BUILD.bazel @@ -3,6 +3,7 @@ load( "csharp_library", "generated_assembly_info", "nuget_pack", + "nuget_package", ) load( "//dotnet:selenium-dotnet-version.bzl", @@ -44,6 +45,8 @@ csharp_library( ], deps = [ "//dotnet/src/webdriver:webdriver-netstandard2.0", + nuget_package("Microsoft.Bcl.AsyncInterfaces"), + nuget_package("System.Threading.Tasks.Extensions"), ], ) @@ -84,6 +87,8 @@ csharp_library( ], deps = [ "//dotnet/src/webdriver:webdriver-netstandard2.0-strongnamed", + nuget_package("Microsoft.Bcl.AsyncInterfaces"), + nuget_package("System.Threading.Tasks.Extensions"), ], ) diff --git a/dotnet/src/support/Events/EventFiringWebDriver.cs b/dotnet/src/support/Events/EventFiringWebDriver.cs index 70b69589f7500..ee27380eec7e4 100644 --- a/dotnet/src/support/Events/EventFiringWebDriver.cs +++ b/dotnet/src/support/Events/EventFiringWebDriver.cs @@ -405,6 +405,17 @@ public void Dispose() GC.SuppressFinalize(this); } + /// + /// Asynchronously disposes this instance. + /// + /// A task representing the asynchronous dispose operation. + public async ValueTask DisposeAsync() + { + await this.DisposeAsyncCore().ConfigureAwait(false); + this.Dispose(false); + GC.SuppressFinalize(this); + } + /// /// Executes JavaScript in the context of the currently selected frame or window. /// @@ -593,6 +604,22 @@ protected virtual void Dispose(bool disposing) } } + /// + /// Asynchronously performs the core dispose logic. + /// + /// A task representing the asynchronous dispose operation. + protected virtual async ValueTask DisposeAsyncCore() + { + if (this.WrappedDriver is IAsyncDisposable asyncDisposable) + { + await asyncDisposable.DisposeAsync().ConfigureAwait(false); + } + else + { + this.WrappedDriver.Dispose(); + } + } + /// /// Raises the event. /// diff --git a/dotnet/src/webdriver/IWebDriver.cs b/dotnet/src/webdriver/IWebDriver.cs index 7a92363acbaa3..9177ae81098a1 100644 --- a/dotnet/src/webdriver/IWebDriver.cs +++ b/dotnet/src/webdriver/IWebDriver.cs @@ -43,7 +43,7 @@ namespace OpenQA.Selenium; /// more fully featured browser when there is a requirement for one. /// /// -public interface IWebDriver : ISearchContext, IDisposable +public interface IWebDriver : ISearchContext, IDisposable, IAsyncDisposable { /// /// Gets or sets the URL the browser is currently displaying. diff --git a/dotnet/src/webdriver/WebDriver.cs b/dotnet/src/webdriver/WebDriver.cs index 78c76f08ebadb..0df8343313859 100644 --- a/dotnet/src/webdriver/WebDriver.cs +++ b/dotnet/src/webdriver/WebDriver.cs @@ -223,6 +223,17 @@ public void Dispose() GC.SuppressFinalize(this); } + /// + /// Asynchronously disposes the WebDriver Instance + /// + /// A task representing the asynchronous dispose operation. + public async ValueTask DisposeAsync() + { + await this.DisposeAsyncCore().ConfigureAwait(false); + this.Dispose(false); + GC.SuppressFinalize(this); + } + /// /// Executes JavaScript "asynchronously" in the context of the currently selected frame or window, /// executing the callback function specified as the last argument in the list of arguments. @@ -672,25 +683,60 @@ protected bool RegisterInternalDriverCommand(string commandName, [NotNullWhen(tr /// if its in the process of disposing protected virtual void Dispose(bool disposing) { - try + if (disposing) { if (this.SessionId is not null) { - this.Execute(DriverCommand.Quit, null); + try + { + + this.Execute(DriverCommand.Quit, null); + + } + catch (NotImplementedException) + { + } + catch (InvalidOperationException) + { + } + catch (WebDriverException) + { + } + finally + { + this.SessionId = null!; + } } + + this.CommandExecutor.Dispose(); } - catch (NotImplementedException) - { - } - catch (InvalidOperationException) - { - } - catch (WebDriverException) - { - } - finally + } + + /// + /// Asynchronously performs the core dispose logic. + /// + /// A task representing the asynchronous dispose operation. + protected virtual async ValueTask DisposeAsyncCore() + { + if (this.SessionId is not null) { - this.SessionId = null!; + try + { + await this.ExecuteAsync(DriverCommand.Quit, null).ConfigureAwait(false); + } + catch (NotImplementedException) + { + } + catch (InvalidOperationException) + { + } + catch (WebDriverException) + { + } + finally + { + this.SessionId = null!; + } } this.CommandExecutor.Dispose(); diff --git a/dotnet/test/common/StubDriver.cs b/dotnet/test/common/StubDriver.cs index 9f14206c40196..64d6cfa821fa4 100644 --- a/dotnet/test/common/StubDriver.cs +++ b/dotnet/test/common/StubDriver.cs @@ -19,6 +19,7 @@ using System; using System.Collections.ObjectModel; +using System.Threading.Tasks; namespace OpenQA.Selenium; @@ -101,4 +102,13 @@ public void Dispose() } #endregion + + #region IAsyncDisposable Members + + public ValueTask DisposeAsync() + { + throw new NotImplementedException(); + } + + #endregion } From 1e2dd9c68db7100c4f2b77a819c34b2f1129ced5 Mon Sep 17 00:00:00 2001 From: Nikolay Borisenko <22616990+nvborisenko@users.noreply.github.com> Date: Thu, 26 Feb 2026 19:13:50 +0300 Subject: [PATCH 2/6] Dispose EventFiringWebDriver simpler --- .../support/Events/EventFiringWebDriver.cs | 34 ++----------------- 1 file changed, 2 insertions(+), 32 deletions(-) diff --git a/dotnet/src/support/Events/EventFiringWebDriver.cs b/dotnet/src/support/Events/EventFiringWebDriver.cs index ee27380eec7e4..f189539cd8869 100644 --- a/dotnet/src/support/Events/EventFiringWebDriver.cs +++ b/dotnet/src/support/Events/EventFiringWebDriver.cs @@ -401,7 +401,7 @@ public ReadOnlyCollection FindElements(By by) /// public void Dispose() { - this.Dispose(true); + WrappedDriver.Dispose(); GC.SuppressFinalize(this); } @@ -411,8 +411,7 @@ public void Dispose() /// A task representing the asynchronous dispose operation. public async ValueTask DisposeAsync() { - await this.DisposeAsyncCore().ConfigureAwait(false); - this.Dispose(false); + await WrappedDriver.DisposeAsync().ConfigureAwait(false); GC.SuppressFinalize(this); } @@ -591,35 +590,6 @@ public Screenshot GetScreenshot() return screenshotDriver.GetScreenshot(); } - /// - /// Frees all managed and, optionally, unmanaged resources used by this instance. - /// - /// to dispose of only managed resources; - /// to dispose of managed and unmanaged resources. - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - this.WrappedDriver.Dispose(); - } - } - - /// - /// Asynchronously performs the core dispose logic. - /// - /// A task representing the asynchronous dispose operation. - protected virtual async ValueTask DisposeAsyncCore() - { - if (this.WrappedDriver is IAsyncDisposable asyncDisposable) - { - await asyncDisposable.DisposeAsync().ConfigureAwait(false); - } - else - { - this.WrappedDriver.Dispose(); - } - } - /// /// Raises the event. /// From 44e4832cd1dbc3b535b3a96ff2e5c775d61d90c6 Mon Sep 17 00:00:00 2001 From: Nikolay Borisenko <22616990+nvborisenko@users.noreply.github.com> Date: Thu, 26 Feb 2026 19:57:34 +0300 Subject: [PATCH 3/6] Fix Chromium disposal pattern --- .../src/webdriver/Chromium/ChromiumDriver.cs | 19 +++++++++++++++++-- dotnet/src/webdriver/Firefox/FirefoxDriver.cs | 11 ----------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/dotnet/src/webdriver/Chromium/ChromiumDriver.cs b/dotnet/src/webdriver/Chromium/ChromiumDriver.cs index 8857f5229b254..71f864f771513 100644 --- a/dotnet/src/webdriver/Chromium/ChromiumDriver.cs +++ b/dotnet/src/webdriver/Chromium/ChromiumDriver.cs @@ -480,9 +480,9 @@ public void StopCasting(string deviceName) } /// - /// Stops the driver from running + /// Disposes of the resources used by the instance, including any active DevTools session. /// - /// if its in the process of disposing + /// Indicates whether the method is being called from a Dispose method (true) or from a finalizer (false). protected override void Dispose(bool disposing) { if (disposing) @@ -497,6 +497,21 @@ protected override void Dispose(bool disposing) base.Dispose(disposing); } + /// + /// Asynchronously disposes of the resources used by the instance, including any active DevTools session. + /// + /// A task representing the asynchronous dispose operation. + protected override async ValueTask DisposeAsyncCore() + { + if (this.devToolsSession != null) + { + this.devToolsSession.Dispose(); + this.devToolsSession = null; + } + + await base.DisposeAsyncCore().ConfigureAwait(false); + } + private static ICapabilities ConvertOptionsToCapabilities(ChromiumOptions options) { if (options == null) diff --git a/dotnet/src/webdriver/Firefox/FirefoxDriver.cs b/dotnet/src/webdriver/Firefox/FirefoxDriver.cs index 0573d2ed368e7..fd4d58c049eb5 100644 --- a/dotnet/src/webdriver/Firefox/FirefoxDriver.cs +++ b/dotnet/src/webdriver/Firefox/FirefoxDriver.cs @@ -414,17 +414,6 @@ protected virtual void PrepareEnvironment() // Does nothing, but provides a hook for subclasses to do "stuff" } - /// - /// Disposes of the FirefoxDriver and frees all resources. - /// - /// A value indicating whether the user initiated the - /// disposal of the object. Pass if the user is actively - /// disposing the object; otherwise . - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - } - private static ICapabilities ConvertOptionsToCapabilities(FirefoxOptions options) { if (options == null) From 7fe86385d5f51170690d7d044af2fc756ed0b411 Mon Sep 17 00:00:00 2001 From: Nikolay Borisenko <22616990+nvborisenko@users.noreply.github.com> Date: Thu, 26 Feb 2026 20:12:09 +0300 Subject: [PATCH 4/6] Fix build error --- dotnet/test/support/Selenium.WebDriver.Support.Tests.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/dotnet/test/support/Selenium.WebDriver.Support.Tests.csproj b/dotnet/test/support/Selenium.WebDriver.Support.Tests.csproj index be6aaef57a009..f5f0ddde1f474 100644 --- a/dotnet/test/support/Selenium.WebDriver.Support.Tests.csproj +++ b/dotnet/test/support/Selenium.WebDriver.Support.Tests.csproj @@ -6,6 +6,7 @@ + From 96275810b3823fcf2b196ff8fca55807978fcbc5 Mon Sep 17 00:00:00 2001 From: Nikolay Borisenko <22616990+nvborisenko@users.noreply.github.com> Date: Thu, 26 Feb 2026 20:49:55 +0300 Subject: [PATCH 5/6] Implement good disposal pattern for EventFiringDriver --- .../support/Events/EventFiringWebDriver.cs | 26 +++++++++++++++++-- dotnet/test/support/Events/BUILD.bazel | 1 + .../Selenium.WebDriver.Support.Tests.csproj | 1 - 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/dotnet/src/support/Events/EventFiringWebDriver.cs b/dotnet/src/support/Events/EventFiringWebDriver.cs index f189539cd8869..9933064e8871f 100644 --- a/dotnet/src/support/Events/EventFiringWebDriver.cs +++ b/dotnet/src/support/Events/EventFiringWebDriver.cs @@ -401,7 +401,7 @@ public ReadOnlyCollection FindElements(By by) /// public void Dispose() { - WrappedDriver.Dispose(); + this.Dispose(true); GC.SuppressFinalize(this); } @@ -411,10 +411,32 @@ public void Dispose() /// A task representing the asynchronous dispose operation. public async ValueTask DisposeAsync() { - await WrappedDriver.DisposeAsync().ConfigureAwait(false); + await this.DisposeAsyncCore().ConfigureAwait(false); + this.Dispose(false); GC.SuppressFinalize(this); } + /// + /// Stops the client from running. + /// + /// If , managed resources are disposed. + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + WrappedDriver.Dispose(); + } + } + + /// + /// Asynchronously performs the core dispose logic. + /// + /// A task representing the asynchronous dispose operation. + protected virtual async ValueTask DisposeAsyncCore() + { + await WrappedDriver.DisposeAsync().ConfigureAwait(false); + } + /// /// Executes JavaScript in the context of the currently selected frame or window. /// diff --git a/dotnet/test/support/Events/BUILD.bazel b/dotnet/test/support/Events/BUILD.bazel index 060534a69a7bc..593314776f691 100644 --- a/dotnet/test/support/Events/BUILD.bazel +++ b/dotnet/test/support/Events/BUILD.bazel @@ -13,6 +13,7 @@ dotnet_nunit_test_suite( "//dotnet/src/support", "//dotnet/src/webdriver:webdriver-net8.0", "//dotnet/test/common:fixtures", + nuget_package("Microsoft.Bcl.AsyncInterfaces"), nuget_package("NUnit"), nuget_package("Moq"), ], diff --git a/dotnet/test/support/Selenium.WebDriver.Support.Tests.csproj b/dotnet/test/support/Selenium.WebDriver.Support.Tests.csproj index f5f0ddde1f474..036e08d4bfd77 100644 --- a/dotnet/test/support/Selenium.WebDriver.Support.Tests.csproj +++ b/dotnet/test/support/Selenium.WebDriver.Support.Tests.csproj @@ -14,7 +14,6 @@ - From b174cb3e8b2dd223918acca49a0a73a991bd364b Mon Sep 17 00:00:00 2001 From: Nikolay Borisenko <22616990+nvborisenko@users.noreply.github.com> Date: Thu, 26 Feb 2026 21:04:36 +0300 Subject: [PATCH 6/6] Force good using in nuget README --- dotnet/src/webdriver/assets/nuget/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/webdriver/assets/nuget/README.md b/dotnet/src/webdriver/assets/nuget/README.md index e780243d3445b..e05cb38ab350b 100644 --- a/dotnet/src/webdriver/assets/nuget/README.md +++ b/dotnet/src/webdriver/assets/nuget/README.md @@ -6,7 +6,7 @@ Selenium is a set of different software tools each with a different approach to using OpenQA.Selenium.Chrome; using OpenQA.Selenium; -using var driver = new ChromeDriver(); +await using var driver = new ChromeDriver(); driver.Url = "https://www.google.com"; driver.FindElement(By.Name("q")).SendKeys("webdriver" + Keys.Return);