diff --git a/dotnet/src/webdriver/DevTools/DevToolsCommandSettings.cs b/dotnet/src/webdriver/DevTools/DevToolsCommandSettings.cs
new file mode 100644
index 0000000000000..9cf74e5bfc432
--- /dev/null
+++ b/dotnet/src/webdriver/DevTools/DevToolsCommandSettings.cs
@@ -0,0 +1,38 @@
+//
+// Licensed to the Software Freedom Conservancy (SFC) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The SFC licenses this file
+// to you under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using Newtonsoft.Json.Linq;
+
+namespace OpenQA.Selenium.DevTools
+{
+ public class DevToolsCommandSettings
+ {
+ public DevToolsCommandSettings(string commandName)
+ {
+ if (string.IsNullOrWhiteSpace(commandName))
+ {
+ throw new ArgumentNullException(nameof(commandName));
+ }
+
+ CommandName = commandName;
+ }
+ public string SessionId { get; set; }
+ public string CommandName { get; set; }
+ public JToken CommandParameters { get; set; }
+ }
+}
diff --git a/dotnet/src/webdriver/DevTools/DevToolsSession.cs b/dotnet/src/webdriver/DevTools/DevToolsSession.cs
index 311f4d74dbd4f..664eae19263b3 100644
--- a/dotnet/src/webdriver/DevTools/DevToolsSession.cs
+++ b/dotnet/src/webdriver/DevTools/DevToolsSession.cs
@@ -316,6 +316,90 @@ public T GetVersionSpecificDomains() where T : DevToolsSessionDomains
return null;
}
+ ///
+ /// Send a collection of and wait on all of their results.
+ ///
+ ///
+ ///
+ ///
+ ///
+ /// A list of command response object implementeing the interface.
+ public async Task> SendCommands(List commands, CancellationToken cancellationToken = default(CancellationToken), int? millisecondsTimeout = null, bool throwExceptionIfResponseNotReceived = true)
+ {
+ if (millisecondsTimeout.HasValue == false)
+ {
+ millisecondsTimeout = Convert.ToInt32(CommandTimeout.TotalMilliseconds);
+ }
+
+ if (this.attachedTargetId == null)
+ {
+ LogTrace("Session not currently attached to a target; reattaching");
+ await this.InitializeSession();
+ }
+
+ var messages = new List();
+ foreach (var item in commands)
+ {
+ messages.Add(new DevToolsCommandData(Interlocked.Increment(ref this.currentCommandId), item.SessionId, item.CommandName, item.CommandParameters));
+ }
+
+ if (this.connection != null && this.connection.IsActive)
+ {
+ foreach (var message in messages)
+ {
+ var contents = JsonConvert.SerializeObject(message);
+
+ this.pendingCommands.TryAdd(message.CommandId, message);
+ await this.connection.SendData(contents).ConfigureAwait(false);
+ }
+
+ WaitHandle.WaitAll(messages.Select(x => x.SyncEvent.WaitHandle).ToArray(), millisecondsTimeout.Value);
+
+ var noResponsesReceived = messages.Where(x => !x.SyncEvent.IsSet);
+ if (noResponsesReceived.Any() && throwExceptionIfResponseNotReceived)
+ {
+ throw new InvalidOperationException($"A command response was not received: {string.Join(", ", noResponsesReceived.Select(x => x.CommandName))}");
+ }
+
+ foreach (var message in messages)
+ {
+ DevToolsCommandData modified;
+ if (this.pendingCommands.TryRemove(message.CommandId, out modified))
+ {
+ if (modified.IsError)
+ {
+ var errorMessage = modified.Result.Value("message");
+ var errorData = modified.Result.Value("data");
+
+ var exceptionMessage = $"{message.CommandName}: {errorMessage}";
+ if (!string.IsNullOrWhiteSpace(errorData))
+ {
+ exceptionMessage = $"{exceptionMessage} - {errorData}";
+ }
+
+ LogTrace("Received Error Response {0}: {1} {2}", modified.CommandId, message, errorData);
+ throw new CommandResponseException(exceptionMessage)
+ {
+ Code = modified.Result.Value("code")
+ };
+ }
+ }
+ }
+
+ return messages.Select(x => new DevToolsCommandResponse
+ {
+ Result = x.Result,
+ SessionId = x.SessionId
+ }).ToList();
+ }
+ else
+ {
+ LogTrace("WebSocket is not connected; not sending {0}", string.Join(", ", commands.Select(itm => itm.CommandName)));
+ }
+
+ return null;
+ }
+
///
/// Releases all resources associated with this .
///
diff --git a/dotnet/src/webdriver/DevTools/IDevToolsSession.cs b/dotnet/src/webdriver/DevTools/IDevToolsSession.cs
index b9d1d06191eb4..dce714ed02ff8 100644
--- a/dotnet/src/webdriver/DevTools/IDevToolsSession.cs
+++ b/dotnet/src/webdriver/DevTools/IDevToolsSession.cs
@@ -85,5 +85,15 @@ Task SendCommand(TCommand command,
/// to throw an exception if a response is not received; otherwise, .
/// The command response object implementing the interface.
Task SendCommand(string commandName, JToken @params, CancellationToken cancellationToken, int? millisecondsTimeout, bool throwExceptionIfResponseNotReceived);
+
+ ///
+ /// Returns a collection of based on a collection of commands.
+ ///
+ ///
+ ///
+ ///
+ ///
+ /// A list of command response object implementeing the interface.
+ Task> SendCommands(List commands, CancellationToken cancellationToken = default(CancellationToken), int? millisecondsTimeout = null, bool throwExceptionIfResponseNotReceived = true);
}
}