You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tinkerpop.apache.org by fl...@apache.org on 2021/10/14 09:16:37 UTC

[tinkerpop] branch TINKERPOP-2556 updated (f787d0f -> 8644fa4)

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

florianhockmann pushed a change to branch TINKERPOP-2556
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git.


 discard f787d0f  TINKERPOP-2556 Add tx() support for .NET
     new 8644fa4  TINKERPOP-2556 Add tx() support for .NET

This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version.  This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:

 * -- * -- B -- O -- O -- O   (f787d0f)
            \
             N -- N -- N   refs/heads/TINKERPOP-2556 (8644fa4)

You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.

Any revisions marked "omit" are not gone; other references still
refer to them.  Any revisions marked "discard" are gone forever.

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../Remote/RemoteTransactionTests.cs}                                 | 4 ++--
 gremlin-dotnet/test/pom.xml                                           | 4 ++--
 2 files changed, 4 insertions(+), 4 deletions(-)
 rename gremlin-dotnet/test/Gremlin.Net.UnitTest/{Driver/Remote/DriverRemoteTransactionTests.cs => Process/Remote/RemoteTransactionTests.cs} (94%)

[tinkerpop] 01/01: TINKERPOP-2556 Add tx() support for .NET

Posted by fl...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

florianhockmann pushed a commit to branch TINKERPOP-2556
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git

commit 8644fa4adec794cf5962f1ed437d4d9a93943311
Author: Florian Hockmann <fh...@florian-hockmann.de>
AuthorDate: Wed Oct 13 11:29:53 2021 +0200

    TINKERPOP-2556 Add tx() support for .NET
---
 .github/workflows/build-test.yml                   |   2 +-
 CHANGELOG.asciidoc                                 |   1 +
 docs/src/reference/gremlin-variants.asciidoc       |  12 ++
 docs/src/upgrade/release-3.5.x.asciidoc            |   8 ++
 .../Driver/Remote/DriverRemoteConnection.cs        |  27 +++-
 gremlin-dotnet/src/Gremlin.Net/Gremlin.Net.csproj  |   1 +
 .../Process/Remote/IRemoteConnection.cs            |  16 +++
 .../Process/Remote/RemoteTransaction.cs            |  84 +++++++++++++
 .../src/Gremlin.Net/Process/Traversal/Bytecode.cs  |   2 +-
 .../IRemoteConnection.cs => Traversal/GraphOp.cs}  |  29 +++--
 .../Process/Traversal/GraphTraversalSource.cs      |  37 +++++-
 .../Docs/Reference/GremlinVariantsTests.cs         |  56 ++++++---
 .../GraphTraversalSourceTests.cs                   |   4 +-
 .../DriverRemoteConnection/GraphTraversalTests.cs  |   2 +-
 .../GraphTraversalTransactionTests.cs              | 107 ++++++++++++++++
 .../Process/Remote/RemoteTransactionTests.cs       |  57 +++++++++
 gremlin-dotnet/test/pom.xml                        | 137 +++++++++++++++++++++
 17 files changed, 543 insertions(+), 39 deletions(-)

diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml
index 74e912e..7e25b0f 100644
--- a/.github/workflows/build-test.yml
+++ b/.github/workflows/build-test.yml
@@ -185,4 +185,4 @@ jobs:
           touch gremlin-dotnet/src/.glv
           touch gremlin-dotnet/test/.glv
           mvn clean install -pl -:gremlin-javascript,-:gremlin-python,-:gremlint -q -DskipTests -Dci
-          mvn verify -pl :gremlin-dotnet,:gremlin-dotnet-tests -P gremlin-dotnet
\ No newline at end of file
+          mvn verify -pl :gremlin-dotnet,:gremlin-dotnet-tests -P gremlin-dotnet -DincludeNeo4j
\ No newline at end of file
diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc
index 5f596b3..fb5a4cb 100644
--- a/CHANGELOG.asciidoc
+++ b/CHANGELOG.asciidoc
@@ -23,6 +23,7 @@ image::https://raw.githubusercontent.com/apache/tinkerpop/master/docs/static/ima
 [[release-3-5-2]]
 === TinkerPop 3.5.2 (Release Date: NOT OFFICIALLY RELEASED YET)
 
+* Added support for `g.Tx()` in .NET.
 * Added support for `with()` constant options to `io()`.
 * Fixed bug in the processing of the `io()` step when constructing a `Traversal` from the grammar.
 * Added the `ConnectedComponent` tokens required to properly process the `with()` of the `connectedComponent()` step.
