This is an automated email from the ASF dual-hosted git repository.

Cole-Greer pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git


The following commit(s) were added to refs/heads/master by this push:
     new 4d5d571b7b Standardized `gremlin-dotnet` connection options (#3468)
4d5d571b7b is described below

commit 4d5d571b7b8f8b64a448148acdac56d5508f5062
Author: Guian Gumpac <[email protected]>
AuthorDate: Mon Jun 29 11:34:03 2026 -0700

    Standardized `gremlin-dotnet` connection options (#3468)
    
    Implements the .NET portion of the TinkerPop 4.x GLV connection-options 
standardization. Renames several `ConnectionSettings` properties and the `Auth` 
factory methods to canonical names (old names kept as `[Obsolete]` deprecated 
aliases), aligns defaults, adds new options, and fixes two latent bugs. .NET 
driver changes only; the other GLVs follow in separate PRs.
    
    **Proposal:** 
https://lists.apache.org/thread/yqtr2wnb1kq2pqqq4002cz511q5o0bkg
    
    ## Renames (deprecated aliases retained)
    
    | Old                       | New              | Default |
    | ------------------------- | ---------------- | ------- |
    | `MaxConnectionsPerServer` | `MaxConnections` | 128     |
    | `IdleConnectionTimeout`   | `IdleTimeout`    | 180s    |
    | `ConnectionTimeout`       | `ConnectTimeout` | 5s      |
    | `KeepAliveInterval`       | `KeepAliveTime`  | 30s     |
    | `EnableCompression`       | `Compression`    | -       |
    | `Auth.BasicAuth`          | `Auth.Basic`     | -       |
    | `Auth.SigV4Auth`          | `Auth.Sigv4`     | -       |
    
    The old property names and auth methods are retained as `[Obsolete]` 
deprecated aliases (marked "As of release 4.0.0, ...") that delegate to the 
canonical members, so existing code keeps compiling.
    
    ## Behavior changes (breaking)
    * **`ConnectTimeout`** default lowered 15s → 5s.
    * **`KeepAliveTime`** is now wired to a real TCP keep-alive socket option 
(via `SocketsHttpHandler.ConnectCallback`) rather than the inert HTTP/2 ping 
timeout, which had no effect on HTTP/1.1. It enables `SO_KEEPALIVE` and sets 
the per-socket idle time on Windows, Linux, and macOS; on other platforms 
keep-alive stays enabled at the OS default idle time.
    * **`Compression`** is now a `{None, Deflate}` enum defaulting to `Deflate` 
(compression on by default); the driver sends `Accept-Encoding: deflate` by 
default. Set `Compression.None` to disable. The deprecated `EnableCompression` 
`bool` is preserved only as a compatibility shim mapping `true`/`false` to 
`Compression.Deflate`/`Compression.None`.
    
    ## New options
    * **`Ssl`** (`SslClientAuthenticationOptions`) - adds client-certificate 
and custom-CA support. `SkipCertificateValidation` is applied to an internal 
copy rather than mutating the caller's options.
    * **`DefaultBatchSize`** (64) - connection-level default that fills the 
per-request `batchSize` when unset.
    * **`MaxResponseHeaderBytes`** - the maximum response header size, in bytes 
(converted internally to the handler's native kilobyte unit).
    * **`ReadTimeout`** - a per-read idle timeout applied to each read of the 
response stream (via per-read `CancelAfter`).
    * **`Proxy`** (`IWebProxy`) - routes connections through an HTTP proxy.
    * **`GremlinServer.FromUrl(string)`** / **`GremlinServer(Uri)`** - 
configure the endpoint from a single URL (scheme, host, port, path), deriving 
SSL from the scheme. The existing host/port/path constructor remains.
    
    ## Removed (breaking)
    * **`maxResponseContentLength`** - responses now stream; `ReadTimeout` is 
the partial mitigation.
    
    ## Bug fixes
    * Fixed deflate response decompression, which threw on the server's 
zlib-framed output because it used `DeflateStream` (raw DEFLATE, RFC 1951) 
instead of `ZLibStream` (zlib, RFC 1950). The bug was previously masked because 
compression was off by default; turning it on by default would have made it a 
default-path failure.
    * Fixed `Ssl` options cloning (used on the skip-certificate-validation 
path) to copy `ClientCertificateContext` and `AllowTlsResume`, which were 
previously dropped, breaking mTLS client certificates and silently re-enabling 
TLS resumption.
    
    ## Testing
    * `gremlin-dotnet` unit tests pass (695 tests), including new 
`ConnectionSettingsTests` and the `ReadTimeout` slow-read timeout test.
    * CHANGELOG, reference config table (`gremlin-variants.asciidoc`), and 
upgrade guide (`release-4.x.x.asciidoc`) updated for the .NET slice.
    
    Assisted-by: Kiro: Claude Opus 4.8
---
 CHANGELOG.asciidoc                                 |  13 +
 docs/src/reference/gremlin-variants.asciidoc       |  42 ++-
 docs/src/upgrade/release-4.x.x.asciidoc            |  40 +++
 gremlin-dotnet/Examples/Connections/Connections.cs |   4 +-
 gremlin-dotnet/src/Gremlin.Net/Driver/Auth.cs      |   5 +-
 .../src/Gremlin.Net/Driver/Compression.cs          | 107 ++++++++
 .../src/Gremlin.Net/Driver/Connection.cs           | 211 ++++++++++++++-
 .../src/Gremlin.Net/Driver/ConnectionSettings.cs   | 126 +++++++--
 .../src/Gremlin.Net/Driver/GremlinServer.cs        |  64 +++++
 .../Gremlin.Net/Driver/Messages/RequestMessage.cs  |  15 ++
 .../src/Gremlin.Net/Driver/ReadTimeoutStream.cs    | 111 ++++++++
 .../Gremlin.Net/Driver/StreamingResponseContext.cs |  12 +-
 .../CompressionBenchmarks.cs                       |   4 +-
 .../Docs/Reference/GremlinVariantsTests.cs         |   2 +-
 .../Driver/AuthIntegrationTests.cs                 |   6 +-
 .../test/Gremlin.Net.UnitTest/Driver/AuthTests.cs  |  28 +-
 .../Driver/ConnectionSettingsTests.cs              | 125 +++++++++
 .../Gremlin.Net.UnitTest/Driver/ConnectionTests.cs | 290 ++++++++++++++++++++-
 .../Driver/GremlinClientTests.cs                   |   2 +-
 .../Driver/GremlinServerTests.cs                   | 136 ++++++++++
 20 files changed, 1264 insertions(+), 79 deletions(-)

diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc
index 1902d3b8a7..c77c4062b9 100644
--- a/CHANGELOG.asciidoc
+++ b/CHANGELOG.asciidoc
@@ -25,6 +25,19 @@ 
image::https://raw.githubusercontent.com/apache/tinkerpop/master/docs/static/ima
 [[release-4-0-0]]
 === TinkerPop 4.0.0 (Release Date: NOT OFFICIALLY RELEASED YET)
 
+* Standardized `gremlin-dotnet` connection options per the TinkerPop 4.x GLV 
proposal:
+** Renamed the `Auth.BasicAuth`/`Auth.SigV4Auth` factory methods to 
`Auth.Basic`/`Auth.Sigv4` (breaking; the old methods have been removed). 
*(breaking)*
+** Renamed `MaxConnectionsPerServer` to `MaxConnections`, 
`IdleConnectionTimeout` to `IdleTimeout`, and `KeepAliveInterval` to 
`KeepAliveTime` (now wired to a real TCP keep-alive socket option rather than 
the inert HTTP/2 ping timeout, enabling `SO_KEEPALIVE` and setting the 
per-socket idle time on Windows, Linux, and macOS, with a no-op fallback to the 
OS default idle time on other platforms); the old property names have been 
removed. *(breaking)*
+** Renamed `ConnectionTimeout` to `ConnectTimeout` (default lowered from 15s 
to 5s; old name removed). *(breaking)*
+** Renamed `EnableCompression` to `Compression`, now a `{None, Deflate}` enum 
defaulting to `Deflate` (compression on by default; set `None` to disable). The 
old `EnableCompression` `bool` has been removed. *(breaking)*
+** Added `Ssl` (an `SslClientAuthenticationOptions`; 
`SkipCertificateValidation` is applied to an internal copy rather than mutating 
the caller's options).
+** Added `BatchSize` (default 64), a connection-level default that fills the 
per-request `batchSize` when unset.
+** Added `MaxResponseHeaderBytes`, exposing the handler's maximum response 
header size.
+** Added `ReadTimeout`, a per-read idle timeout applied to each read of the 
response stream.
+** Each timeout option is also settable in milliseconds via an `int` companion 
property (`ConnectTimeoutMillis`, `IdleTimeoutMillis`, `ReadTimeoutMillis`, 
`KeepAliveTimeMillis`); the unsuffixed `TimeSpan` property remains the 
idiomatic form and both reflect the same value.
+** Added `Proxy`, routing connections through an `IWebProxy`.
+* Fixed `gremlin-dotnet` deflate response decompression, which threw on the 
server's zlib-framed output because it used `DeflateStream` (raw DEFLATE, RFC 
1951) instead of `ZLibStream` (zlib, RFC 1950); the bug was previously masked 
because compression was off by default.
+* Fixed `gremlin-dotnet` SSL options cloning (used on the 
skip-certificate-validation path) to copy `ClientCertificateContext` and 
`AllowTlsResume`, which were previously dropped, breaking mTLS client 
certificates and silently re-enabling TLS resumption.
 * Standardized `gremlin-python` connection options per the TinkerPop 4.x GLV 
proposal:
 ** Renamed `pool_size` to `max_connections` (breaking; the old name has been 
removed) and changed the default from 8 to 128; `max_connections` is now also 
applied to the aiohttp `TCPConnector` `limit` so the transport layer reflects 
the option in addition to sizing the Connection pool.
 ** Renamed `ssl_options` to `ssl` accepting an `ssl.SSLContext` (breaking; the 
old name has been removed).
diff --git a/docs/src/reference/gremlin-variants.asciidoc 
b/docs/src/reference/gremlin-variants.asciidoc
index eb3e324a98..941e2e83dd 100644
--- a/docs/src/reference/gremlin-variants.asciidoc
+++ b/docs/src/reference/gremlin-variants.asciidoc
@@ -2589,7 +2589,7 @@ 
include::../../../gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Docs/Reference
 
 Authentication is handled through request interceptors. Interceptors are 
functions that modify the outgoing HTTP
 request before it is sent — they are used for authentication, custom headers, 
or request signing. The `Auth` class
-provides `BasicAuth()` and `SigV4Auth()` as built-in interceptors:
+provides `Basic()` and `SigV4()` as built-in interceptors:
 
 [source,csharp]
 ----
@@ -2597,12 +2597,12 @@ provides `BasicAuth()` and `SigV4Auth()` as built-in 
interceptors:
 var server = new GremlinServer("localhost", 8182, enableSsl: true);
 using var client = new GremlinClient(server,
     connectionSettings: new ConnectionSettings { SkipCertificateValidation = 
true },
-    interceptors: new[] { Auth.BasicAuth("username", "password") });
+    interceptors: new[] { Auth.Basic("username", "password") });
 
 // SigV4 authentication
 var server = new GremlinServer("localhost", 8182, enableSsl: true);
 using var client = new GremlinClient(server,
-    interceptors: new[] { Auth.SigV4Auth("service-region", "service-name") });
+    interceptors: new[] { Auth.Sigv4("service-region", "service-name") });
 ----
 
 If you authenticate to a remote <<connecting-gremlin-server,Gremlin Server>> or
@@ -2658,18 +2658,36 @@ constructor:
 [width="100%",cols="3,10,^2",options="header"]
 |=========================================================
 |Key |Description |Default
-|ConnectionTimeout |The TCP connection timeout. |15 s
-|IdleConnectionTimeout |How long idle connections stay in the pool before 
being closed. |180 s
-|MaxConnectionsPerServer |The maximum concurrent connections to a single 
server. |128
-|KeepAliveInterval |The TCP keep-alive probe interval. |30 s
-|EnableCompression |Whether to request deflate compression. |false
+|ConnectTimeoutMillis |The TCP connect timeout in milliseconds (transport 
establishment, i.e. TCP connect plus TLS handshake where applicable, not an 
HTTP request timeout). Also settable as `ConnectTimeout` (a `TimeSpan`). |5000
+|IdleTimeoutMillis |How long in milliseconds idle connections stay in the pool 
before being closed. Also settable as `IdleTimeout` (a `TimeSpan`). |180000
+|MaxConnections |The maximum concurrent connections to a single server. |128
+|KeepAliveTimeMillis |Idle time in milliseconds before TCP keep-alive probes 
begin on an otherwise idle connection. Enables `SO_KEEPALIVE` on the socket and 
sets the per-socket idle time on Windows, Linux, and macOS; on other platforms 
keep-alive stays enabled at the OS default idle time. Also settable as 
`KeepAliveTime` (a `TimeSpan`). |30000
+|Compression |The response compression algorithm (`Compression.None` or 
`Compression.Deflate`). |`Compression.Deflate`
+|BatchSize |The connection-level default batch size used to fill the 
per-request batch size when it is unset. |64
+|Ssl |The `SslClientAuthenticationOptions` used for HTTPS connections (client 
certificates, custom CA, protocols, etc.). `SkipCertificateValidation` is 
applied to an internal copy of these options rather than mutating the object 
you provide. |`null`
+|MaxResponseHeaderBytes |The maximum allowed size, in bytes, of the response 
headers. `0` leaves the handler default unchanged (converted internally to the 
handler's native kilobyte unit). |0
+|ReadTimeoutMillis |The idle-read timeout in milliseconds applied to each 
individual read of the response stream. It resets per chunk. `0` (the default) 
disables it. Also settable as `ReadTimeout` (a `TimeSpan`; 
`Timeout.InfiniteTimeSpan` disables). |0
+|Proxy |The `IWebProxy` used for connections. |`null`
 |EnableUserAgentOnConnect |Enables sending a user agent to the server on 
requests.
 More details can be found in provider docs
 
link:https://tinkerpop.apache.org/docs/x.y.z/dev/provider/#_graph_driver_provider_requirements[here].|true
 |BulkResults |Whether to send the bulkResults header on all requests. |false
-|SkipCertificateValidation |Whether to skip SSL certificate validation. Only 
use for testing with self-signed certificates. |false
+|SkipCertificateValidation |Whether to skip SSL certificate validation. Only 
use for testing with self-signed certificates. When `Ssl` is also provided, the 
accept-all callback is set on an internal copy so the supplied options object 
is never mutated. |false
 |=========================================================
 
+Note that no driver timeout bounds the *total* duration of a request once it 
is under way. `ReadTimeout` only bounds
+the gap between response chunks, so a response that keeps producing chunks 
will not time out no matter how long it
+runs overall, and there is no client-side "overall" request timeout. If you 
need an absolute deadline, impose it in
+your application by passing a cancellation token that cancels after the 
deadline:
+
+[source,csharp]
+----
+// bound the entire request (submit plus full result iteration) to 30 seconds
+using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
+var results = await client.SubmitAsync<Vertex>(
+    "g.V().out().out()", cancellationToken: cts.Token);
+----
+
 ==== GremlinClient Settings
 
 The following options can be passed to the `GremlinClient` constructor:
@@ -2699,7 +2717,7 @@ A request interceptor is a `Func<HttpRequestContext, 
Task>` that mutates the `Ht
 `Task` for async support but does not produce a value). A list of these is 
maintained and will be run sequentially for
 each request. When creating a `GremlinClient`, the `interceptors` parameter 
accepts an ordered collection of
 interceptors. Order matters, so if one interceptor depends on another's 
output, ensure they are added in the correct
-order. Note that authentication (e.g. `Auth.BasicAuth()`, `Auth.SigV4Auth()`) 
is also implemented using interceptors.
+order. Note that authentication (e.g. `Auth.Basic()`, `Auth.Sigv4()`) is also 
implemented using interceptors.
 These factory methods return interceptor delegates that can be included in the 
`interceptors` list. Alternatively, the
 `auth` parameter on `GremlinClient` appends the auth interceptor to the end of 
the list so it runs last.
 
@@ -2709,7 +2727,7 @@ var server = new GremlinServer("localhost", 8182);
 using var client = new GremlinClient(server,
     interceptors: new Func<HttpRequestContext, Task>[]
     {
-        Auth.BasicAuth("username", "password"),
+        Auth.Basic("username", "password"),
         context =>
         {
             context.Headers["X-Custom-Header"] = "value";
@@ -2881,7 +2899,7 @@ order and are useful for authentication, custom headers, 
or request signing.
 [source,csharp]
 ----
 var client = new GremlinClient(new GremlinServer("localhost", 8182),
-    interceptors: new[] { Auth.BasicAuth("username", "password") });
+    interceptors: new[] { Auth.Basic("username", "password") });
 ----
 
 When `requestSerializer` is set to `null`, the request body is passed as a 
`RequestMessage` to interceptors, and an
diff --git a/docs/src/upgrade/release-4.x.x.asciidoc 
b/docs/src/upgrade/release-4.x.x.asciidoc
index 8481489614..0c1a3ca37d 100644
--- a/docs/src/upgrade/release-4.x.x.asciidoc
+++ b/docs/src/upgrade/release-4.x.x.asciidoc
@@ -32,6 +32,46 @@ complete list of all the modifications that are part of this 
release.
 
 === Upgrading for Users
 
+==== Standardizing .NET Connection Options
+
+TinkerPop 4.x standardizes connection option names and defaults across the 
GLVs. In `gremlin-dotnet`, several
+`ConnectionSettings` properties and the `Auth` factory methods have been 
renamed for consistency. Because this is a
+major version, the old names have been removed rather than retained as 
aliases, and a number of new options have been
+added. The notes below describe the .NET changes. See <<glv-driver-changes, 
GLV Driver Changes>> for the
+equivalent changes in the other drivers.
+
+Renames (breaking). The following members have been renamed and the old names 
removed. Migrate to the new names:
+
+- `ConnectionTimeout` is now `ConnectTimeout`.
+- `MaxConnectionsPerServer` is now `MaxConnections`.
+- `IdleConnectionTimeout` is now `IdleTimeout`.
+- `KeepAliveInterval` is now `KeepAliveTime`.
+- `EnableCompression` is now `Compression`.
+- The `Auth.BasicAuth` and `Auth.SigV4Auth` factory methods are now 
`Auth.Basic` and `Auth.Sigv4`.
+
+Behavior changes. These change runtime behavior on upgrade, even if you do not 
change your configuration:
+
+- `ConnectTimeout` now defaults to 5s (lowered from 15s).
+- `KeepAliveTime` is now wired to a real TCP keep-alive socket option rather 
than the inert HTTP/2 ping timeout. It
+enables `SO_KEEPALIVE` and sets the per-socket idle time on Windows, Linux, 
and macOS; on other platforms keep-alive
+stays enabled at the OS default idle time.
+- `Compression` is now a `{None, Deflate}` enum defaulting to `Deflate` 
(compression on by default), so the driver
+sends `Accept-Encoding: deflate` by default. Set `Compression.None` to 
disable. The old `EnableCompression` `bool`
+has been removed.
+
+New options:
+
+- `Ssl` (an `SslClientAuthenticationOptions` for client certificates and 
custom CAs; `SkipCertificateValidation` is
+applied to an internal copy rather than mutating the supplied options).
+- `BatchSize` (default 64): a connection-level default that fills the 
per-request batch size when unset.
+- `MaxResponseHeaderBytes`: the maximum allowed size, in bytes, of the 
response headers.
+- `ReadTimeout`: a per-read idle timeout applied to each individual read of 
the response stream.
+- Each timeout is also settable in milliseconds via an `int` companion 
property (`ConnectTimeoutMillis`, `IdleTimeoutMillis`,
+  `ReadTimeoutMillis`, `KeepAliveTimeMillis`); the unsuffixed `TimeSpan` 
property is the idiomatic form.
+- `Proxy`: routes connections through an `IWebProxy`.
+
+See: 
link:https://lists.apache.org/thread/yqtr2wnb1kq2pqqq4002cz511q5o0bkg[[DISCUSS] 
Standardizing GLV connection options in TinkerPop 4].
+
 ==== Standardizing Go Connection Options
 
 TinkerPop 4.x standardizes connection option names and defaults across the 
GLVs. In `gremlin-go`, several
diff --git a/gremlin-dotnet/Examples/Connections/Connections.cs 
b/gremlin-dotnet/Examples/Connections/Connections.cs
index bcfda7e4f8..8e7354c201 100644
--- a/gremlin-dotnet/Examples/Connections/Connections.cs
+++ b/gremlin-dotnet/Examples/Connections/Connections.cs
@@ -54,7 +54,7 @@ public class ConnectionExample
         var server = new GremlinServer(ServerHost, ServerPort);
         var settings = new ConnectionSettings
         {
-            ConnectionTimeout = TimeSpan.FromSeconds(30),
+            ConnectTimeout = TimeSpan.FromSeconds(30),
         };
         using var remoteConnection = new DriverRemoteConnection(
             new GremlinClient(server, connectionSettings: settings), "g");
@@ -71,7 +71,7 @@ public class ConnectionExample
         var server = new GremlinServer(ServerHost, SecureServerPort, 
enableSsl: true);
         var client = new GremlinClient(server,
             connectionSettings: new ConnectionSettings { 
SkipCertificateValidation = true },
-            interceptors: new[] { Auth.BasicAuth("stephen", "password") });
+            interceptors: new[] { Auth.Basic("stephen", "password") });
         using var remoteConnection = new DriverRemoteConnection(client, "g");
         var g = Traversal().With(remoteConnection);
 
diff --git a/gremlin-dotnet/src/Gremlin.Net/Driver/Auth.cs 
b/gremlin-dotnet/src/Gremlin.Net/Driver/Auth.cs
index eaf706f6dd..d713dc9473 100644
--- a/gremlin-dotnet/src/Gremlin.Net/Driver/Auth.cs
+++ b/gremlin-dotnet/src/Gremlin.Net/Driver/Auth.cs
@@ -43,7 +43,7 @@ namespace Gremlin.Net.Driver
         /// <param name="username">The username.</param>
         /// <param name="password">The password.</param>
         /// <returns>A request interceptor delegate.</returns>
-        public static Func<HttpRequestContext, Task> BasicAuth(string 
username, string password)
+        public static Func<HttpRequestContext, Task> Basic(string username, 
string password)
         {
             var encoded = Convert.ToBase64String(
                 Encoding.UTF8.GetBytes(username + ":" + password));
@@ -68,7 +68,7 @@ namespace Gremlin.Net.Driver
         ///     Optional AWS credentials. When null, the default credential 
chain is used.
         /// </param>
         /// <returns>A request interceptor delegate.</returns>
-        public static Func<HttpRequestContext, Task> SigV4Auth(
+        public static Func<HttpRequestContext, Task> Sigv4(
             string region, string service, AWSCredentials? credentials = null)
         {
             // Cache the credential provider once when using the default chain.
@@ -105,6 +105,7 @@ namespace Gremlin.Net.Driver
             };
         }
 
+
         private static void SignRequest(HttpRequestContext context,
             ImmutableCredentials credentials, AWS4Signer signer, 
SigningClientConfig clientConfig)
         {
diff --git a/gremlin-dotnet/src/Gremlin.Net/Driver/Compression.cs 
b/gremlin-dotnet/src/Gremlin.Net/Driver/Compression.cs
new file mode 100644
index 0000000000..15bc0e5853
--- /dev/null
+++ b/gremlin-dotnet/src/Gremlin.Net/Driver/Compression.cs
@@ -0,0 +1,107 @@
+#region License
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.
+ */
+
+#endregion
+
+using System;
+
+namespace Gremlin.Net.Driver
+{
+    /// <summary>
+    ///     The compression algorithm requested for server responses. The 
server currently
+    ///     supports <see cref="Deflate"/> only; additional members are 
reserved for when the
+    ///     server adds support for them (server-side first).
+    /// </summary>
+    public enum CompressionType
+    {
+        /// <summary>
+        ///     No compression.
+        /// </summary>
+        None,
+
+        /// <summary>
+        ///     Deflate compression.
+        /// </summary>
+        Deflate
+    }
+
+    /// <summary>
+    ///     Configures response compression. A <see cref="CompressionType"/> 
is implicitly
+    ///     convertible (<see cref="CompressionType.Deflate"/> = <see 
cref="Deflate"/>,
+    ///     <see cref="CompressionType.None"/> = <see cref="None"/>).
+    /// </summary>
+    public readonly struct Compression : IEquatable<Compression>
+    {
+        /// <summary>
+        ///     Gets the configured compression algorithm.
+        /// </summary>
+        public CompressionType Type { get; }
+
+        /// <summary>
+        ///     Initializes a new instance of the <see cref="Compression"/> 
struct.
+        /// </summary>
+        /// <param name="type">The compression algorithm.</param>
+        public Compression(CompressionType type)
+        {
+            Type = type;
+        }
+
+        /// <summary>
+        ///     No compression.
+        /// </summary>
+        public static Compression None => new 
Compression(CompressionType.None);
+
+        /// <summary>
+        ///     Deflate compression.
+        /// </summary>
+        public static Compression Deflate => new 
Compression(CompressionType.Deflate);
+
+        /// <summary>
+        ///     Gets whether compression is enabled (i.e. the algorithm is not 
<see cref="CompressionType.None"/>).
+        /// </summary>
+        public bool Enabled => Type != CompressionType.None;
+
+        /// <summary>
+        ///     Implicitly converts a <see cref="CompressionType"/> to a <see 
cref="Compression"/>.
+        /// </summary>
+        /// <param name="type">The compression algorithm.</param>
+        public static implicit operator Compression(CompressionType type) =>
+            new Compression(type);
+
+        /// <inheritdoc />
+        public bool Equals(Compression other) => Type == other.Type;
+
+        /// <inheritdoc />
+        public override bool Equals(object? obj) => obj is Compression other 
&& Equals(other);
+
+        /// <inheritdoc />
+        public override int GetHashCode() => (int)Type;
+
+        /// <summary>Determines whether two <see cref="Compression"/> values 
are equal.</summary>
+        public static bool operator ==(Compression left, Compression right) => 
left.Equals(right);
+
+        /// <summary>Determines whether two <see cref="Compression"/> values 
are not equal.</summary>
+        public static bool operator !=(Compression left, Compression right) => 
!left.Equals(right);
+
+        /// <inheritdoc />
+        public override string ToString() => Type.ToString();
+    }
+}
diff --git a/gremlin-dotnet/src/Gremlin.Net/Driver/Connection.cs 
b/gremlin-dotnet/src/Gremlin.Net/Driver/Connection.cs
index 82a0d17724..7e16bffea9 100644
--- a/gremlin-dotnet/src/Gremlin.Net/Driver/Connection.cs
+++ b/gremlin-dotnet/src/Gremlin.Net/Driver/Connection.cs
@@ -67,18 +67,125 @@ namespace Gremlin.Net.Driver
 
             var handler = new SocketsHttpHandler
             {
-                PooledConnectionIdleTimeout = settings.IdleConnectionTimeout,
-                MaxConnectionsPerServer = settings.MaxConnectionsPerServer,
-                ConnectTimeout = settings.ConnectionTimeout,
-                KeepAlivePingTimeout = settings.KeepAliveInterval,
+                PooledConnectionIdleTimeout = settings.IdleTimeout,
+                MaxConnectionsPerServer = settings.MaxConnections,
+                ConnectTimeout = settings.ConnectTimeout,
             };
-            if (settings.SkipCertificateValidation)
+
+            // Rewire keep-alive to a real TCP socket option (HTTP/1.1). The 
handler's
+            // KeepAlivePingTimeout only applies to HTTP/2; instead open the 
socket ourselves
+            // in a ConnectCallback and set the TCP keep-alive idle time. 
Probe interval and
+            // count stay at OS defaults (not standardized).
+            var keepAliveTime = settings.KeepAliveTime;
+            handler.ConnectCallback = async (context, cancellationToken) =>
+            {
+                // Resolve the endpoint to concrete IP addresses and attempt 
each with its own
+                // socket. A single Socket cannot be reused across connection 
attempts (handing a
+                // multi-address DnsEndPoint to one socket throws "Sockets on 
this platform are
+                // invalid for use after a failed connection attempt"), so a 
fresh socket per
+                // address is required to support round-robin/fallback DNS.
+                var endpoint = context.DnsEndPoint;
+                System.Net.IPAddress[] addresses;
+                if (System.Net.IPAddress.TryParse(endpoint.Host, out var 
literal))
+                {
+                    addresses = new[] { literal };
+                }
+                else
+                {
+                    addresses = await System.Net.Dns.GetHostAddressesAsync(
+                        endpoint.Host, 
cancellationToken).ConfigureAwait(false);
+                }
+
+                if (addresses.Length == 0)
+                {
+                    throw new System.Net.Sockets.SocketException(
+                        (int)System.Net.Sockets.SocketError.HostNotFound);
+                }
+
+                System.Exception? lastError = null;
+                foreach (var address in addresses)
+                {
+                    var socket = new System.Net.Sockets.Socket(
+                        address.AddressFamily,
+                        System.Net.Sockets.SocketType.Stream,
+                        System.Net.Sockets.ProtocolType.Tcp)
+                    {
+                        NoDelay = true
+                    };
+                    try
+                    {
+                        
socket.SetSocketOption(System.Net.Sockets.SocketOptionLevel.Socket,
+                            System.Net.Sockets.SocketOptionName.KeepAlive, 
true);
+                        var keepAliveSeconds = (int)keepAliveTime.TotalSeconds;
+                        if (keepAliveSeconds > 0)
+                        {
+                            // Set the idle time before the first keep-alive 
probe. Windows/Linux use
+                            // the TcpKeepAliveTime enum; macOS uses the 
equivalent raw TCP_KEEPALIVE
+                            // option. Other platforms keep the OS default 
idle time.
+                            if (OperatingSystem.IsWindows() || 
OperatingSystem.IsLinux())
+                            {
+                                
socket.SetSocketOption(System.Net.Sockets.SocketOptionLevel.Tcp,
+                                    
System.Net.Sockets.SocketOptionName.TcpKeepAliveTime, keepAliveSeconds);
+                            }
+                            else if (OperatingSystem.IsMacOS())
+                            {
+                                // TCP_KEEPALIVE on macOS (<sys/socket.h>: 
0x10) is the idle-time knob,
+                                // the analog of Linux TCP_KEEPIDLE.
+                                const int tcpKeepAliveMacOs = 0x10;
+                                
socket.SetSocketOption(System.Net.Sockets.SocketOptionLevel.Tcp,
+                                    
(System.Net.Sockets.SocketOptionName)tcpKeepAliveMacOs, keepAliveSeconds);
+                            }
+                        }
+                        await socket.ConnectAsync(
+                            new System.Net.IPEndPoint(address, endpoint.Port), 
cancellationToken)
+                            .ConfigureAwait(false);
+                        return new System.Net.Sockets.NetworkStream(socket, 
ownsSocket: true);
+                    }
+                    catch (System.Exception ex)
+                    {
+                        socket.Dispose();
+                        lastError = ex;
+                    }
+                }
+
+                throw lastError ?? new System.Net.Sockets.SocketException(
+                    (int)System.Net.Sockets.SocketError.HostUnreachable);
+            };
+
+            // Configure SSL/TLS. Start from the user-supplied options (if 
any) so client
+            // certificates, custom CAs, and protocol settings are preserved. 
When
+            // SkipCertificateValidation is set we must NOT mutate the 
caller's options object
+            // (it is a reference type that may be shared across clients); 
instead we clone it
+            // and install the accept-all callback on the copy.
+            if (settings.Ssl != null || settings.SkipCertificateValidation)
             {
-                handler.SslOptions = new 
System.Net.Security.SslClientAuthenticationOptions
+                System.Net.Security.SslClientAuthenticationOptions sslOptions;
+                if (settings.SkipCertificateValidation)
                 {
-                    RemoteCertificateValidationCallback = (_, _, _, _) => true,
-                };
+                    sslOptions = CloneSslOptions(settings.Ssl);
+                    sslOptions.RemoteCertificateValidationCallback = (_, _, _, 
_) => true;
+                }
+                else
+                {
+                    sslOptions = settings.Ssl!;
+                }
+                handler.SslOptions = sslOptions;
             }
+
+            // Expose the max response header size. The native handler unit is 
kilobytes while
+            // the user provides bytes, so convert (rounding up to avoid 
silently lowering the cap).
+            if (settings.MaxResponseHeaderBytes > 0)
+            {
+                handler.MaxResponseHeadersLength =
+                    
MaxResponseHeaderBytesToKilobytes(settings.MaxResponseHeaderBytes);
+            }
+
+            if (settings.Proxy != null)
+            {
+                handler.Proxy = settings.Proxy;
+                handler.UseProxy = true;
+            }
+
             _httpClient = new HttpClient(handler);
         }
 
@@ -111,7 +218,18 @@ namespace Gremlin.Net.Driver
             var headers = new Dictionary<string, string>();
             headers["Accept"] = _responseSerializer.MimeType;
 
-            if (_settings.EnableCompression)
+            // Fill the per-request batch size from the connection-level 
default when the
+            // request did not set one. Build a copy for the outgoing request 
so the caller's
+            // RequestMessage is never mutated (resubmitting the same message 
must not pick up
+            // a previously injected default). A per-request explicit 
batchSize always wins.
+            var outgoingMessage = requestMessage;
+            if (!outgoingMessage.Fields.ContainsKey(Tokens.ArgsBatchSize))
+            {
+                outgoingMessage = outgoingMessage.CloneWithField(
+                    Tokens.ArgsBatchSize, _settings.BatchSize);
+            }
+
+            if (_settings.Compression.Type == CompressionType.Deflate)
             {
                 headers["Accept-Encoding"] = "deflate";
             }
@@ -129,13 +247,13 @@ namespace Gremlin.Net.Driver
             // Promote transactionId to HTTP header before interceptors run.
             // The field remains in the serialized body as well (dual 
transmission
             // per the HTTP transaction protocol specification).
-            if (requestMessage.Fields.TryGetValue(Tokens.ArgsTransactionId, 
out var txIdObj) &&
+            if (outgoingMessage.Fields.TryGetValue(Tokens.ArgsTransactionId, 
out var txIdObj) &&
                 txIdObj is string txId && !string.IsNullOrEmpty(txId))
             {
                 headers["X-Transaction-Id"] = txId;
             }
 
-            var context = new HttpRequestContext("POST", _uri, headers, 
requestMessage);
+            var context = new HttpRequestContext("POST", _uri, headers, 
outgoingMessage);
 
             foreach (var interceptor in _interceptors)
             {
@@ -212,13 +330,26 @@ namespace Gremlin.Net.Driver
             {
                 var contentStream = await response.Content.ReadAsStreamAsync()
                     .ConfigureAwait(false);
-                DeflateStream? deflateStream = null;
+
+                // Apply the per-read idle timeout (if configured) to the raw 
content stream so it
+                // covers both the compressed and decompressed read paths.
+                if (_settings.ReadTimeout > TimeSpan.Zero)
+                {
+                    contentStream = new ReadTimeoutStream(contentStream, 
_settings.ReadTimeout);
+                }
+
+                // The server (gremlin-server HttpContentCompressionHandler) 
compresses with
+                // java.util.zip.Deflater's default constructor, which emits a 
zlib-wrapped
+                // stream (RFC 1950: 2-byte header + Adler-32 checksum), not 
raw DEFLATE
+                // (RFC 1951). ZLibStream understands that wrapper; 
DeflateStream would throw
+                // on the zlib header.
+                Stream? decompressionStream = null;
                 if 
(response.Content.Headers.ContentEncoding.Contains("deflate"))
                 {
-                    deflateStream = new DeflateStream(contentStream, 
CompressionMode.Decompress);
+                    decompressionStream = new ZLibStream(contentStream, 
CompressionMode.Decompress);
                 }
                 streamingContext = new StreamingResponseContext(
-                    response, contentStream, deflateStream);
+                    response, contentStream, decompressionStream);
 
                 var resultStream = _responseSerializer.DeserializeMessageAsync(
                     streamingContext.Stream, cancellationToken);
@@ -283,6 +414,58 @@ namespace Gremlin.Net.Driver
             }
         }
 
+        /// <summary>
+        ///     Converts a maximum response header size expressed in bytes to 
the kilobyte unit
+        ///     used by <see 
cref="SocketsHttpHandler.MaxResponseHeadersLength"/>, rounding up so
+        ///     the configured byte cap is never silently lowered. For example 
1024 bytes maps to
+        ///     1 KB, 1025 bytes maps to 2 KB, and 8192 bytes maps to 8 KB. 
Callers only invoke
+        ///     this when <paramref name="maxResponseHeaderBytes"/> is 
positive.
+        /// </summary>
+        /// <param name="maxResponseHeaderBytes">The header cap in bytes 
(expected to be positive).</param>
+        /// <returns>The equivalent cap in kilobytes, rounded up.</returns>
+        internal static int MaxResponseHeaderBytesToKilobytes(int 
maxResponseHeaderBytes)
+        {
+            return (maxResponseHeaderBytes + 1023) / 1024;
+        }
+
+        /// <summary>
+        ///     Creates a shallow copy of the supplied
+        ///     <see 
cref="System.Net.Security.SslClientAuthenticationOptions"/> so the caller's
+        ///     object is never mutated when the skip-cert convenience is 
applied. Copies the
+        ///     commonly used properties; the accept-all
+        ///     <see 
cref="System.Net.Security.SslClientAuthenticationOptions.RemoteCertificateValidationCallback"/>
+        ///     is set on the returned copy by the caller.
+        /// </summary>
+        /// <param name="source">The caller-owned options to clone, or 
<c>null</c>.</param>
+        /// <returns>A new options instance carrying the copied 
settings.</returns>
+        private static System.Net.Security.SslClientAuthenticationOptions 
CloneSslOptions(
+            System.Net.Security.SslClientAuthenticationOptions? source)
+        {
+            var clone = new 
System.Net.Security.SslClientAuthenticationOptions();
+            if (source == null)
+            {
+                return clone;
+            }
+
+            clone.ClientCertificates = source.ClientCertificates;
+            clone.EnabledSslProtocols = source.EnabledSslProtocols;
+            clone.TargetHost = source.TargetHost;
+            // RemoteCertificateValidationCallback is intentionally NOT copied 
here: the caller
+            // overwrites it with the accept-all callback (skip-cert is the 
only path that clones).
+            clone.LocalCertificateSelectionCallback = 
source.LocalCertificateSelectionCallback;
+            clone.CipherSuitesPolicy = source.CipherSuitesPolicy;
+            clone.EncryptionPolicy = source.EncryptionPolicy;
+            clone.ApplicationProtocols = source.ApplicationProtocols;
+            clone.CertificateRevocationCheckMode = 
source.CertificateRevocationCheckMode;
+            clone.AllowRenegotiation = source.AllowRenegotiation;
+            // ClientCertificateContext carries the mTLS client certificate 
chain; omitting it
+            // would break client-certificate auth when combined with 
skip-cert.
+            clone.ClientCertificateContext = source.ClientCertificateContext;
+            // AllowTlsResume defaults to true, so it must be copied to honor 
a caller's false.
+            clone.AllowTlsResume = source.AllowTlsResume;
+            return clone;
+        }
+
         /// <summary>
         ///     Attempts to extract an error message from a JSON response body.
         ///     The server sometimes responds with a JSON object containing a 
"message" field
diff --git a/gremlin-dotnet/src/Gremlin.Net/Driver/ConnectionSettings.cs 
b/gremlin-dotnet/src/Gremlin.Net/Driver/ConnectionSettings.cs
index 83faebf7ed..5381acc055 100644
--- a/gremlin-dotnet/src/Gremlin.Net/Driver/ConnectionSettings.cs
+++ b/gremlin-dotnet/src/Gremlin.Net/Driver/ConnectionSettings.cs
@@ -22,6 +22,8 @@
 #endregion
 
 using System;
+using System.Net;
+using System.Net.Security;
 
 namespace Gremlin.Net.Driver
 {
@@ -31,49 +33,91 @@ namespace Gremlin.Net.Driver
     public class ConnectionSettings
     {
         /// <summary>
-        ///     The default TCP connection timeout.
+        ///     The default TCP connect timeout (transport establishment, i.e. 
TCP connect plus
+        ///     TLS handshake where applicable - not an HTTP request timeout).
         /// </summary>
-        public static readonly TimeSpan DefaultConnectionTimeout = 
TimeSpan.FromSeconds(15);
+        public static readonly TimeSpan DefaultConnectTimeout = 
TimeSpan.FromSeconds(5);
 
         /// <summary>
         ///     The default idle connection timeout.
         /// </summary>
-        public static readonly TimeSpan DefaultIdleConnectionTimeout = 
TimeSpan.FromSeconds(180);
+        public static readonly TimeSpan DefaultIdleTimeout = 
TimeSpan.FromSeconds(180);
 
         /// <summary>
-        ///     The default maximum connections per server.
+        ///     The default maximum concurrent connections to a single server.
         /// </summary>
-        public const int DefaultMaxConnectionsPerServer = 128;
+        public const int DefaultMaxConnections = 128;
 
         /// <summary>
-        ///     The default TCP keep-alive probe interval.
+        ///     The default TCP keep-alive idle time (how long a connection is 
idle before the
+        ///     first keep-alive probe is sent).
         /// </summary>
-        public static readonly TimeSpan DefaultKeepAliveInterval = 
TimeSpan.FromSeconds(30);
+        public static readonly TimeSpan DefaultKeepAliveTime = 
TimeSpan.FromSeconds(30);
 
         /// <summary>
-        ///     Gets or sets the TCP connection timeout.
+        ///     The default batch size used to fill the per-request batch size 
when it is unset.
         /// </summary>
-        public TimeSpan ConnectionTimeout { get; set; } = 
DefaultConnectionTimeout;
+        public const int DefaultBatchSizeValue = 64;
+
+        /// <summary>
+        ///     Gets or sets the TCP connect timeout. This is a 
transport-establishment timeout
+        ///     (TCP connect plus TLS handshake where applicable), not an HTTP 
request timeout.
+        /// </summary>
+        public TimeSpan ConnectTimeout { get; set; } = DefaultConnectTimeout;
+
+        /// <summary>
+        ///     Gets or sets <see cref="ConnectTimeout"/> in whole 
milliseconds. This is the millisecond
+        ///     view of the same setting; <see cref="ConnectTimeout"/> is the 
idiomatic <see cref="TimeSpan"/> form.
+        /// </summary>
+        public int ConnectTimeoutMillis
+        {
+            get => (int) ConnectTimeout.TotalMilliseconds;
+            set => ConnectTimeout = TimeSpan.FromMilliseconds(value);
+        }
 
         /// <summary>
         ///     Gets or sets how long idle connections stay in the pool before 
being closed.
         /// </summary>
-        public TimeSpan IdleConnectionTimeout { get; set; } = 
DefaultIdleConnectionTimeout;
+        public TimeSpan IdleTimeout { get; set; } = DefaultIdleTimeout;
+
+        /// <summary>
+        ///     Gets or sets <see cref="IdleTimeout"/> in whole milliseconds. 
This is the millisecond
+        ///     view of the same setting; <see cref="IdleTimeout"/> is the 
idiomatic <see cref="TimeSpan"/> form.
+        /// </summary>
+        public int IdleTimeoutMillis
+        {
+            get => (int) IdleTimeout.TotalMilliseconds;
+            set => IdleTimeout = TimeSpan.FromMilliseconds(value);
+        }
 
         /// <summary>
         ///     Gets or sets the maximum concurrent connections to a single 
server.
         /// </summary>
-        public int MaxConnectionsPerServer { get; set; } = 
DefaultMaxConnectionsPerServer;
+        public int MaxConnections { get; set; } = DefaultMaxConnections;
+
+        /// <summary>
+        ///     Gets or sets the TCP keep-alive idle time: how long a 
connection may be idle
+        ///     before the first keep-alive probe is sent. Probe interval and 
count stay at OS
+        ///     defaults.
+        /// </summary>
+        public TimeSpan KeepAliveTime { get; set; } = DefaultKeepAliveTime;
 
         /// <summary>
-        ///     Gets or sets the TCP keep-alive probe interval.
+        ///     Gets or sets <see cref="KeepAliveTime"/> in whole 
milliseconds. This is the millisecond
+        ///     view of the same setting; <see cref="KeepAliveTime"/> is the 
idiomatic <see cref="TimeSpan"/> form.
         /// </summary>
-        public TimeSpan KeepAliveInterval { get; set; } = 
DefaultKeepAliveInterval;
+        public int KeepAliveTimeMillis
+        {
+            get => (int) KeepAliveTime.TotalMilliseconds;
+            set => KeepAliveTime = TimeSpan.FromMilliseconds(value);
+        }
 
         /// <summary>
-        ///     Gets or sets whether to request deflate compression.
+        ///     Gets or sets the response compression algorithm. Defaults to
+        ///     <see cref="Driver.Compression.Deflate"/> (on); set <see 
cref="Driver.Compression.None"/>
+        ///     to disable.
         /// </summary>
-        public bool EnableCompression { get; set; } = false;
+        public Compression Compression { get; set; } = Compression.Deflate;
 
         /// <summary>
         ///     Gets or sets whether to send the User-Agent header.
@@ -85,10 +129,60 @@ namespace Gremlin.Net.Driver
         /// </summary>
         public bool BulkResults { get; set; } = false;
 
+        /// <summary>
+        ///     Gets or sets the connection-level default batch size that 
fills the per-request batch size
+        ///     when it is not set on the request (client-side 
default-filling; no wire change).
+        /// </summary>
+        public int BatchSize { get; set; } = DefaultBatchSizeValue;
+
+        /// <summary>
+        ///     Gets or sets the SSL/TLS options used for HTTPS connections 
(client certificates,
+        ///     custom CA, protocols, etc.). When set, these options are used 
to configure the
+        ///     underlying handler. <see cref="SkipCertificateValidation"/> is 
applied to an
+        ///     internal copy of these options rather than mutating the object 
provided here.
+        /// </summary>
+        public SslClientAuthenticationOptions? Ssl { get; set; }
+
         /// <summary>
         ///     Gets or sets whether to skip SSL certificate validation.
-        ///     Only use for testing with self-signed certificates.
+        ///     Only use for testing with self-signed certificates. When <see 
cref="Ssl"/> is also
+        ///     provided, the accept-all callback is set on an internal copy 
of those options so the
+        ///     caller's <see cref="SslClientAuthenticationOptions"/> instance 
is never mutated
+        ///     (which is important when one instance is shared across 
multiple clients).
         /// </summary>
         public bool SkipCertificateValidation { get; set; } = false;
+
+        /// <summary>
+        ///     Gets or sets the maximum allowed size, in bytes, of the 
response headers.
+        ///     A value of <c>0</c> (the default) leaves the handler default 
unchanged. The native
+        ///     handler unit is kilobytes; the byte value provided here is 
converted internally.
+        /// </summary>
+        public int MaxResponseHeaderBytes { get; set; } = 0;
+
+        /// <summary>
+        ///     Gets or sets the idle-read timeout applied to each individual 
read of the response
+        ///     stream. It resets per chunk, so it is an idle-read timeout 
rather than a
+        ///     whole-request deadline. <see 
cref="System.Threading.Timeout.InfiniteTimeSpan"/>
+        ///     (the default) disables it.
+        /// </summary>
+        public TimeSpan ReadTimeout { get; set; } = 
System.Threading.Timeout.InfiniteTimeSpan;
+
+        /// <summary>
+        ///     Gets or sets <see cref="ReadTimeout"/> in whole milliseconds, 
where <c>0</c> disables it
+        ///     (mapping to <see 
cref="System.Threading.Timeout.InfiniteTimeSpan"/>). This is the millisecond
+        ///     view of the same setting; <see cref="ReadTimeout"/> is the 
idiomatic <see cref="TimeSpan"/> form.
+        /// </summary>
+        public int ReadTimeoutMillis
+        {
+            get => ReadTimeout <= TimeSpan.Zero ? 0 : (int) 
ReadTimeout.TotalMilliseconds;
+            set => ReadTimeout = value <= 0 ? 
System.Threading.Timeout.InfiniteTimeSpan : TimeSpan.FromMilliseconds(value);
+        }
+
+        /// <summary>
+        ///     Gets or sets the HTTP proxy used for connections. When set, it 
is applied to the
+        ///     underlying handler explicitly.
+        /// </summary>
+        public IWebProxy? Proxy { get; set; }
+
     }
 }
diff --git a/gremlin-dotnet/src/Gremlin.Net/Driver/GremlinServer.cs 
b/gremlin-dotnet/src/Gremlin.Net/Driver/GremlinServer.cs
index 0d8a41dfd1..0e023cfa5c 100644
--- a/gremlin-dotnet/src/Gremlin.Net/Driver/GremlinServer.cs
+++ b/gremlin-dotnet/src/Gremlin.Net/Driver/GremlinServer.cs
@@ -43,6 +43,62 @@ namespace Gremlin.Net.Driver
             Uri = CreateUri(hostname, port, enableSsl, path);
         }
 
+        /// <summary>
+        ///     Creates a new instance of the <see cref="GremlinServer" /> 
class from a single URL.
+        /// </summary>
+        /// <param name="url">
+        ///     The URL of the Gremlin endpoint, e.g. 
<c>https://localhost:8182/gremlin</c>. The scheme determines
+        ///     whether SSL is enabled (<c>https</c> enables it, <c>http</c> 
disables it) and the host, port and path
+        ///     are taken from the URL. When the URL omits the port the 
default <c>8182</c> is used, and when it omits
+        ///     the path the default <c>/gremlin</c> is used.
+        /// </param>
+        /// <returns>A new <see cref="GremlinServer" /> configured from the 
given URL.</returns>
+        /// <exception cref="ArgumentNullException">Thrown when <paramref 
name="url" /> is null.</exception>
+        /// <exception cref="ArgumentException">
+        ///     Thrown when <paramref name="url" /> is not a valid absolute 
URL or does not use the
+        ///     <c>http</c> or <c>https</c> scheme.
+        /// </exception>
+        public static GremlinServer FromUrl(string url)
+        {
+            if (url == null) throw new ArgumentNullException(nameof(url));
+            if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))
+                throw new ArgumentException($"'{url}' is not a valid absolute 
URL.", nameof(url));
+            return new GremlinServer(uri);
+        }
+
+        /// <summary>
+        ///     Initializes a new instance of the <see cref="GremlinServer" /> 
class from a <see cref="System.Uri" />.
+        /// </summary>
+        /// <param name="uri">
+        ///     The URI of the Gremlin endpoint. The scheme determines whether 
SSL is enabled (<c>https</c> enables it,
+        ///     <c>http</c> disables it). When the URI omits the port the 
default <c>8182</c> is used, and when it omits
+        ///     the path the default <c>/gremlin</c> is used.
+        /// </param>
+        /// <exception cref="ArgumentNullException">Thrown when <paramref 
name="uri" /> is null.</exception>
+        /// <exception cref="ArgumentException">
+        ///     Thrown when <paramref name="uri" /> does not use the 
<c>http</c> or <c>https</c> scheme.
+        /// </exception>
+        public GremlinServer(Uri uri)
+        {
+            if (uri == null) throw new ArgumentNullException(nameof(uri));
+            ValidateScheme(uri.Scheme);
+
+            var enableSsl = string.Equals(uri.Scheme, "https", 
StringComparison.OrdinalIgnoreCase);
+
+            // Only override the port when the URL specifies one; otherwise 
keep the default 8182.
+            // System.Uri auto-fills the scheme default port (80/443) and 
flags it via IsDefaultPort,
+            // so treat that case as "not specified".
+            var port = uri.IsDefaultPort ? 8182 : uri.Port;
+
+            // Likewise, only override the path when the URL has a non-empty 
path, otherwise keep the default
+            // /gremlin. System.Uri turns a path-less URL into AbsolutePath 
"/", so treat "/" or empty as default.
+            var path = string.IsNullOrEmpty(uri.AbsolutePath) || 
uri.AbsolutePath == "/"
+                ? "/gremlin"
+                : uri.AbsolutePath;
+
+            Uri = CreateUri(uri.Host, port, enableSsl, path);
+        }
+
         /// <summary>
         ///     Gets the URI of the Gremlin Server.
         /// </summary>
@@ -53,5 +109,13 @@ namespace Gremlin.Net.Driver
             var scheme = enableSsl ? "https" : "http";
             return new Uri($"{scheme}://{hostname}:{port}{path}");
         }
+
+        private static void ValidateScheme(string scheme)
+        {
+            if (!string.Equals(scheme, "http", 
StringComparison.OrdinalIgnoreCase) &&
+                !string.Equals(scheme, "https", 
StringComparison.OrdinalIgnoreCase))
+                throw new ArgumentException(
+                    $"Unsupported scheme '{scheme}'. Only 'http' and 'https' 
are supported.");
+        }
     }
 }
diff --git a/gremlin-dotnet/src/Gremlin.Net/Driver/Messages/RequestMessage.cs 
b/gremlin-dotnet/src/Gremlin.Net/Driver/Messages/RequestMessage.cs
index 2ebd02cc29..e843f43d9e 100644
--- a/gremlin-dotnet/src/Gremlin.Net/Driver/Messages/RequestMessage.cs
+++ b/gremlin-dotnet/src/Gremlin.Net/Driver/Messages/RequestMessage.cs
@@ -52,6 +52,21 @@ namespace Gremlin.Net.Driver.Messages
         /// </summary>
         public Dictionary<string, object> Fields { get; }
 
+        /// <summary>
+        ///     Returns a copy of this message with <paramref name="key"/> set 
to
+        ///     <paramref name="value"/>, without mutating this instance (and 
therefore without
+        ///     mutating the caller-owned message). Used to fill 
connection-level defaults onto
+        ///     the outgoing request only.
+        /// </summary>
+        /// <param name="key">The field key to set on the copy.</param>
+        /// <param name="value">The field value to set on the copy.</param>
+        /// <returns>A new <see cref="RequestMessage"/> carrying the added 
field.</returns>
+        internal RequestMessage CloneWithField(string key, object value)
+        {
+            var copiedFields = new Dictionary<string, object>(Fields) { [key] 
= value };
+            return new RequestMessage(Gremlin, copiedFields);
+        }
+
         /// <summary>
         ///     Initializes a <see cref="Builder" /> to build a <see 
cref="RequestMessage" />.
         /// </summary>
diff --git a/gremlin-dotnet/src/Gremlin.Net/Driver/ReadTimeoutStream.cs 
b/gremlin-dotnet/src/Gremlin.Net/Driver/ReadTimeoutStream.cs
new file mode 100644
index 0000000000..aa006ed6a0
--- /dev/null
+++ b/gremlin-dotnet/src/Gremlin.Net/Driver/ReadTimeoutStream.cs
@@ -0,0 +1,111 @@
+#region License
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.
+ */
+
+#endregion
+
+using System;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Gremlin.Net.Driver
+{
+    /// <summary>
+    ///     Wraps a stream and applies an idle-read timeout to each individual 
read by linking a
+    ///     <see cref="CancellationTokenSource.CancelAfter(TimeSpan)"/> with 
the caller's token.
+    ///     The timeout resets per chunk, so it is an idle-read timeout rather 
than a whole-request
+    ///     deadline. A non-positive timeout disables the behavior.
+    /// </summary>
+    internal sealed class ReadTimeoutStream : Stream
+    {
+        private readonly Stream _inner;
+        private readonly TimeSpan _readTimeout;
+
+        public ReadTimeoutStream(Stream inner, TimeSpan readTimeout)
+        {
+            _inner = inner ?? throw new ArgumentNullException(nameof(inner));
+            _readTimeout = readTimeout;
+        }
+
+        private bool TimeoutEnabled => _readTimeout > TimeSpan.Zero;
+
+        public override async ValueTask<int> ReadAsync(Memory<byte> buffer,
+            CancellationToken cancellationToken = default)
+        {
+            if (!TimeoutEnabled)
+            {
+                return await _inner.ReadAsync(buffer, 
cancellationToken).ConfigureAwait(false);
+            }
+
+            using var timeoutCts = new CancellationTokenSource();
+            using var linkedCts =
+                
CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, 
timeoutCts.Token);
+            timeoutCts.CancelAfter(_readTimeout);
+            try
+            {
+                return await _inner.ReadAsync(buffer, 
linkedCts.Token).ConfigureAwait(false);
+            }
+            catch (OperationCanceledException) when 
(timeoutCts.IsCancellationRequested &&
+                                                     
!cancellationToken.IsCancellationRequested)
+            {
+                throw new TimeoutException(
+                    $"Read timed out after {_readTimeout.TotalSeconds:0.###}s 
waiting for response data.");
+            }
+        }
+
+        public override Task<int> ReadAsync(byte[] buffer, int offset, int 
count,
+            CancellationToken cancellationToken)
+        {
+            return ReadAsync(buffer.AsMemory(offset, count), 
cancellationToken).AsTask();
+        }
+
+        public override int Read(byte[] buffer, int offset, int count)
+        {
+            // The driver always reads asynchronously; provide a correct sync 
fallback.
+            return _inner.Read(buffer, offset, count);
+        }
+
+        public override bool CanRead => _inner.CanRead;
+        public override bool CanSeek => false;
+        public override bool CanWrite => false;
+        public override long Length => throw new NotSupportedException();
+
+        public override long Position
+        {
+            get => throw new NotSupportedException();
+            set => throw new NotSupportedException();
+        }
+
+        public override void Flush() => _inner.Flush();
+        public override long Seek(long offset, SeekOrigin origin) => throw new 
NotSupportedException();
+        public override void SetLength(long value) => throw new 
NotSupportedException();
+        public override void Write(byte[] buffer, int offset, int count) => 
throw new NotSupportedException();
+
+        protected override void Dispose(bool disposing)
+        {
+            if (disposing)
+            {
+                _inner.Dispose();
+            }
+            base.Dispose(disposing);
+        }
+    }
+}
diff --git a/gremlin-dotnet/src/Gremlin.Net/Driver/StreamingResponseContext.cs 
b/gremlin-dotnet/src/Gremlin.Net/Driver/StreamingResponseContext.cs
index 9d40e1f39f..a61ed2c647 100644
--- a/gremlin-dotnet/src/Gremlin.Net/Driver/StreamingResponseContext.cs
+++ b/gremlin-dotnet/src/Gremlin.Net/Driver/StreamingResponseContext.cs
@@ -36,26 +36,26 @@ namespace Gremlin.Net.Driver
     {
         private readonly HttpResponseMessage _response;
         private readonly Stream _contentStream;
-        private readonly DeflateStream? _deflateStream;
+        private readonly Stream? _decompressionStream;
 
         /// <summary>
         ///     Gets the stream to read from — the decompression stream if 
present,
         ///     otherwise the raw content stream.
         /// </summary>
-        public Stream Stream => (Stream?)_deflateStream ?? _contentStream;
+        public Stream Stream => _decompressionStream ?? _contentStream;
 
         /// <summary>
         ///     Initializes a new instance of the <see 
cref="StreamingResponseContext"/> class.
         /// </summary>
         /// <param name="response">The HTTP response message.</param>
         /// <param name="contentStream">The raw content stream from the 
response.</param>
-        /// <param name="deflateStream">An optional deflate decompression 
stream wrapping the content stream.</param>
+        /// <param name="decompressionStream">An optional decompression stream 
wrapping the content stream.</param>
         public StreamingResponseContext(HttpResponseMessage response, Stream 
contentStream,
-            DeflateStream? deflateStream = null)
+            Stream? decompressionStream = null)
         {
             _response = response;
             _contentStream = contentStream;
-            _deflateStream = deflateStream;
+            _decompressionStream = decompressionStream;
         }
 
         /// <summary>
@@ -63,7 +63,7 @@ namespace Gremlin.Net.Driver
         /// </summary>
         public void Dispose()
         {
-            _deflateStream?.Dispose();
+            _decompressionStream?.Dispose();
             _contentStream.Dispose();
             _response.Dispose();
         }
diff --git 
a/gremlin-dotnet/test/Gremlin.Net.Benchmarks/CompressionBenchmarks.cs 
b/gremlin-dotnet/test/Gremlin.Net.Benchmarks/CompressionBenchmarks.cs
index ac51e17f0a..8e8abf462b 100644
--- a/gremlin-dotnet/test/Gremlin.Net.Benchmarks/CompressionBenchmarks.cs
+++ b/gremlin-dotnet/test/Gremlin.Net.Benchmarks/CompressionBenchmarks.cs
@@ -34,14 +34,14 @@ public class CompressionBenchmarks
     public static async Task GraphBinaryWithoutCompression()
     {
         var client = new GremlinClient(new GremlinServer("localhost", 45940),
-            connectionSettings: new ConnectionSettings { EnableCompression = 
false });
+            connectionSettings: new ConnectionSettings { Compression = 
Compression.None });
         await PerformBenchmarkWithClient(client);
     }
 
     public static async Task GraphBinaryWithCompression()
     {
         var client = new GremlinClient(new GremlinServer("localhost", 45940),
-            connectionSettings: new ConnectionSettings { EnableCompression = 
true });
+            connectionSettings: new ConnectionSettings { Compression = 
Compression.Deflate });
         await PerformBenchmarkWithClient(client);
     }
 
diff --git 
a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Docs/Reference/GremlinVariantsTests.cs
 
b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Docs/Reference/GremlinVariantsTests.cs
index be2506151e..b67d7a7ecb 100644
--- 
a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Docs/Reference/GremlinVariantsTests.cs
+++ 
b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Docs/Reference/GremlinVariantsTests.cs
@@ -143,7 +143,7 @@ var username = "username";
 var password = "password";
 var gremlinServer = new GremlinServer("localhost", 8182, enableSsl: true);
 using var gremlinClient = new GremlinClient(gremlinServer,
-    interceptors: new[] { Auth.BasicAuth(username, password) });
+    interceptors: new[] { Auth.Basic(username, password) });
 // end::submittingScriptsWithAuthentication[]
         }
         
diff --git 
a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Driver/AuthIntegrationTests.cs
 
b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Driver/AuthIntegrationTests.cs
index 3b5c9eed07..ca564f987a 100644
--- 
a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Driver/AuthIntegrationTests.cs
+++ 
b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Driver/AuthIntegrationTests.cs
@@ -53,7 +53,7 @@ namespace Gremlin.Net.IntegrationTest.Driver
         {
             // The secure server uses SimpleAuthenticator with credentials: 
stephen/password
             using var gremlinClient = CreateSecureClient(
-                new[] { Auth.BasicAuth("stephen", "password") });
+                new[] { Auth.Basic("stephen", "password") });
 
             var response = await 
gremlinClient.SubmitAsync<long>("g.inject(1).count()");
 
@@ -65,7 +65,7 @@ namespace Gremlin.Net.IntegrationTest.Driver
         {
             // Test through DriverRemoteConnection + traversal
             using var client = CreateSecureClient(
-                new[] { Auth.BasicAuth("stephen", "password") });
+                new[] { Auth.Basic("stephen", "password") });
             using var remote = new DriverRemoteConnection(client, "gmodern");
             var g = AnonymousTraversalSource.Traversal().With(remote);
 
@@ -78,7 +78,7 @@ namespace Gremlin.Net.IntegrationTest.Driver
         public async Task ShouldFailWithWrongCredentials()
         {
             using var gremlinClient = CreateSecureClient(
-                new[] { Auth.BasicAuth("stephen", "wrongpassword") });
+                new[] { Auth.Basic("stephen", "wrongpassword") });
 
             // The server returns auth errors as JSON (not GraphBinary), so 
Connection
             // extracts the message and throws HttpRequestException.
diff --git a/gremlin-dotnet/test/Gremlin.Net.UnitTest/Driver/AuthTests.cs 
b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Driver/AuthTests.cs
index 40f5239e44..76c9910d7f 100644
--- a/gremlin-dotnet/test/Gremlin.Net.UnitTest/Driver/AuthTests.cs
+++ b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Driver/AuthTests.cs
@@ -43,7 +43,7 @@ namespace Gremlin.Net.UnitTest.Driver
         [Fact]
         public async Task BasicAuthShouldSetCorrectAuthorizationHeader()
         {
-            var interceptor = Auth.BasicAuth("user", "pass");
+            var interceptor = Auth.Basic("user", "pass");
             var context = CreateTestContext();
 
             await interceptor(context);
@@ -55,7 +55,7 @@ namespace Gremlin.Net.UnitTest.Driver
         [Fact]
         public async Task BasicAuthShouldSetHeaderOnEveryInvocation()
         {
-            var interceptor = Auth.BasicAuth("user", "pass");
+            var interceptor = Auth.Basic("user", "pass");
             var context1 = CreateTestContext();
             var context2 = CreateTestContext();
 
@@ -70,7 +70,7 @@ namespace Gremlin.Net.UnitTest.Driver
         [Fact]
         public async Task BasicAuthShouldHandleColonsInPassword()
         {
-            var interceptor = Auth.BasicAuth("user", "pass:with:colons");
+            var interceptor = Auth.Basic("user", "pass:with:colons");
             var context = CreateTestContext();
 
             await interceptor(context);
@@ -83,7 +83,7 @@ namespace Gremlin.Net.UnitTest.Driver
         [Fact]
         public async Task BasicAuthShouldHandleUnicodeCharacters()
         {
-            var interceptor = Auth.BasicAuth("用户", "密码");
+            var interceptor = Auth.Basic("用户", "密码");
             var context = CreateTestContext();
 
             await interceptor(context);
@@ -96,7 +96,7 @@ namespace Gremlin.Net.UnitTest.Driver
         [Fact]
         public async Task BasicAuthShouldOverwriteExistingAuthorizationHeader()
         {
-            var interceptor = Auth.BasicAuth("user", "pass");
+            var interceptor = Auth.Basic("user", "pass");
             var context = CreateTestContext();
             context.Headers["Authorization"] = "Bearer old-token";
 
@@ -109,7 +109,7 @@ namespace Gremlin.Net.UnitTest.Driver
         [Fact]
         public async Task BasicAuthShouldHandleEmptyCredentials()
         {
-            var interceptor = Auth.BasicAuth("", "");
+            var interceptor = Auth.Basic("", "");
             var context = CreateTestContext();
 
             await interceptor(context);
@@ -140,7 +140,7 @@ namespace Gremlin.Net.UnitTest.Driver
         [Fact]
         public async Task SigV4AuthShouldAddRequiredHeaders()
         {
-            var interceptor = Auth.SigV4Auth("gremlin-east-1", 
"tinkerpop-sigv4", TestBasicCredentials);
+            var interceptor = Auth.Sigv4("gremlin-east-1", "tinkerpop-sigv4", 
TestBasicCredentials);
             var context = CreateSigv4TestContext();
 
             await interceptor(context);
@@ -154,7 +154,7 @@ namespace Gremlin.Net.UnitTest.Driver
         [Fact]
         public async Task SigV4AuthShouldHaveCorrectAuthorizationPrefix()
         {
-            var interceptor = Auth.SigV4Auth("gremlin-west-2", 
"tinkerpop-sigv4", TestBasicCredentials);
+            var interceptor = Auth.Sigv4("gremlin-west-2", "tinkerpop-sigv4", 
TestBasicCredentials);
             var context = CreateSigv4TestContext();
 
             await interceptor(context);
@@ -166,7 +166,7 @@ namespace Gremlin.Net.UnitTest.Driver
         [Fact]
         public async Task 
SigV4AuthShouldAddSessionTokenForTemporaryCredentials()
         {
-            var interceptor = Auth.SigV4Auth("gremlin-east-1", 
"tinkerpop-sigv4", TestSessionCredentials);
+            var interceptor = Auth.Sigv4("gremlin-east-1", "tinkerpop-sigv4", 
TestSessionCredentials);
             var context = CreateSigv4TestContext();
 
             await interceptor(context);
@@ -178,7 +178,7 @@ namespace Gremlin.Net.UnitTest.Driver
         [Fact]
         public async Task 
SigV4AuthShouldNotAddSessionTokenForPermanentCredentials()
         {
-            var interceptor = Auth.SigV4Auth("gremlin-east-1", 
"tinkerpop-sigv4", TestBasicCredentials);
+            var interceptor = Auth.Sigv4("gremlin-east-1", "tinkerpop-sigv4", 
TestBasicCredentials);
             var context = CreateSigv4TestContext();
 
             await interceptor(context);
@@ -190,7 +190,7 @@ namespace Gremlin.Net.UnitTest.Driver
         public async Task SigV4AuthContentHashShouldMatchBodySha256()
         {
             var body = new byte[] { 0x84, 0x00, 0xFD, 0x01 };
-            var interceptor = Auth.SigV4Auth("gremlin-east-1", 
"tinkerpop-sigv4", TestBasicCredentials);
+            var interceptor = Auth.Sigv4("gremlin-east-1", "tinkerpop-sigv4", 
TestBasicCredentials);
             var context = CreateSigv4TestContext(body);
 
             await interceptor(context);
@@ -204,7 +204,7 @@ namespace Gremlin.Net.UnitTest.Driver
         [Fact]
         public async Task SigV4AuthShouldHandleEmptyBody()
         {
-            var interceptor = Auth.SigV4Auth("gremlin-east-1", 
"tinkerpop-sigv4", TestBasicCredentials);
+            var interceptor = Auth.Sigv4("gremlin-east-1", "tinkerpop-sigv4", 
TestBasicCredentials);
             var context = CreateSigv4TestContext(Array.Empty<byte>());
 
             await interceptor(context);
@@ -217,7 +217,7 @@ namespace Gremlin.Net.UnitTest.Driver
         [Fact]
         public async Task SigV4AuthShouldSetCorrectHost()
         {
-            var interceptor = Auth.SigV4Auth("gremlin-east-1", 
"tinkerpop-sigv4", TestBasicCredentials);
+            var interceptor = Auth.Sigv4("gremlin-east-1", "tinkerpop-sigv4", 
TestBasicCredentials);
             var context = CreateSigv4TestContext();
 
             await interceptor(context);
@@ -228,7 +228,7 @@ namespace Gremlin.Net.UnitTest.Driver
         [Fact]
         public async Task SigV4AuthShouldThrowWhenBodyIsNotByteArray()
         {
-            var interceptor = Auth.SigV4Auth("gremlin-east-1", 
"tinkerpop-sigv4", TestBasicCredentials);
+            var interceptor = Auth.Sigv4("gremlin-east-1", "tinkerpop-sigv4", 
TestBasicCredentials);
             var context = new HttpRequestContext("POST", new 
Uri("https://example.com:8182/gremlin";),
                 new Dictionary<string, string>
                 {
diff --git 
a/gremlin-dotnet/test/Gremlin.Net.UnitTest/Driver/ConnectionSettingsTests.cs 
b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Driver/ConnectionSettingsTests.cs
new file mode 100644
index 0000000000..19d37e93cc
--- /dev/null
+++ b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Driver/ConnectionSettingsTests.cs
@@ -0,0 +1,125 @@
+#region License
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.
+ */
+
+#endregion
+
+using System;
+using System.Net.Security;
+using System.Threading;
+using Gremlin.Net.Driver;
+using Xunit;
+
+namespace Gremlin.Net.UnitTest.Driver
+{
+    public class ConnectionSettingsTests
+    {
+        [Fact]
+        public void ShouldUseStandardizedDefaults()
+        {
+            var settings = new ConnectionSettings();
+
+            Assert.Equal(TimeSpan.FromSeconds(5), settings.ConnectTimeout);
+            Assert.Equal(TimeSpan.FromSeconds(180), settings.IdleTimeout);
+            Assert.Equal(128, settings.MaxConnections);
+            Assert.Equal(TimeSpan.FromSeconds(30), settings.KeepAliveTime);
+            Assert.Equal(64, settings.BatchSize);
+            Assert.Equal(Compression.Deflate, settings.Compression);
+            Assert.False(settings.SkipCertificateValidation);
+            Assert.Null(settings.Ssl);
+            Assert.Null(settings.Proxy);
+            Assert.Equal(0, settings.MaxResponseHeaderBytes);
+            Assert.Equal(Timeout.InfiniteTimeSpan, settings.ReadTimeout);
+        }
+
+        [Fact]
+        public void CompressionShouldDefaultToDeflate()
+        {
+            Assert.Equal(CompressionType.Deflate, new 
ConnectionSettings().Compression.Type);
+            Assert.True(new ConnectionSettings().Compression.Enabled);
+        }
+
+        [Fact]
+        public void CompressionShouldAcceptEnumValue()
+        {
+            var settings = new ConnectionSettings { Compression = 
CompressionType.Deflate };
+
+            Assert.Equal(Compression.Deflate, settings.Compression);
+        }
+
+        [Fact]
+        public void CompressionEqualityShouldWork()
+        {
+            Assert.Equal(Compression.Deflate, Compression.Deflate);
+            Assert.NotEqual(Compression.Deflate, Compression.None);
+            Assert.True(Compression.None == CompressionType.None);
+            Assert.True(Compression.Deflate != Compression.None);
+        }
+
+        [Fact]
+        public void ShouldAllowSettingSslOptions()
+        {
+            var ssl = new SslClientAuthenticationOptions { TargetHost = 
"example.com" };
+            var settings = new ConnectionSettings { Ssl = ssl };
+
+            Assert.Same(ssl, settings.Ssl);
+        }
+
+        [Fact]
+        public void MillisOptionsShouldSetTheTimeSpanProperties()
+        {
+            var settings = new ConnectionSettings
+            {
+                ConnectTimeoutMillis = 2000,
+                IdleTimeoutMillis = 60000,
+                KeepAliveTimeMillis = 15000,
+                ReadTimeoutMillis = 30000
+            };
+
+            Assert.Equal(TimeSpan.FromMilliseconds(2000), 
settings.ConnectTimeout);
+            Assert.Equal(TimeSpan.FromMilliseconds(60000), 
settings.IdleTimeout);
+            Assert.Equal(TimeSpan.FromMilliseconds(15000), 
settings.KeepAliveTime);
+            Assert.Equal(TimeSpan.FromMilliseconds(30000), 
settings.ReadTimeout);
+        }
+
+        [Fact]
+        public void MillisOptionsShouldReflectTheTimeSpanProperties()
+        {
+            var settings = new ConnectionSettings
+            {
+                ConnectTimeout = TimeSpan.FromSeconds(2),
+                ReadTimeout = TimeSpan.FromSeconds(30)
+            };
+
+            Assert.Equal(2000, settings.ConnectTimeoutMillis);
+            Assert.Equal(30000, settings.ReadTimeoutMillis);
+        }
+
+        [Fact]
+        public void ReadTimeoutMillisZeroShouldDisableTheReadTimeout()
+        {
+            var settings = new ConnectionSettings { ReadTimeoutMillis = 0 };
+
+            Assert.Equal(Timeout.InfiniteTimeSpan, settings.ReadTimeout);
+            Assert.Equal(0, settings.ReadTimeoutMillis);
+        }
+
+    }
+}
diff --git a/gremlin-dotnet/test/Gremlin.Net.UnitTest/Driver/ConnectionTests.cs 
b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Driver/ConnectionTests.cs
index 5601029363..7a3bee9e62 100644
--- a/gremlin-dotnet/test/Gremlin.Net.UnitTest/Driver/ConnectionTests.cs
+++ b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Driver/ConnectionTests.cs
@@ -141,7 +141,7 @@ namespace Gremlin.Net.UnitTest.Driver
         {
             var (httpClient, handler) = CreateMockHttpClient();
             var serializer = CreateMockSerializer();
-            var settings = new ConnectionSettings { EnableCompression = true };
+            var settings = new ConnectionSettings { Compression = 
Compression.Deflate };
             using var connection = new Connection(TestUri, serializer, 
settings, httpClient);
 
             await connection.SubmitAsync<object>(CreateTestRequest());
@@ -156,7 +156,7 @@ namespace Gremlin.Net.UnitTest.Driver
         {
             var (httpClient, handler) = CreateMockHttpClient();
             var serializer = CreateMockSerializer();
-            var settings = new ConnectionSettings { EnableCompression = false 
};
+            var settings = new ConnectionSettings { Compression = 
Compression.None };
             using var connection = new Connection(TestUri, serializer, 
settings, httpClient);
 
             await connection.SubmitAsync<object>(CreateTestRequest());
@@ -166,6 +166,21 @@ namespace Gremlin.Net.UnitTest.Driver
                 e => e.Value == "deflate");
         }
 
+        [Fact]
+        public async Task ShouldSetAcceptEncodingByDefault()
+        {
+            var (httpClient, handler) = CreateMockHttpClient();
+            var serializer = CreateMockSerializer();
+            var settings = new ConnectionSettings();
+            using var connection = new Connection(TestUri, serializer, 
settings, httpClient);
+
+            await connection.SubmitAsync<object>(CreateTestRequest());
+
+            Assert.NotNull(handler.CapturedRequest);
+            Assert.Contains(handler.CapturedRequest!.Headers.AcceptEncoding,
+                e => e.Value == "deflate");
+        }
+
         [Fact]
         public async Task ShouldSetUserAgentWhenEnabled()
         {
@@ -226,21 +241,24 @@ namespace Gremlin.Net.UnitTest.Driver
         [Fact]
         public async Task ShouldDecompressDeflateResponse()
         {
-            // Compress the minimal response bytes with deflate
+            // Compress the minimal response bytes the way the server does: 
java.util.zip.Deflater's
+            // default constructor emits a zlib-wrapped stream (RFC 1950: 
2-byte header + Adler-32),
+            // which corresponds to .NET's ZLibStream (NOT the raw RFC 1951 
DeflateStream). Using
+            // ZLibStream here exercises the real wire format and would catch 
a raw/zlib mismatch.
             var originalBytes = BuildMinimalResponseBytes();
             byte[] compressedBytes;
             using (var compressedStream = new MemoryStream())
             {
-                using (var deflateStream = new DeflateStream(compressedStream, 
CompressionMode.Compress, true))
+                using (var zlibStream = new ZLibStream(compressedStream, 
CompressionMode.Compress, true))
                 {
-                    deflateStream.Write(originalBytes, 0, 
originalBytes.Length);
+                    zlibStream.Write(originalBytes, 0, originalBytes.Length);
                 }
                 compressedBytes = compressedStream.ToArray();
             }
 
             var (httpClient, handler) = CreateMockHttpClient(compressedBytes, 
"deflate");
             var serializer = CreateMockSerializer();
-            var settings = new ConnectionSettings { EnableCompression = true };
+            var settings = new ConnectionSettings { Compression = 
Compression.Deflate };
             using var connection = new Connection(TestUri, serializer, 
settings, httpClient);
 
             // Should not throw — decompression should work
@@ -262,6 +280,110 @@ namespace Gremlin.Net.UnitTest.Driver
             connection.Dispose();
         }
 
+        [Fact]
+        public void ShouldNotMutateUserSslOptionsWhenSkippingCertValidation()
+        {
+            // The public constructor must NOT mutate the caller's 
SslClientAuthenticationOptions
+            // when SkipCertificateValidation is set. The options object is a 
reference type that
+            // may be shared across clients, so mutating it in place could 
silently disable
+            // validation on another client. Instead, the skip-cert callback 
must be installed on
+            // an internal clone, leaving the caller's object untouched.
+            var userSsl = new 
System.Net.Security.SslClientAuthenticationOptions
+            {
+                TargetHost = "example.com"
+            };
+            var settings = new ConnectionSettings
+            {
+                Ssl = userSsl,
+                SkipCertificateValidation = true
+            };
+
+            using var connection = new Connection(
+                TestUri, CreateMockSerializer(), settings);
+
+            // The caller's own options object must be left exactly as 
supplied: its
+            // RemoteCertificateValidationCallback must remain null and its 
other fields intact.
+            Assert.Equal("example.com", userSsl.TargetHost);
+            Assert.Null(userSsl.RemoteCertificateValidationCallback);
+            // settings.Ssl must still reference the very same object the 
caller provided.
+            Assert.Same(userSsl, settings.Ssl);
+        }
+
+        [Fact]
+        public void 
ShouldNotShareSkipCertCallbackAcrossClientsSharingSslOptions()
+        {
+            // Reusing one Ssl options object across two clients, only one of 
which skips cert
+            // validation, must not leak the accept-all callback onto the 
shared object (and thus
+            // onto the other client).
+            var sharedSsl = new 
System.Net.Security.SslClientAuthenticationOptions
+            {
+                TargetHost = "example.com"
+            };
+
+            var skipSettings = new ConnectionSettings
+            {
+                Ssl = sharedSsl,
+                SkipCertificateValidation = true
+            };
+            var strictSettings = new ConnectionSettings
+            {
+                Ssl = sharedSsl,
+                SkipCertificateValidation = false
+            };
+
+            using var skipConnection = new Connection(TestUri, 
CreateMockSerializer(), skipSettings);
+            using var strictConnection = new Connection(TestUri, 
CreateMockSerializer(), strictSettings);
+
+            // The shared object must never have had the accept-all callback 
written onto it.
+            Assert.Null(sharedSsl.RemoteCertificateValidationCallback);
+        }
+
+        [Fact]
+        public void ShouldConstructWithProxyAndMaxHeaderBytes()
+        {
+            var settings = new ConnectionSettings
+            {
+                Proxy = new System.Net.WebProxy("http://localhost:3128";),
+                MaxResponseHeaderBytes = 16384
+            };
+
+            // Should construct the handler without throwing.
+            using var connection = new Connection(
+                TestUri, CreateMockSerializer(), settings);
+        }
+
+        [Theory]
+        [InlineData(1, 1)]      // a single byte still needs one whole kilobyte
+        [InlineData(1023, 1)]   // just under 1 KB rounds up to 1
+        [InlineData(1024, 1)]   // exactly 1 KB stays 1 (no spurious round-up)
+        [InlineData(1025, 2)]   // one byte over a KB boundary rounds up to 2
+        [InlineData(8191, 8)]   // just under 8 KB rounds up to 8
+        [InlineData(8192, 8)]   // exactly 8 KB (the default) stays 8
+        [InlineData(8193, 9)]   // one byte over rounds up to 9
+        [InlineData(16384, 16)] // exactly 16 KB stays 16
+        public void ShouldRoundMaxResponseHeaderBytesUpToKilobytes(int bytes, 
int expectedKilobytes)
+        {
+            // SocketsHttpHandler.MaxResponseHeadersLength is expressed in 
kilobytes while the
+            // public option is in bytes. The conversion must round UP so the 
configured byte cap
+            // is never silently lowered (ceil(bytes / 1024)). This asserts 
the rounding math the
+            // public Connection constructor applies to the handler.
+            Assert.Equal(expectedKilobytes, 
Connection.MaxResponseHeaderBytesToKilobytes(bytes));
+        }
+
+        [Fact]
+        public void ShouldLeaveMaxResponseHeadersAtDefaultWhenBytesUnset()
+        {
+            // When MaxResponseHeaderBytes is 0 (the default / unset), the 
constructor must NOT
+            // touch the handler's MaxResponseHeadersLength, leaving the .NET 
default in place.
+            // The conversion is only applied for a positive byte cap, so 
constructing with the
+            // default must succeed without invoking the rounding path.
+            var settings = new ConnectionSettings();
+            Assert.Equal(0, settings.MaxResponseHeaderBytes);
+
+            // Should construct without throwing and without configuring the 
header cap.
+            using var connection = new Connection(TestUri, 
CreateMockSerializer(), settings);
+        }
+
         private static RequestMessage CreateTestRequest()
         {
             return RequestMessage.Build("g.V()").AddG("g").Create();
@@ -940,6 +1062,162 @@ namespace Gremlin.Net.UnitTest.Driver
             Assert.True(handler.WasCalled, "SendAsync should have been 
called");
         }
 
+        [Fact]
+        public async Task ShouldFillBatchSizeFromDefaultWhenUnset()
+        {
+            var (httpClient, handler) = CreateMockHttpClient();
+            var serializer = CreateMockSerializer();
+            var settings = new ConnectionSettings { BatchSize = 42 };
+            using var connection = new Connection(TestUri, serializer, 
settings, httpClient);
+
+            var request = CreateTestRequest();
+            await connection.SubmitAsync<object>(request);
+
+            // The caller-owned request must NOT be mutated by default-filling.
+            Assert.False(request.Fields.ContainsKey(Tokens.ArgsBatchSize));
+            // The outgoing wire payload must carry the connection-level 
default.
+            Assert.Equal(42, await ReadBatchSizeFromBodyAsync(handler));
+        }
+
+        [Fact]
+        public async Task ShouldNotOverrideExplicitBatchSize()
+        {
+            var (httpClient, handler) = CreateMockHttpClient();
+            var serializer = CreateMockSerializer();
+            var settings = new ConnectionSettings { BatchSize = 42 };
+            using var connection = new Connection(TestUri, serializer, 
settings, httpClient);
+
+            var request = 
RequestMessage.Build("g.V()").AddG("g").AddBatchSize(100).Create();
+            await connection.SubmitAsync<object>(request);
+
+            // A per-request explicit batchSize always wins, on the caller 
object and the wire.
+            Assert.Equal(100, request.Fields[Tokens.ArgsBatchSize]);
+            Assert.Equal(100, await ReadBatchSizeFromBodyAsync(handler));
+        }
+
+        [Fact]
+        public async Task ShouldUseDefaultBatchSizeOf64ByDefault()
+        {
+            var (httpClient, handler) = CreateMockHttpClient();
+            var serializer = CreateMockSerializer();
+            var settings = new ConnectionSettings();
+            using var connection = new Connection(TestUri, serializer, 
settings, httpClient);
+
+            var request = CreateTestRequest();
+            await connection.SubmitAsync<object>(request);
+
+            Assert.False(request.Fields.ContainsKey(Tokens.ArgsBatchSize));
+            Assert.Equal(64, await ReadBatchSizeFromBodyAsync(handler));
+        }
+
+        [Fact]
+        public async Task ShouldNotPersistDefaultBatchSizeAcrossResubmissions()
+        {
+            var (httpClient, handler) = CreateMockHttpClient();
+            var serializer = CreateMockSerializer();
+            var settings = new ConnectionSettings { BatchSize = 42 };
+            using var connection = new Connection(TestUri, serializer, 
settings, httpClient);
+
+            // Resubmitting the same message must not carry over a previously 
injected default.
+            var request = CreateTestRequest();
+            await connection.SubmitAsync<object>(request);
+            await connection.SubmitAsync<object>(request);
+
+            Assert.False(request.Fields.ContainsKey(Tokens.ArgsBatchSize));
+            Assert.Equal(42, await ReadBatchSizeFromBodyAsync(handler));
+        }
+
+        /// <summary>
+        ///     Reads the serialized JSON request body captured by the mock 
handler and returns
+        ///     the <c>batchSize</c> field value, or <c>null</c> when it is 
absent.
+        /// </summary>
+        private static async Task<int?> ReadBatchSizeFromBodyAsync(MockHandler 
handler)
+        {
+            Assert.NotNull(handler.CapturedRequest);
+            var bodyBytes = await 
handler.CapturedRequest!.Content!.ReadAsByteArrayAsync();
+            using var doc = System.Text.Json.JsonDocument.Parse(bodyBytes);
+            if (doc.RootElement.TryGetProperty(Tokens.ArgsBatchSize, out var 
batchSizeProp))
+            {
+                return batchSizeProp.GetInt32();
+            }
+            return null;
+        }
+
+        [Fact]
+        public async Task ShouldTimeOutSlowReadWhenReadTimeoutSet()
+        {
+            // A response stream that blocks indefinitely on read should 
trigger the
+            // per-read idle timeout once it is consumed during 
deserialization.
+            var blockingStream = new BlockingStream();
+            var handler = new StreamMockHandler(blockingStream);
+            var httpClient = new HttpClient(handler);
+            // The serializer actually reads from the stream so the read 
timeout can fire.
+            var serializer = CreateReadingSerializer();
+            var settings = new ConnectionSettings
+            {
+                ReadTimeout = TimeSpan.FromMilliseconds(100)
+            };
+            using var connection = new Connection(TestUri, serializer, 
settings, httpClient);
+
+            var result = await 
connection.SubmitAsync<object>(CreateTestRequest());
+
+            // The background streaming task surfaces the timeout as a faulted 
enumeration.
+            await Assert.ThrowsAnyAsync<Exception>(async () =>
+            {
+                await foreach (var _ in result) { }
+            });
+        }
+
+        private static IMessageSerializer CreateReadingSerializer(
+            string mimeType = SerializationTokens.GraphBinary4MimeType)
+        {
+            var serializer = Substitute.For<IMessageSerializer>();
+            serializer.MimeType.Returns(mimeType);
+            serializer.DeserializeMessageAsync(Arg.Any<Stream>(), 
Arg.Any<CancellationToken>())
+                .Returns(callInfo => ReadAllAsync((Stream)callInfo[0], 
(CancellationToken)callInfo[1]));
+            return serializer;
+        }
+
+        private static async IAsyncEnumerable<object> ReadAllAsync(Stream 
stream,
+            [System.Runtime.CompilerServices.EnumeratorCancellation] 
CancellationToken cancellationToken)
+        {
+            var buffer = new byte[16];
+            while (await stream.ReadAsync(buffer, 
cancellationToken).ConfigureAwait(false) > 0)
+            {
+                yield return new object();
+            }
+        }
+
+        /// <summary>
+        ///     A stream whose ReadAsync never completes until cancelled, used 
to exercise the
+        ///     per-read timeout.
+        /// </summary>
+        private sealed class BlockingStream : Stream
+        {
+            public override async ValueTask<int> ReadAsync(Memory<byte> buffer,
+                CancellationToken cancellationToken = default)
+            {
+                await Task.Delay(Timeout.Infinite, 
cancellationToken).ConfigureAwait(false);
+                return 0;
+            }
+
+            public override int Read(byte[] buffer, int offset, int count)
+            {
+                Thread.Sleep(Timeout.Infinite);
+                return 0;
+            }
+
+            public override bool CanRead => true;
+            public override bool CanSeek => false;
+            public override bool CanWrite => false;
+            public override long Length => throw new NotSupportedException();
+            public override long Position { get => 0; set { } }
+            public override void Flush() { }
+            public override long Seek(long offset, SeekOrigin origin) => throw 
new NotSupportedException();
+            public override void SetLength(long value) => throw new 
NotSupportedException();
+            public override void Write(byte[] buffer, int offset, int count) 
=> throw new NotSupportedException();
+        }
+
         /// <summary>
         ///     A test HttpMessageHandler that captures the request and 
returns a canned response.
         ///     The response uses ByteArrayContent but does NOT dispose the 
content stream
diff --git 
a/gremlin-dotnet/test/Gremlin.Net.UnitTest/Driver/GremlinClientTests.cs 
b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Driver/GremlinClientTests.cs
index 58442eb7d1..83518bc3b4 100644
--- a/gremlin-dotnet/test/Gremlin.Net.UnitTest/Driver/GremlinClientTests.cs
+++ b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Driver/GremlinClientTests.cs
@@ -51,7 +51,7 @@ namespace Gremlin.Net.UnitTest.Driver
         {
             var settings = new ConnectionSettings
             {
-                EnableCompression = true,
+                Compression = Compression.Deflate,
                 BulkResults = true
             };
             using var client = new GremlinClient(new GremlinServer(), 
connectionSettings: settings);
diff --git 
a/gremlin-dotnet/test/Gremlin.Net.UnitTest/Driver/GremlinServerTests.cs 
b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Driver/GremlinServerTests.cs
index add13d169f..35e6113b80 100644
--- a/gremlin-dotnet/test/Gremlin.Net.UnitTest/Driver/GremlinServerTests.cs
+++ b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Driver/GremlinServerTests.cs
@@ -21,6 +21,7 @@
 
 #endregion
 
+using System;
 using Gremlin.Net.Driver;
 using Xunit;
 
@@ -80,5 +81,140 @@ namespace Gremlin.Net.UnitTest.Driver
 
             Assert.Equal("/gremlin", gremlinServer.Uri.AbsolutePath);
         }
+
+        [Fact]
+        public void ShouldBuildFromHttpUrl()
+        {
+            var gremlinServer = 
GremlinServer.FromUrl("http://example.com:8182/gremlin";);
+
+            var uri = gremlinServer.Uri;
+
+            Assert.Equal("http", uri.Scheme);
+            Assert.Equal("example.com", uri.Host);
+            Assert.Equal(8182, uri.Port);
+            Assert.Equal("/gremlin", uri.AbsolutePath);
+        }
+
+        [Fact]
+        public void ShouldBuildFromHttpsUrl()
+        {
+            var gremlinServer = 
GremlinServer.FromUrl("https://example.com:8182/gremlin";);
+
+            var uri = gremlinServer.Uri;
+
+            Assert.Equal("https", uri.Scheme);
+            Assert.Equal("example.com", uri.Host);
+            Assert.Equal(8182, uri.Port);
+            Assert.Equal("/gremlin", uri.AbsolutePath);
+        }
+
+        [Fact]
+        public void ShouldBuildFromUrlWithCustomPath()
+        {
+            var gremlinServer = 
GremlinServer.FromUrl("https://example.com:8182/custom/path";);
+
+            var uri = gremlinServer.Uri;
+
+            Assert.Equal("/custom/path", uri.AbsolutePath);
+            Assert.Equal("https://example.com:8182/custom/path";, 
uri.AbsoluteUri);
+        }
+
+        [Theory]
+        [InlineData("http://example.com:8182/gremlin";, "example.com", 8182)]
+        [InlineData("https://1.2.3.4:5678/gremlin";, "1.2.3.4", 5678)]
+        public void ShouldExtractHostAndPortFromUrl(string url, string 
expectedHost, int expectedPort)
+        {
+            var gremlinServer = GremlinServer.FromUrl(url);
+
+            Assert.Equal(expectedHost, gremlinServer.Uri.Host);
+            Assert.Equal(expectedPort, gremlinServer.Uri.Port);
+        }
+
+        [Fact]
+        public void ShouldParseSecureUrlWithExplicitPortAndPath()
+        {
+            var uri = GremlinServer.FromUrl("https://h:1234/p";).Uri;
+
+            Assert.Equal("https", uri.Scheme);
+            Assert.Equal("h", uri.Host);
+            Assert.Equal(1234, uri.Port);
+            Assert.Equal("/p", uri.AbsolutePath);
+        }
+
+        [Fact]
+        public void ShouldParseInsecureUrlWithExplicitPortAndPath()
+        {
+            var uri = GremlinServer.FromUrl("http://h:1234/p";).Uri;
+
+            Assert.Equal("http", uri.Scheme);
+            Assert.Equal("h", uri.Host);
+            Assert.Equal(1234, uri.Port);
+            Assert.Equal("/p", uri.AbsolutePath);
+        }
+
+        [Fact]
+        public void ShouldDefaultPortWhenUrlOmitsPort()
+        {
+            var uri = GremlinServer.FromUrl("https://host/gremlin";).Uri;
+
+            Assert.Equal("https", uri.Scheme);
+            Assert.Equal("host", uri.Host);
+            Assert.Equal(8182, uri.Port);
+            Assert.Equal("/gremlin", uri.AbsolutePath);
+        }
+
+        [Fact]
+        public void ShouldDefaultPathWhenUrlOmitsPath()
+        {
+            var uri = GremlinServer.FromUrl("https://host:8182";).Uri;
+
+            Assert.Equal("https", uri.Scheme);
+            Assert.Equal("host", uri.Host);
+            Assert.Equal(8182, uri.Port);
+            Assert.Equal("/gremlin", uri.AbsolutePath);
+        }
+
+        [Fact]
+        public void ShouldDefaultPortAndPathWhenUrlOmitsBoth()
+        {
+            var uri = GremlinServer.FromUrl("https://host";).Uri;
+
+            Assert.Equal("https", uri.Scheme);
+            Assert.Equal("host", uri.Host);
+            Assert.Equal(8182, uri.Port);
+            Assert.Equal("/gremlin", uri.AbsolutePath);
+        }
+
+        [Fact]
+        public void ShouldRejectUnsupportedSchemeForUrl()
+        {
+            Assert.Throws<ArgumentException>(() => 
GremlinServer.FromUrl("ws://example.com:8182/gremlin"));
+        }
+
+        [Fact]
+        public void ShouldRejectNonAbsoluteUrl()
+        {
+            Assert.Throws<ArgumentException>(() => 
GremlinServer.FromUrl("example.com:8182/gremlin"));
+        }
+
+        [Fact]
+        public void ShouldRejectNullUrl()
+        {
+            Assert.Throws<ArgumentNullException>(() => 
GremlinServer.FromUrl(null!));
+        }
+
+        [Fact]
+        public void ShouldBuildFromUri()
+        {
+            var gremlinServer = new GremlinServer(new 
Uri("https://example.com:8182/gremlin";));
+
+            Assert.Equal("https://example.com:8182/gremlin";, 
gremlinServer.Uri.AbsoluteUri);
+        }
+
+        [Fact]
+        public void ShouldRejectUriWithUnsupportedScheme()
+        {
+            Assert.Throws<ArgumentException>(() => new GremlinServer(new 
Uri("ftp://example.com:8182/gremlin";)));
+        }
     }
 }

Reply via email to