IGNITE-24329 .NET: Fix LazyTransaction observable timestamp (#5129)

Record observable timestamp when the user starts the transaction.

(cherry picked from commit 6c4f36a8fa5e6236425f3ea4f9d855bafe496a7c)
diff --git a/modules/platforms/dotnet/Apache.Ignite.Tests/PartitionAwarenessTests.cs b/modules/platforms/dotnet/Apache.Ignite.Tests/PartitionAwarenessTests.cs
index 143733c..516016f 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Tests/PartitionAwarenessTests.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/PartitionAwarenessTests.cs
@@ -447,7 +447,7 @@
         node.ClearOps();
         node2?.ClearOps();
 
-        ITransaction? tx = withTx ? new LazyTransaction(default) : null;
+        ITransaction? tx = withTx ? new LazyTransaction(default, 0) : null;
 
         await action(tx);
 
diff --git a/modules/platforms/dotnet/Apache.Ignite.Tests/Transactions/TransactionsTests.cs b/modules/platforms/dotnet/Apache.Ignite.Tests/Transactions/TransactionsTests.cs
index 4f8e4ac..78beb7b 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Tests/Transactions/TransactionsTests.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/Transactions/TransactionsTests.cs
@@ -196,25 +196,25 @@
         }
 
         [Test]
-        public async Task TestReadOnlyTxSeesOldDataAfterUpdate()
+        public async Task TestReadOnlyTxSeesOldDataAfterUpdate([Values(true, false)] bool readBeforeUpdate)
         {
             var key = Random.Shared.NextInt64(1000, long.MaxValue);
             var keyPoco = new Poco { Key = key };
 
             await PocoView.UpsertAsync(null, new Poco { Key = key, Val = "11" });
 
-            await using var tx = await Client.Transactions.BeginAsync(new TransactionOptions { ReadOnly = true });
-            Assert.AreEqual("11", (await PocoView.GetAsync(tx, keyPoco)).Value.Val);
+            await using var roTx = await Client.Transactions.BeginAsync(new TransactionOptions { ReadOnly = true });
 
-            // Update data in a different tx.
-            await using (var tx2 = await Client.Transactions.BeginAsync())
+            if (readBeforeUpdate)
             {
-                await PocoView.UpsertAsync(null, new Poco { Key = key, Val = "22" });
-                await tx2.CommitAsync();
+                Assert.AreEqual("11", (await PocoView.GetAsync(roTx, keyPoco)).Value.Val);
             }
 
-            // Old tx sees old data.
-            Assert.AreEqual("11", (await PocoView.GetAsync(tx, keyPoco)).Value.Val);
+            // Update data in a different (implicit) tx.
+            await PocoView.UpsertAsync(transaction: null, new Poco { Key = key, Val = "22" });
+
+            // Old read-only tx sees old data.
+            Assert.AreEqual("11", (await PocoView.GetAsync(roTx, keyPoco)).Value.Val);
 
             // New tx sees new data
             await using var tx3 = await Client.Transactions.BeginAsync(new TransactionOptions { ReadOnly = true });
diff --git a/modules/platforms/dotnet/Apache.Ignite/Internal/IgniteClientInternal.cs b/modules/platforms/dotnet/Apache.Ignite/Internal/IgniteClientInternal.cs
index e04ca9c..28ca8eb 100644
--- a/modules/platforms/dotnet/Apache.Ignite/Internal/IgniteClientInternal.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Internal/IgniteClientInternal.cs
@@ -47,7 +47,7 @@
             var tables = new Tables(socket, sql);
 
             Tables = tables;
-            Transactions = new Transactions.Transactions();
+            Transactions = new Transactions.Transactions(socket);
             Compute = new Compute.Compute(socket, tables);
             Sql = sql;
         }
diff --git a/modules/platforms/dotnet/Apache.Ignite/Internal/Transactions/LazyTransaction.cs b/modules/platforms/dotnet/Apache.Ignite/Internal/Transactions/LazyTransaction.cs
index 933ad33..c72f36f 100644
--- a/modules/platforms/dotnet/Apache.Ignite/Internal/Transactions/LazyTransaction.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Internal/Transactions/LazyTransaction.cs
@@ -46,6 +46,8 @@
 
     private readonly TransactionOptions _options;
 
+    private readonly long _observableTimestamp;
+
     private int _state = StateOpen;
 
     private volatile Task<Transaction>? _tx;
@@ -54,7 +56,12 @@
     /// Initializes a new instance of the <see cref="LazyTransaction"/> class.
     /// </summary>
     /// <param name="options">Options.</param>
-    public LazyTransaction(TransactionOptions options) => _options = options;
+    /// <param name="observableTimestamp">Observable timestamp.</param>
+    public LazyTransaction(TransactionOptions options, long observableTimestamp)
+    {
+        _options = options;
+        _observableTimestamp = observableTimestamp;
+    }
 
     /// <inheritdoc/>
     public bool IsReadOnly => _options.ReadOnly;
@@ -170,14 +177,14 @@
                 return txTask;
             }
 
-            txTask = BeginAsync(socket, preferredNode);
+            txTask = BeginAsync(socket, preferredNode, _observableTimestamp);
             _tx = txTask;
 
             return txTask;
         }
     }
 
-    private async Task<Transaction> BeginAsync(ClientFailoverSocket failoverSocket, PreferredNode preferredNode)
+    private async Task<Transaction> BeginAsync(ClientFailoverSocket failoverSocket, PreferredNode preferredNode, long observableTimestamp)
     {
         using var writer = ProtoCommon.GetMessageWriter();
         Write();
@@ -198,7 +205,7 @@
             var w = writer.MessageWriter;
             w.Write(_options.ReadOnly);
             w.Write(_options.TimeoutMillis);
-            w.Write(failoverSocket.ObservableTimestamp);
+            w.Write(observableTimestamp);
         }
     }
 
diff --git a/modules/platforms/dotnet/Apache.Ignite/Internal/Transactions/Transactions.cs b/modules/platforms/dotnet/Apache.Ignite/Internal/Transactions/Transactions.cs
index 02fb3f1..f182e5c 100644
--- a/modules/platforms/dotnet/Apache.Ignite/Internal/Transactions/Transactions.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Internal/Transactions/Transactions.cs
@@ -27,10 +27,18 @@
 /// </summary>
 internal class Transactions : ITransactions
 {
+    private readonly ClientFailoverSocket _socket;
+
+    /// <summary>
+    /// Initializes a new instance of the <see cref="Transactions"/> class.
+    /// </summary>
+    /// <param name="socket">Socket.</param>
+    public Transactions(ClientFailoverSocket socket) => _socket = socket;
+
     /// <inheritdoc/>
     [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "Tx is returned.")]
     public ValueTask<ITransaction> BeginAsync(TransactionOptions options) =>
-        ValueTask.FromResult((ITransaction)new LazyTransaction(options));
+        ValueTask.FromResult<ITransaction>(new LazyTransaction(options, _socket.ObservableTimestamp));
 
     /// <inheritdoc />
     public override string ToString() => IgniteToStringBuilder.Build(GetType());