diff --git a/docs/src/reference/gremlin-variants.asciidoc b/docs/src/reference/gremlin-variants.asciidoc
index b7d80d9..d107d07 100644
--- a/docs/src/reference/gremlin-variants.asciidoc
+++ b/docs/src/reference/gremlin-variants.asciidoc
@@ -1279,6 +1279,18 @@ NOTE: Many of the TraversalStrategy classes in Gremlin.Net are proxies to the re
 JVM-based Gremlin traversal machine. As such, their `Apply(ITraversal)` method does nothing. However, the strategy is
 encoded in the Gremlin.Net bytecode and transmitted to the Gremlin traversal machine for re-construction machine-side.
 
+[[gremlin-dotnet-transactions]]
+=== Transactions
+
+To get a full understanding of this section, it would be good to start by reading the <<transactions,Transactions>>
+section of this documentation, which discusses transactions in the general context of TinkerPop itself. This section
+builds on that content by demonstrating the transactional syntax for C#.
+
+[source,csharp]
+----
+include::../../../gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Docs/Reference/GremlinVariantsTests.cs[tags=transactions]
+----
+
 [[gremlin-dotnet-lambda]]
 === The Lambda Solution
 
diff --git a/docs/src/upgrade/release-3.5.x.asciidoc b/docs/src/upgrade/release-3.5.x.asciidoc
index 2507865..af1c9fb 100644
--- a/docs/src/upgrade/release-3.5.x.asciidoc
+++ b/docs/src/upgrade/release-3.5.x.asciidoc
@@ -30,6 +30,14 @@ complete list of all the modifications that are part of this release.
 
 === Upgrading for Users
 
+==== tx() in .NET
+
+After Javascript, .NET is now the second non-JVM variant of Gremlin to get support for
+link:https://tinkerpop.apache.org/docs/3.5.2/reference/#transactions[remote transactions]. An example of the `Tx()`
+syntax can be found in the .NET link:https://tinkerpop.apache.org/docs/3.5.2/reference/#gremlin-dotnet-transactions[Transaction Section].
+
+See: link:https://issues.apache.org/jira/browse/TINKERPOP-2556[TINKERPOP-2556]
+
 ==== datetime()
 
 Gremlin in native programming languages can all construct a native date and time object. In Java, that would probably
diff --git a/gremlin-dotnet/src/Gremlin.Net/Driver/Remote/DriverRemoteConnection.cs b/gremlin-dotnet/src/Gremlin.Net/Driver/Remote/DriverRemoteConnection.cs
index 006cf8a..2f32f77 100644
--- a/gremlin-dotnet/src/Gremlin.Net/Driver/Remote/DriverRemoteConnection.cs
+++ b/gremlin-dotnet/src/Gremlin.Net/Driver/Remote/DriverRemoteConnection.cs
@@ -25,7 +25,6 @@ using System;
 using System.Collections.Generic;
 using System.Threading.Tasks;
 using Gremlin.Net.Driver.Messages;
-using Gremlin.Net.Driver;
 using Gremlin.Net.Process.Remote;
 using Gremlin.Net.Process.Traversal;
 using Gremlin.Net.Process.Traversal.Strategy.Decoration;
@@ -49,6 +48,12 @@ namespace Gremlin.Net.Driver.Remote
                     {Tokens.ArgsEvalTimeout, "scriptEvaluationTimeout", Tokens.ArgsBatchSize, 
                      Tokens.RequestId, Tokens.ArgsUserAgent};
 
+        private readonly string _sessionId;
+        private string Processor => IsSessionBound ? Tokens.ProcessorSession : Tokens.ProcessorTraversal;
+
+        /// <inheritdoc />
+        public bool IsSessionBound => _sessionId != null;
+
         /// <summary>
         ///     Initializes a new <see cref="IRemoteConnection" /> using "g" as the default remote TraversalSource name.
         /// </summary>
@@ -91,6 +96,12 @@ namespace Gremlin.Net.Driver.Remote
             _traversalSource = traversalSource ?? throw new ArgumentNullException(nameof(traversalSource));
         }
 
+        private DriverRemoteConnection(IGremlinClient client, string traversalSource, Guid sessionId)
+            : this(client, traversalSource)
+        {
+            _sessionId = sessionId.ToString();
+        }
+
         /// <summary>
         ///     Submits <see cref="Bytecode" /> for evaluation to a remote Gremlin Server.
         /// </summary>
@@ -107,11 +118,16 @@ namespace Gremlin.Net.Driver.Remote
         {
             var requestMsg =
                 RequestMessage.Build(Tokens.OpsBytecode)
-                    .Processor(Tokens.ProcessorTraversal)
+                    .Processor(Processor)
                     .OverrideRequestId(requestid)
                     .AddArgument(Tokens.ArgsGremlin, bytecode)
                     .AddArgument(Tokens.ArgsAliases, new Dictionary<string, string> {{"g", _traversalSource}});
 
+            if (IsSessionBound)
+            {
+                requestMsg.AddArgument(Tokens.ArgsSession, _sessionId);
+            }
+
             var optionsStrategyInst = bytecode.SourceInstructions.Find(
                 s => s.OperatorName == "withStrategies" && s.Arguments[0] is OptionsStrategy);
             if (optionsStrategyInst != null)
@@ -130,6 +146,13 @@ namespace Gremlin.Net.Driver.Remote
         }
 
         /// <inheritdoc />
