Skip to content
Open
Show file tree
Hide file tree
Changes from 21 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
804 changes: 804 additions & 0 deletions specifications/sessions/tests/snapshot-sessions.json

Large diffs are not rendered by default.

410 changes: 409 additions & 1 deletion specifications/sessions/tests/snapshot-sessions.yml

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions src/MongoDB.Driver/ClientSessionHandle.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ public IServerSession ServerSession
}
}

public BsonTimestamp SnapshotTime => _coreSession.SnapshotTime;

/// <inheritdoc />
public ICoreSessionHandle WrappedCoreSession => _coreSession;

Expand Down
10 changes: 9 additions & 1 deletion src/MongoDB.Driver/ClientSessionOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
*/

using System;
using MongoDB.Bson;
using MongoDB.Driver.Core.Bindings;

namespace MongoDB.Driver
Expand Down Expand Up @@ -46,6 +47,12 @@ public class ClientSessionOptions
/// </value>
public bool Snapshot { get; set;}

/// <summary>
/// Gets or sets the snapshot time. If set, Snapshot must be true.
/// <value> The snapshot time </value>
/// </summary>
public BsonTimestamp SnapshotTime { get; set; }

// internal methods
internal CoreSessionOptions ToCore(bool isImplicit = false)
{
Expand All @@ -55,7 +62,8 @@ internal CoreSessionOptions ToCore(bool isImplicit = false)
isCausallyConsistent: isCausallyConsistent,
isImplicit: isImplicit,
isSnapshot: Snapshot,
defaultTransactionOptions: DefaultTransactionOptions);
defaultTransactionOptions: DefaultTransactionOptions,
snapshotTime: SnapshotTime);
}
}
}
1 change: 1 addition & 0 deletions src/MongoDB.Driver/Core/Bindings/CoreSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ private CoreSession(
{
_cluster = Ensure.IsNotNull(cluster, nameof(cluster));
_options = Ensure.IsNotNull(options, nameof(options));
_snapshotTime = options.SnapshotTime;
}

// public properties
Expand Down
32 changes: 31 additions & 1 deletion src/MongoDB.Driver/Core/Bindings/CoreSessionOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
* limitations under the License.
*/

using MongoDB.Bson;

namespace MongoDB.Driver.Core.Bindings
{
/// <summary>
Expand All @@ -25,6 +27,7 @@ public class CoreSessionOptions
private readonly bool _isCausallyConsistent;
private readonly bool _isImplicit;
private readonly bool _isSnapshot;
private readonly BsonTimestamp _snapshotTime;

// constructors
/// <summary>
Expand All @@ -34,16 +37,35 @@ public class CoreSessionOptions
/// <param name="isImplicit">if set to <c>true</c> this session is an implicit session.</param>
/// <param name="isSnapshot">if set to <c>true</c> this session is a snapshot session.</param>
/// <param name="defaultTransactionOptions">The default transaction options.</param>
/// <param name="snapshotTime">The snapshot time. If this is set, isSnapshot must be true.</param>
public CoreSessionOptions(
bool isCausallyConsistent = false,
bool isImplicit = false,
TransactionOptions defaultTransactionOptions = null,
bool isSnapshot = false)
bool isSnapshot = false,
BsonTimestamp snapshotTime = null)
{
_isCausallyConsistent = isCausallyConsistent;
_isImplicit = isImplicit;
_isSnapshot = isSnapshot;
_defaultTransactionOptions = defaultTransactionOptions;
_snapshotTime = snapshotTime;
}

/// <summary>
/// Initializes a new instance of the <see cref="CoreSessionOptions" /> class.
/// </summary>
/// <param name="isCausallyConsistent">if set to <c>true</c> this session is causally consistent]</param>
/// <param name="isImplicit">if set to <c>true</c> this session is an implicit session.</param>
/// <param name="isSnapshot">if set to <c>true</c> this session is a snapshot session.</param>
/// <param name="defaultTransactionOptions">The default transaction options.</param>
public CoreSessionOptions(
bool isCausallyConsistent,
bool isImplicit,
TransactionOptions defaultTransactionOptions,
bool isSnapshot)
: this(isCausallyConsistent, isImplicit, defaultTransactionOptions, isSnapshot, null)
{
}

// public properties
Expand Down Expand Up @@ -78,5 +100,13 @@ public CoreSessionOptions(
/// <c>true</c> if this session is a snapshot session; otherwise, <c>false</c>.
/// </value>
public bool IsSnapshot => _isSnapshot;

/// <summary>
/// Gets the snapshot time for snapshot sessions.
/// </summary>
/// <value>
/// The snapshot time as a <see cref="BsonTimestamp"/>, or <c>null</c> if not set.
/// </value>
public BsonTimestamp SnapshotTime => _snapshotTime;
}
}
24 changes: 23 additions & 1 deletion src/MongoDB.Driver/IClientSessionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,35 @@
* limitations under the License.
*/

