diff --git a/dotnet/src/webdriver/Command.cs b/dotnet/src/webdriver/Command.cs index b7acb42ef2dbf..28034ce57dfa4 100644 --- a/dotnet/src/webdriver/Command.cs +++ b/dotnet/src/webdriver/Command.cs @@ -20,6 +20,7 @@ using OpenQA.Selenium.Internal; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; @@ -31,15 +32,29 @@ namespace OpenQA.Selenium; /// </summary> public class Command { - private readonly static JsonSerializerOptions s_jsonSerializerOptions = new() + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = $"All trimming-unsafe access points to {nameof(s_jsonSerializerOptions)} are annotated as such")] + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = $"All AOT-unsafe access points to {nameof(s_jsonSerializerOptions)} are annotated as such")] + private static class JsonOptionsHolder { - TypeInfoResolverChain = + public readonly static JsonSerializerOptions s_jsonSerializerOptions = new() { - CommandJsonSerializerContext.Default, - new DefaultJsonTypeInfoResolver() - }, - Converters = { new ResponseValueJsonConverter() } - }; + TypeInfoResolver = GetTypeInfoResolver(), + Converters = { new ResponseValueJsonConverter() } + }; + + private static IJsonTypeInfoResolver GetTypeInfoResolver() + { +#if NET8_0_OR_GREATER + if (!System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeSupported) + { + return CommandJsonSerializerContext.Default; + } +#endif + return JsonTypeInfoResolver.Combine(CommandJsonSerializerContext.Default, new DefaultJsonTypeInfoResolver()); + } + } + + private readonly Dictionary<string, object?> _parameters; /// <summary> /// Initializes a new instance of the <see cref="Command"/> class using a command name and a JSON-encoded string for the parameters. @@ -47,8 +62,10 @@ public class Command /// <param name="name">Name of the command</param> /// <param name="jsonParameters">Parameters for the command as a JSON-encoded string.</param> public Command(string name, string jsonParameters) - : this(null, name, ConvertParametersFromJson(jsonParameters)) { + this.SessionId = null; + this._parameters = ConvertParametersFromJson(jsonParameters) ?? new Dictionary<string, object?>(); + this.Name = name ?? throw new ArgumentNullException(nameof(name)); } /// <summary> @@ -61,7 +78,7 @@ public Command(string name, string jsonParameters) public Command(SessionId? sessionId, string name, Dictionary<string, object?>? parameters) { this.SessionId = sessionId; - this.Parameters = parameters ?? new Dictionary<string, object?>(); + this._parameters = parameters ?? new Dictionary<string, object?>(); this.Name = name ?? throw new ArgumentNullException(nameof(name)); } @@ -81,18 +98,32 @@ public Command(SessionId? sessionId, string name, Dictionary<string, object?>? p /// Gets the parameters of the command /// </summary> [JsonPropertyName("parameters")] - public Dictionary<string, object?> Parameters { get; } + public Dictionary<string, object?> Parameters + { + [RequiresUnreferencedCode("Adding untyped parameter values for JSON serialization has best-effort AOT support. Ensure only Selenium types and well-known .NET types are added.")] + [RequiresDynamicCode("Adding untyped parameter values for JSON serialization has best-effort AOT support. Ensure only Selenium types and well-known .NET types are added.")] + get => _parameters; + } /// <summary> /// Gets the parameters of the command as a JSON-encoded string. /// </summary> public string ParametersAsJsonString { + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = $"All trimming-unsafe access points to {nameof(_parameters)} are annotated as such")] + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = $"All AOT-unsafe access points to {nameof(_parameters)} are annotated as such")] get { - if (this.Parameters != null && this.Parameters.Count > 0) + if (HasParameters()) { - return JsonSerializer.Serialize(this.Parameters, s_jsonSerializerOptions); + try + { + return JsonSerializer.Serialize(this._parameters, JsonOptionsHolder.s_jsonSerializerOptions); + } + catch (NotSupportedException ex) + { + throw new WebDriverException("Attempted to serialize an unsupported type. Ensure you are using Selenium types, or well-known .NET types such as Dictionary<string, object> and object[]", ex); + } } else { @@ -101,6 +132,25 @@ public string ParametersAsJsonString } } + internal bool HasParameters() + { + return this._parameters != null && this._parameters.Count > 0; + } + + internal bool TryGetValueAndRemoveIfNotNull(string key, [NotNullWhen(true)] out object? value) + { + if (this._parameters.TryGetValue(key, out value)) + { + if (value is not null) + { + this._parameters.Remove(key); + return true; + } + } + + return false; + } + /// <summary> /// Returns a string of the Command object /// </summary> @@ -168,5 +218,6 @@ public override string ToString() [JsonSerializable(typeof(Dictionary<string, short>))] [JsonSerializable(typeof(Dictionary<string, ushort>))] [JsonSerializable(typeof(Dictionary<string, string>))] +[JsonSerializable(typeof(object[]))] [JsonSourceGenerationOptions(Converters = [typeof(ResponseValueJsonConverter)])] internal partial class CommandJsonSerializerContext : JsonSerializerContext; diff --git a/dotnet/src/webdriver/HttpCommandInfo.cs b/dotnet/src/webdriver/HttpCommandInfo.cs index 38e7b382d697f..60d227250aec2 100644 --- a/dotnet/src/webdriver/HttpCommandInfo.cs +++ b/dotnet/src/webdriver/HttpCommandInfo.cs @@ -119,17 +119,13 @@ private static string GetCommandPropertyValue(string propertyName, Command comma propertyValue = commandToExecute.SessionId.ToString(); } } - else if (commandToExecute.Parameters != null && commandToExecute.Parameters.Count > 0) + else if (commandToExecute.HasParameters()) { // Extract the URL parameter, and remove it from the parameters dictionary // so it doesn't get transmitted as a JSON parameter. - if (commandToExecute.Parameters.TryGetValue(propertyName, out var propertyValueObject)) + if (commandToExecute.TryGetValueAndRemoveIfNotNull(propertyName, out var propertyValueObject)) { - if (propertyValueObject != null) - { - propertyValue = propertyValueObject.ToString()!; - commandToExecute.Parameters.Remove(propertyName); - } + propertyValue = propertyValueObject.ToString()!; } } diff --git a/dotnet/src/webdriver/WebDriver.cs b/dotnet/src/webdriver/WebDriver.cs index 77f82461053d7..b88403643d2d0 100644 --- a/dotnet/src/webdriver/WebDriver.cs +++ b/dotnet/src/webdriver/WebDriver.cs @@ -40,7 +40,7 @@ public class WebDriver : IWebDriver, ISearchContext, IJavaScriptExecutor, IFinds /// </summary> protected static readonly TimeSpan DefaultCommandTimeout = TimeSpan.FromSeconds(60); private IFileDetector fileDetector = new DefaultFileDetector(); - private NetworkManager network; + private NetworkManager? network; private WebElementFactory elementFactory; private readonly List<string> registeredCommands = new List<string>();