+        public RemoteTransaction Tx(GraphTraversalSource g)
+        {
+            var session = new DriverRemoteConnection(_client, _traversalSource, Guid.NewGuid());
+            return new RemoteTransaction(session, g);
+        }
+
+        /// <inheritdoc />
         public void Dispose()
         {
             _client?.Dispose();
diff --git a/gremlin-dotnet/src/Gremlin.Net/Gremlin.Net.csproj b/gremlin-dotnet/src/Gremlin.Net/Gremlin.Net.csproj
index 0f963bb..da81da3 100644
--- a/gremlin-dotnet/src/Gremlin.Net/Gremlin.Net.csproj
+++ b/gremlin-dotnet/src/Gremlin.Net/Gremlin.Net.csproj
@@ -21,6 +21,7 @@ limitations under the License.
     <TargetFramework>netstandard2.0</TargetFramework>
     <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
+    <LangVersion>8</LangVersion>
   </PropertyGroup>
 
   <PropertyGroup Label="Package">
diff --git a/gremlin-dotnet/src/Gremlin.Net/Process/Remote/IRemoteConnection.cs b/gremlin-dotnet/src/Gremlin.Net/Process/Remote/IRemoteConnection.cs
index 5393bcb..d251461 100644
--- a/gremlin-dotnet/src/Gremlin.Net/Process/Remote/IRemoteConnection.cs
+++ b/gremlin-dotnet/src/Gremlin.Net/Process/Remote/IRemoteConnection.cs
@@ -38,5 +38,21 @@ namespace Gremlin.Net.Process.Remote
         /// <param name="bytecode">The <see cref="Bytecode" /> to send.</param>
         /// <returns>The <see cref="ITraversal" /> with the results and optional side-effects.</returns>
         Task<ITraversal<S, E>> SubmitAsync<S, E>(Bytecode bytecode);
+
+        /// <summary>
+        ///     Creates a <see cref="RemoteTransaction" /> in the context of a <see cref="GraphTraversalSource" /> designed to work with
+        ///     remote semantics.
+        /// </summary>
+        /// <param name="graphTraversalSource">
+        ///     The <see cref="GraphTraversalSource" /> providing the context for the
+        ///     <see cref="RemoteTransaction" />.
+        /// </param>
+        /// <returns>The created <see cref="RemoteTransaction" />.</returns>
+        RemoteTransaction Tx(GraphTraversalSource graphTraversalSource);
+        
+        /// <summary>
+        ///     Determines if the connection is bound to a session.
+        /// </summary>
+        bool IsSessionBound { get; }
     }
 }