using System;
using System.Threading;
using System.Threading.Tasks;
using MongoDB.Bson;

namespace MongoDB.Driver
{
/// <summary>
/// Extension methods for <see cref="IClientSession"/>.
/// </summary>
public static class IClientSessionExtensions
{
/// <summary>
/// Gets the snapshot time for a snapshot session.
/// </summary>
/// <param name="session">The client session handle.</param>
/// <returns>The snapshot time as a <see cref="BsonTimestamp"/>.</returns>
/// <exception cref="InvalidOperationException">Thrown when the session is not a snapshot session.</exception>
public static BsonTimestamp GetSnapshotTime(this IClientSession session)
{
var clientSessionHandle = (ClientSessionHandle)session;
return clientSessionHandle.WrappedCoreSession.IsSnapshot ?
clientSessionHandle.SnapshotTime
: throw new InvalidOperationException("Cannot retrieve snapshot time from a non-snapshot session.");
Comment on lines +34 to +39
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This public extension method hard-casts IClientSession to ClientSessionHandle, which can surface an InvalidCastException for callers with a different IClientSession implementation (or proxies/mocks). Since the PR description indicates this should be an extension on IClientSessionHandle, prefer changing the signature to GetSnapshotTime(this IClientSessionHandle session) and avoid the concrete cast; alternatively use pattern matching and throw a more intentional exception type/message when the session isn't a driver session handle.

Suggested change
public static BsonTimestamp GetSnapshotTime(this IClientSession session)
{
var clientSessionHandle = (ClientSessionHandle)session;
return clientSessionHandle.WrappedCoreSession.IsSnapshot ?
clientSessionHandle.SnapshotTime
: throw new InvalidOperationException("Cannot retrieve snapshot time from a non-snapshot session.");
/// <exception cref="NotSupportedException">Thrown when the session is not a <see cref="ClientSessionHandle"/>.</exception>
public static BsonTimestamp GetSnapshotTime(this IClientSession session)
{
if (session is ClientSessionHandle clientSessionHandle)
{
return clientSessionHandle.WrappedCoreSession.IsSnapshot
? clientSessionHandle.SnapshotTime
: throw new InvalidOperationException("Cannot retrieve snapshot time from a non-snapshot session.");
}
throw new NotSupportedException("GetSnapshotTime is only supported for sessions of type ClientSessionHandle.");

Copilot uses AI. Check for mistakes.
}
}

// TODO: CSOT: Make it public when CSOT will be ready for GA
internal static class IClientSessionExtensions
internal static class IInternalClientSessionExtensions
{
// TODO: Merge these extension methods in IClientSession interface on major release
public static void AbortTransaction(this IClientSession session, AbortTransactionOptions options, CancellationToken cancellationToken = default)
Expand Down
12 changes: 10 additions & 2 deletions src/MongoDB.Driver/MongoClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -622,9 +622,17 @@ private RenderArgs<BsonDocument> GetRenderArgs()

private IClientSessionHandle StartSession(ClientSessionOptions options)
{
if (options != null && options.Snapshot && options.CausalConsistency == true)
if (options != null)
{
throw new NotSupportedException("Combining both causal consistency and snapshot options is not supported.");
if (options.SnapshotTime != null && !options.Snapshot)
{
throw new NotSupportedException("Specifying a snapshot time requires snapshot to be true.");
}

if (options.Snapshot && options.CausalConsistency == true)
{
throw new NotSupportedException("Combining both causal consistency and snapshot options is not supported.");
}
}

options ??= new ClientSessionOptions();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,44 @@ public void Ensure_cluster_times_are_not_gossiped_on_SDAM_commands()
commandStartedEvents[0].Command["$clusterTime"].Should().Be(clusterTime);
}

// https://github.com/mongodb/specifications/blob/192976b194afdb1f458cbba2530c73de6b2c700f/source/sessions/tests/README.md#21-having-snapshottime-set-and-snapshot-set-to-false-is-not-allowed
[Fact]
public void If_SnapshotTime_is_set_Snapshot_must_be_true()
{
RequireServer.Check();

var sessionOptions = new ClientSessionOptions
{
Snapshot = false,
SnapshotTime = new BsonTimestamp(1, 1)
};

var mongoClient = DriverTestConfiguration.Client;

var exception = Record.Exception(() => mongoClient.StartSession(sessionOptions));
exception.Should().BeOfType<NotSupportedException>();
}

// https://github.com/mongodb/specifications/blob/192976b194afdb1f458cbba2530c73de6b2c700f/source/sessions/tests/README.md#22-retrieving-snapshottime-on-a-non-snapshot-session-raises-an-error
[Fact]
public void Retrieving_SnapshotTime_on_non_snapshot_session_raises_error()
{
RequireServer.Check().Supports(Feature.SnapshotReads).ClusterTypes(ClusterType.ReplicaSet, ClusterType.Sharded);

var sessionOptions = new ClientSessionOptions
{
Snapshot = false
};

var mongoClient = DriverTestConfiguration.Client;

using var session = mongoClient.StartSession(sessionOptions);

var exception = Record.Exception(() => session.GetSnapshotTime());
exception.Should().BeOfType<InvalidOperationException>();
exception.Message.Should().Contain("non-snapshot session");
}

private sealed class MongocryptdContext : IDisposable
{
public IMongoClient MongoClient { get; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1080,6 +1080,9 @@ private IClientSessionHandle CreateSession(BsonDocument entity, Dictionary<strin
case "snapshot":
options.Snapshot = option.Value.ToBoolean();
break;
case "snapshotTime":
options.SnapshotTime = _results[option.Value.AsString].AsBsonTimestamp;
break;
case "causalConsistency":
options.CausalConsistency = option.Value.ToBoolean();
break;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/* Copyright 2010-present MongoDB Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

using System;
using System.Threading;
using System.Threading.Tasks;
using MongoDB.Bson;

namespace MongoDB.Driver.Tests.UnifiedTestOperations;

public class UnifiedGetSnapshotOperation : IUnifiedEntityTestOperation
{
private readonly IClientSessionHandle _session;

public UnifiedGetSnapshotOperation(IClientSessionHandle session)
{
_session = session;
}

public OperationResult Execute(CancellationToken cancellationToken) => GetSnapshotTime();

public Task<OperationResult> ExecuteAsync(CancellationToken cancellationToken) => Task.FromResult(GetSnapshotTime());

private OperationResult GetSnapshotTime()
{
try
{
return OperationResult.FromResult(_session.GetSnapshotTime());
}
catch (Exception exception)
{
return OperationResult.FromException(exception);
}
}
}

public class UnifiedGetSnapshotOperationBuilder
{
private readonly UnifiedEntityMap _entityMap;

public UnifiedGetSnapshotOperationBuilder(UnifiedEntityMap entityMap)
{
_entityMap = entityMap;
}

public UnifiedGetSnapshotOperation Build(string targetSessionId, BsonDocument arguments)
{
if (arguments != null)
{
throw new FormatException("GetSnapshotTime is not expected to contain arguments.");
}

var session = _entityMap.Sessions[targetSessionId];
return new UnifiedGetSnapshotOperation(session);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ public IUnifiedTestOperation CreateOperation(string operationName, string target
"abortTransaction" => new UnifiedAbortTransactionOperationBuilder(_entityMap).Build(targetEntityId, operationArguments),
"commitTransaction" => new UnifiedCommitTransactionOperationBuilder(_entityMap).Build(targetEntityId, operationArguments),
"endSession" => new UnifiedEndSessionOperationBuilder(_entityMap).Build(targetEntityId, operationArguments),
"getSnapshotTime" => new UnifiedGetSnapshotOperationBuilder(_entityMap).Build(targetEntityId, operationArguments),
"startTransaction" => new UnifiedStartTransactionOperationBuilder(_entityMap).Build(targetEntityId, operationArguments),
"withTransaction" => new UnifiedWithTransactionOperationBuilder(_entityMap).Build(targetEntityId, operationArguments),
_ => throw new FormatException($"Invalid method name: '{operationName}'."),
Expand Down