\ No newline at end of file
diff --git a/gremlin-dotnet/src/Gremlin.Net/Process/Remote/RemoteTransaction.cs b/gremlin-dotnet/src/Gremlin.Net/Process/Remote/RemoteTransaction.cs
new file mode 100644
index 0000000..394af09
--- /dev/null
+++ b/gremlin-dotnet/src/Gremlin.Net/Process/Remote/RemoteTransaction.cs
@@ -0,0 +1,84 @@
+#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.Threading.Tasks;
+using Gremlin.Net.Process.Traversal;
+
+namespace Gremlin.Net.Process.Remote
+{
+    /// <summary>
+    ///     A controller for a remote transaction that is constructed from <code>g.Tx()</code>. Calling <code>Begin()</code> on
+    ///     this object will produce a new <code>GraphTraversalSource</code> that is bound to a remote transaction over which
+    ///     multiple traversals may be executed in that context. Calling <code>CommitAsync()</code> or
+    ///     <code>RollbackAsync()</code> will then close the transaction and thus, the session. This feature only works with
+    ///     transaction enabled graphs.
+    /// </summary>
+    public class RemoteTransaction
+    {
+        private readonly IRemoteConnection _sessionBasedConnection;
+        private GraphTraversalSource _g;
+
+        /// <summary>
+        ///     Initializes a new instance of the <see cref="RemoteTransaction" /> class.
+        /// </summary>
+        /// <param name="connection">The session bound connection that will be used to control this transaction.</param>
+        /// <param name="g">The graph traversal source from which a session bound traversal source will be created.</param>
+        public RemoteTransaction(IRemoteConnection connection, GraphTraversalSource g)
+        {
+            _sessionBasedConnection = connection;
+            _g = g;
+        }
+
+        /// <summary>
+        ///     Spawns a <see cref="GraphTraversalSource" /> that is bound to a remote session which enables a transaction.
+        /// </summary>
+        /// <returns>A <see cref="GraphTraversalSource" /> bound to a remote session.</returns>
+        /// <exception cref="InvalidOperationException">Thrown if this transaction is already bound to a session.</exception>
+        public GraphTraversalSource Begin()
+        {
+            if (_g.IsSessionBound)
+            {
+                throw new InvalidOperationException("Transaction already started on this object");
+            }
+            _g = new GraphTraversalSource(_g.TraversalStrategies, _g.Bytecode, _sessionBasedConnection);
+            return _g;
+        }
+
+        /// <summary>
+        ///     Commits the transaction.
+        /// </summary>
+        public async Task CommitAsync()
+        {
+            await _sessionBasedConnection.SubmitAsync<object, object>(GraphOp.Commit).ConfigureAwait(false);
+        }
+
+        /// <summary>
+        ///     Rolls back the transaction.
+        /// </summary>
+        public async Task RollbackAsync()
+        {
+            await _sessionBasedConnection.SubmitAsync<object, object>(GraphOp.Rollback).ConfigureAwait(false);
+        }
+    }
+}
\ No newline at end of file
diff --git a/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/Bytecode.cs b/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/Bytecode.cs
index 7149e8b..cd53a1c 100644
--- a/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/Bytecode.cs
+++ b/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/Bytecode.cs
@@ -38,7 +38,7 @@ namespace Gremlin.Net.Process.Traversal
     /// </remarks>
     public class Bytecode
     {
-        private static readonly object[] EmptyArray = new object[0];
+        private static readonly object[] EmptyArray = Array.Empty<object>();
 
         /// <summary>
         ///     Initializes a new instance of the <see cref="Bytecode" /> class.
diff --git a/gremlin-dotnet/src/Gremlin.Net/Process/Remote/IRemoteConnection.cs b/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/GraphOp.cs
similarity index 55%
copy from gremlin-dotnet/src/Gremlin.Net/Process/Remote/IRemoteConnection.cs
copy to gremlin-dotnet/src/Gremlin.Net/Process/Traversal/GraphOp.cs
index 5393bcb..20b6975 100644
--- a/gremlin-dotnet/src/Gremlin.Net/Process/Remote/IRemoteConnection.cs
+++ b/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/GraphOp.cs
@@ -21,22 +21,29 @@
 
 #endregion
 
-using System.Threading.Tasks;
-using Gremlin.Net.Process.Traversal;
-
-namespace Gremlin.Net.Process.Remote
+namespace Gremlin.Net.Process.Traversal
 {
     /// <summary>
-    ///     A simple abstraction of a "connection" to a "server".
+    ///     A graph operation is a static <see cref="Bytecode" /> form that does not translate to a traversal but instead
+    ///     refers to a specific function to perform on a graph instance.
     /// </summary>
-    public interface IRemoteConnection
+    public static class GraphOp
     {
         /// <summary>
-        ///     Submits <see cref="ITraversal" /> <see cref="Bytecode" /> to a server and returns a
-        ///     <see cref="ITraversal" />.
+        ///     Commit a transaction.
         /// </summary>
-        /// <param name="bytecode">The <see cref="Bytecode" /> to send.</param>
-        /// <returns>The <see cref="ITraversal" /> with the results and optional side-effects.</returns>
-        Task<ITraversal<S, E>> SubmitAsync<S, E>(Bytecode bytecode);
+        public static Bytecode Commit { get; } = CreateGraphOp("tx", "commit");
+
+        /// <summary>
+        ///     Rollback a transaction.
+        /// </summary>
+        public static Bytecode Rollback { get; } = CreateGraphOp("tx", "rollback");
+
+        private static Bytecode CreateGraphOp(string name, object value)
+        {
+            var bytecode = new Bytecode();
+            bytecode.AddSource(name, value);
+            return bytecode;
+        }
     }
 }
\ No newline at end of file
diff --git a/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/GraphTraversalSource.cs b/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/GraphTraversalSource.cs
index c74e826..2ab9d01 100644
--- a/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/GraphTraversalSource.cs
+++ b/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/GraphTraversalSource.cs
@@ -24,6 +24,7 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
+using Gremlin.Net.Driver.Remote;
 using Gremlin.Net.Process.Remote;
 using Gremlin.Net.Process.Traversal.Strategy.Decoration;
 using Gremlin.Net.Structure;
@@ -38,6 +39,10 @@ namespace Gremlin.Net.Process.Traversal
     /// </summary>
     public class GraphTraversalSource
     {
+        private readonly IRemoteConnection _connection;
+
+        public bool IsSessionBound => _connection is { IsSessionBound: true };
+        
         /// <summary>
         ///     Gets or sets the traversal strategies associated with this graph traversal source.
         /// </summary>
@@ -71,6 +76,15 @@ namespace Gremlin.Net.Process.Traversal
             Bytecode = bytecode;
         }
 
+        public GraphTraversalSource(ICollection<ITraversalStrategy> traversalStrategies, Bytecode bytecode,
+            IRemoteConnection connection)
+            : this(traversalStrategies.Where(strategy => strategy.GetType() != typeof(RemoteStrategy)).ToList(),
+                bytecode)
+        {
+            _connection = connection;
+            TraversalStrategies.Add(new RemoteStrategy(connection));
+        }
+
         public GraphTraversalSource With(string key)
         {
             return With(key, true);
@@ -243,12 +257,24 @@ namespace Gremlin.Net.Process.Traversal
         ///     <see cref="GraphTraversal{SType, EType}" />.
         /// </param>
         /// <returns>A <see cref="GraphTraversalSource" /> configured to use the provided <see cref="IRemoteConnection" />.</returns>
-        public GraphTraversalSource WithRemote(IRemoteConnection remoteConnection)
+        public GraphTraversalSource WithRemote(IRemoteConnection remoteConnection) =>
+            new GraphTraversalSource(new List<ITraversalStrategy>(TraversalStrategies),
+                new Bytecode(Bytecode), remoteConnection);
+
+        /// <summary>
+        ///     Spawns a new <see cref="RemoteTransaction"/> object that can then start and stop a transaction.
+        /// </summary>
+        /// <returns>The spawned transaction.</returns>
+        /// <exception cref="InvalidOperationException">Thrown if this traversal source is already bound to a session.</exception>
+        public RemoteTransaction Tx()
         {
-            var source = new GraphTraversalSource(new List<ITraversalStrategy>(TraversalStrategies),
-                new Bytecode(Bytecode));
-            source.TraversalStrategies.Add(new RemoteStrategy(remoteConnection));
-            return source;
+            // you can't do g.tx().begin().tx() - no child transactions
+            if (IsSessionBound)
+            {
+                throw new InvalidOperationException(
+                    "This GraphTraversalSource is already bound to a transaction - child transactions are not supported");
+            }
+            return _connection.Tx(this);
         }
 
         /// <summary>
@@ -377,7 +403,6 @@ namespace Gremlin.Net.Process.Traversal
                 traversal.Bytecode.AddStep("io", file);
             return traversal;
         }
-
     }
     
 #pragma warning restore 1591
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 3eda74d..fb8479f 100644
--- a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Docs/Reference/GremlinVariantsTests.cs
+++ b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Docs/Reference/GremlinVariantsTests.cs
@@ -21,6 +21,7 @@
 
 #endregion
 
+using System;
 using System.Threading.Tasks;
 using Gremlin.Net.Driver;
 using Gremlin.Net.Driver.Messages;
@@ -107,11 +108,10 @@ edgeValueMaps = g.V().OutE().ValueMap<object, object>().With(WithOptions.Tokens)
         {
 // tag::submittingScripts[]
 var gremlinServer = new GremlinServer("localhost", 8182);
-using (var gremlinClient = new GremlinClient(gremlinServer))
-{
-    var response =
-        await gremlinClient.SubmitWithSingleResultAsync<string>("g.V().has('person','name','marko')");
-}
+using var gremlinClient = new GremlinClient(gremlinServer);
+
+var response =
+    await gremlinClient.SubmitWithSingleResultAsync<string>("g.V().has('person','name','marko')");
 // end::submittingScripts[]
         }
         
@@ -120,15 +120,14 @@ using (var gremlinClient = new GremlinClient(gremlinServer))
         {
 // tag::submittingScriptsWithTimeout[]
 var gremlinServer = new GremlinServer("localhost", 8182);
-using (var gremlinClient = new GremlinClient(gremlinServer))
-{
-    var response =
-        await gremlinClient.SubmitWithSingleResultAsync<string>(
-            RequestMessage.Build(Tokens.OpsEval).
-                AddArgument(Tokens.ArgsGremlin, "g.V().count()").
-                AddArgument(Tokens.ArgsEvalTimeout, 500).
-                Create());
-}
+using var gremlinClient = new GremlinClient(gremlinServer);
+
+var response =
+    await gremlinClient.SubmitWithSingleResultAsync<string>(
+        RequestMessage.Build(Tokens.OpsEval).
+            AddArgument(Tokens.ArgsGremlin, "g.V().count()").
+            AddArgument(Tokens.ArgsEvalTimeout, 500).
+            Create());
 // end::submittingScriptsWithTimeout[]
         }
 
@@ -141,5 +140,34 @@ var password = "password";
 var gremlinServer = new GremlinServer("localhost", 8182, true, username, password);
 // end::submittingScriptsWithAuthentication[]
         }
+        
+        [Fact(Skip = "No Server under localhost")]
+        public async Task TransactionsTest()
+        {
+// tag::transactions[]
+using var gremlinClient = new GremlinClient(new GremlinServer("localhost", 8182));
+var g = Traversal().WithRemote(new DriverRemoteConnection(gremlinClient));
+var tx = g.Tx();    // create a transaction
+
+// spawn a new GraphTraversalSource binding all traversals established from it to tx
+var gtx = tx.Begin();
+
+// execute traversals using gtx occur within the scope of the transaction held by tx. the
+// tx is closed after calls to CommitAsync or RollbackAsync and cannot be re-used. simply spawn a
+// new Transaction from g.Tx() to create a new one as needed. the g context remains
+// accessible through all this as a sessionless connection.
+try
+{
+    await gtx.AddV("person").Property("name", "jorge").Promise(t => t.Iterate());
+    await gtx.AddV("person").Property("name", "josh").Promise(t => t.Iterate());
+    
+    await tx.CommitAsync();
+}
+catch (Exception)
+{
+    await tx.RollbackAsync();
+}
+// end::transactions[]
+        }
     }
 }
\ No newline at end of file
diff --git a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Process/Traversal/DriverRemoteConnection/GraphTraversalSourceTests.cs b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Process/Traversal/DriverRemoteConnection/GraphTraversalSourceTests.cs
index 1a35679..713394f 100644
--- a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Process/Traversal/DriverRemoteConnection/GraphTraversalSourceTests.cs
+++ b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Process/Traversal/DriverRemoteConnection/GraphTraversalSourceTests.cs
@@ -1,4 +1,4 @@
-#region License
+#region License
 
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
@@ -21,10 +21,8 @@
 
 #endregion
 
-using System;
 using System.Collections.Generic;
 using Gremlin.Net.Process.Traversal;
-using Gremlin.Net.Process.Traversal.Strategy.Verification;
 using Xunit;
 
 namespace Gremlin.Net.IntegrationTest.Process.Traversal.DriverRemoteConnection
diff --git a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Process/Traversal/DriverRemoteConnection/GraphTraversalTests.cs b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Process/Traversal/DriverRemoteConnection/GraphTraversalTests.cs
index 89597a3..b0347bc 100644
--- a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Process/Traversal/DriverRemoteConnection/GraphTraversalTests.cs
+++ b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Process/Traversal/DriverRemoteConnection/GraphTraversalTests.cs
@@ -247,4 +247,4 @@ namespace Gremlin.Net.IntegrationTest.Process.Traversal.DriverRemoteConnection
             Assert.Equal(6, count);
         }
     }
-}
+}
\ No newline at end of file
diff --git a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Process/Traversal/DriverRemoteConnection/GraphTraversalTransactionTests.cs b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Process/Traversal/DriverRemoteConnection/GraphTraversalTransactionTests.cs
new file mode 100644
index 0000000..257f20b
--- /dev/null
+++ b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Process/Traversal/DriverRemoteConnection/GraphTraversalTransactionTests.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;
+using System.Threading.Tasks;
+using Gremlin.Net.Process.Remote;
+using Gremlin.Net.Process.Traversal;
+using Xunit;
+
+namespace Gremlin.Net.IntegrationTest.Process.Traversal.DriverRemoteConnection
+{
+    public class GraphTraversalTransactionTests : IDisposable
+    {
+        private readonly IRemoteConnection _connection = new RemoteConnectionFactory().CreateRemoteConnection("gtx");
+
+        [IgnoreIfTransactionsNotSupportedFact]
+        public async Task ShouldSupportRemoteTransactionsCommit()
+        {
+            var g = AnonymousTraversalSource.Traversal().WithRemote(_connection);
+            var tx = g.Tx();
+            var gtx = tx.Begin();
+            await gtx.AddV("person").Property("name", "jorge").Promise(t => t.Iterate()).ConfigureAwait(false);
+            await gtx.AddV("person").Property("name", "josh").Promise(t => t.Iterate()).ConfigureAwait(false);
+            
+            // Assert within the transaction
+            var count = await gtx.V().Count().Promise(t => t.Next()).ConfigureAwait(false);
+            Assert.Equal(2, count);
+            
+            // Vertices should not be visible in a different transaction before commiting
+            count = await g.V().Count().Promise(t => t.Next()).ConfigureAwait(false);
+            Assert.Equal(0, count);
+            
+            // Now commit changes to test outside of the transaction
+            await tx.CommitAsync().ConfigureAwait(false);
+
+            count = await g.V().Count().Promise(t => t.Next()).ConfigureAwait(false);
+            Assert.Equal(2, count);
+        }
+        
+        [IgnoreIfTransactionsNotSupportedFact]
+        public async Task ShouldSupportRemoteTransactionsRollback()
+        {
+            var g = AnonymousTraversalSource.Traversal().WithRemote(_connection);
+            var tx = g.Tx();
+            var gtx = tx.Begin();
+            await gtx.AddV("person").Property("name", "jorge").Promise(t => t.Iterate()).ConfigureAwait(false);
+            await gtx.AddV("person").Property("name", "josh").Promise(t => t.Iterate()).ConfigureAwait(false);
+            
+            // Assert within the transaction
+            var count = await gtx.V().Count().Promise(t => t.Next()).ConfigureAwait(false);
+            Assert.Equal(2, count);
+            
+            // Now rollback changes to test outside of the transaction
+            await tx.RollbackAsync().ConfigureAwait(false);
+
+            count = await g.V().Count().Promise(t => t.Next()).ConfigureAwait(false);
+            Assert.Equal(0, count);
+            
+            g.V().Count().Next();
+        }
+
+        public void Dispose()
+        {
+            EmptyGraph();
+        }
+
+        private void EmptyGraph()
+        {
+            var g = AnonymousTraversalSource.Traversal().WithRemote(_connection);
+            g.V().Drop().Iterate();
+        }
+    }
+
+    public sealed class IgnoreIfTransactionsNotSupportedFact : FactAttribute
+    {
+        public IgnoreIfTransactionsNotSupportedFact()
+        {
+            if (!TransactionsSupported)
+            {
+                Skip = "Transactions not supported";
+            }
+        }
+
+        private static bool TransactionsSupported =>
+            Convert.ToBoolean(Environment.GetEnvironmentVariable("TEST_TRANSACTIONS"));
+    }
+}
\ No newline at end of file
diff --git a/gremlin-dotnet/test/Gremlin.Net.UnitTest/Process/Remote/RemoteTransactionTests.cs b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Process/Remote/RemoteTransactionTests.cs
new file mode 100644
index 0000000..284566c
--- /dev/null
+++ b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Process/Remote/RemoteTransactionTests.cs
@@ -0,0 +1,57 @@
+#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 Gremlin.Net.Driver;
+using Gremlin.Net.Driver.Remote;
+using Gremlin.Net.Process.Traversal;
+using Moq;
+using Xunit;
+
+namespace Gremlin.Net.UnitTest.Process.Remote
+{
+    public class RemoteTransactionTests
+    {
+        [Fact]
+        public void ShouldNotAllowBeginMoreThanOnce()
+        {
+            var g = AnonymousTraversalSource.Traversal()
+                .WithRemote(new DriverRemoteConnection(Mock.Of<IGremlinClient>()));
+            var tx = g.Tx();
+            tx.Begin();
+
+            Assert.Throws<InvalidOperationException>(() => tx.Begin());
+        }
+        
+        [Fact]
+        public void ShouldNotSupportChildTransactions()
+        {
+            var g = AnonymousTraversalSource.Traversal()
+                .WithRemote(new DriverRemoteConnection(Mock.Of<IGremlinClient>()));
+            var tx = g.Tx();
+            
+            var gtx = tx.Begin();
+            Assert.Throws<InvalidOperationException>(() => gtx.Tx());
+        }
+    }
+}
\ No newline at end of file
diff --git a/gremlin-dotnet/test/pom.xml b/gremlin-dotnet/test/pom.xml
index d110391..1870207 100644
--- a/gremlin-dotnet/test/pom.xml
+++ b/gremlin-dotnet/test/pom.xml
@@ -96,6 +96,16 @@ limitations under the License.
                         <extensions>true</extensions>
                         <configuration>
                             <skip>${skipTests}</skip>
+                            <!--
+                            transaction testing is disabled unless the -DincludeNeo4j flag enables the include-neo4j
+                            maven profile which is a standard profile we use to add neo4j to testing explicitly - for
+                            dotnet we set this TEST_TRANSACTIONS environment variable that can be accessed in tests to
+                            determine if we skip transaction oriented tests or not. without neo4j we can't test Tx()
+                            so this is disabled by default and enabled in the include-neo4j profile below
+                            -->
+                            <environment>
+                                <TEST_TRANSACTIONS>false</TEST_TRANSACTIONS>
+                            </environment>
                         </configuration>
                     </plugin>
                     <plugin>
@@ -113,6 +123,11 @@ limitations under the License.
                                 <version>${project.version}</version>
                             </dependency>
                             <dependency>
+                                <groupId>org.apache.tinkerpop</groupId>
+                                <artifactId>neo4j-gremlin</artifactId>
+                                <version>${project.version}</version>
+                            </dependency>
+                            <dependency>
                                 <groupId>log4j</groupId>
                                 <artifactId>log4j</artifactId>
                                 <version>${log4j.version}</version>
@@ -206,5 +221,127 @@ limitations under the License.
                 </plugins>
             </build>
         </profile>
+        <!--
+          This profile will include neo4j for purposes of transactional testing within Gremlin Server.
+          Tests that require neo4j specifically will be "ignored" if this profile is not turned on.
+        -->
+        <profile>
+            <id>include-neo4j</id>
+            <activation>
+                <activeByDefault>false</activeByDefault>
+                <property>
+                    <name>includeNeo4j</name>
+                </property>
+            </activation>
+            <properties>
+                <packaging.type>dotnet-integration-test</packaging.type>
+            </properties>
+            <build>
+                <plugins>
+                    <!-- with neo4j present we can enable transaction testing -->
+                    <plugin>
+                        <groupId>org.eobjects.build</groupId>
+                        <artifactId>dotnet-maven-plugin</artifactId>
+                        <configuration>
+                            <environment>
+                                <TEST_TRANSACTIONS>true</TEST_TRANSACTIONS>
+                            </environment>
+                        </configuration>
+                    </plugin>
+                    <plugin>
+                        <groupId>org.codehaus.gmavenplus</groupId>
+                        <artifactId>gmavenplus-plugin</artifactId>
+                        <dependencies>
+                            <dependency>
+                                <groupId>org.neo4j</groupId>
+                                <artifactId>neo4j-tinkerpop-api-impl</artifactId>
+                                <version>0.9-3.4.0</version>
+                                <exclusions>
+                                    <exclusion>
+                                        <groupId>org.neo4j</groupId>
+                                        <artifactId>neo4j-kernel</artifactId>
+                                    </exclusion>
+                                    <exclusion>
+                                        <groupId>org.apache.commons</groupId>
+                                        <artifactId>commons-lang3</artifactId>
+                                    </exclusion>
+                                    <exclusion>
+                                        <groupId>org.apache.commons</groupId>
+                                        <artifactId>commons-text</artifactId>
+                                    </exclusion>
+                                    <exclusion>
+                                        <groupId>com.github.ben-manes.caffeine</groupId>
+                                        <artifactId>caffeine</artifactId>
+                                    </exclusion>
+                                    <exclusion>
+                                        <groupId>org.scala-lang</groupId>
+                                        <artifactId>scala-library</artifactId>
+                                    </exclusion>
+                                    <exclusion>
+                                        <groupId>org.scala-lang</groupId>
+                                        <artifactId>scala-reflect</artifactId>
+                                    </exclusion>
+                                    <exclusion>
+                                        <groupId>org.slf4j</groupId>
+                                        <artifactId>slf4j-api</artifactId>
+                                    </exclusion>
+                                    <exclusion>
+                                        <groupId>org.slf4j</groupId>
+                                        <artifactId>slf4j-nop</artifactId>
+                                    </exclusion>
+                                    <exclusion>
+                                        <groupId>org.apache.lucene</groupId>
+                                        <artifactId>lucene-core</artifactId>
+                                    </exclusion>
+                                    <exclusion>
+                                        <groupId>io.dropwizard.metrics</groupId>
+                                        <artifactId>metrics-core</artifactId>
+                                    </exclusion>
+                                    <exclusion>
+                                        <groupId>io.netty</groupId>
+                                        <artifactId>netty-all</artifactId>
+                                    </exclusion>
+                                    <exclusion>
+                                        <groupId>org.ow2.asm</groupId>
+                                        <artifactId>asm</artifactId>
+                                    </exclusion>
+                                </exclusions>
+                            </dependency>
+                            <dependency>
+                                <groupId>org.scala-lang</groupId>
+                                <artifactId>scala-library</artifactId>
+                                <version>2.11.8</version>
+                            </dependency>
+                            <dependency>
+                                <groupId>org.scala-lang</groupId>
+                                <artifactId>scala-reflect</artifactId>
+                                <version>2.11.8</version>
+                            </dependency>
+                            <dependency>
+                                <groupId>org.apache.lucene</groupId>
+                                <artifactId>lucene-core</artifactId>
+                                <version>5.5.0</version>
+                            </dependency>
+                            <dependency>
+                                <groupId>io.dropwizard.metrics</groupId>
+                                <artifactId>metrics-core</artifactId>
+                                <version>4.0.2</version>
+                            </dependency>
+                            <dependency>
+                                <groupId>org.neo4j</groupId>
+                                <artifactId>neo4j-kernel</artifactId>
+                                <version>3.4.11</version>
+                                <exclusions>
+                                    <exclusion>
+                                        <groupId>io.netty</groupId>
+                                        <artifactId>netty-all</artifactId>
+                                    </exclusion>
+                                </exclusions>
+                            </dependency>
+                        </dependencies>
+                    </plugin>
+                </plugins>
+            </build>
+        </profile>
     </profiles>
 </project>
\ No newline at end of file