Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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.

409 changes: 408 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.
/// </summary>
/// <value> The snapshot time. </value>
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
35 changes: 33 additions & 2 deletions src/MongoDB.Driver/Core/Bindings/CoreSessionOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
* limitations under the License.
*/

using MongoDB.Bson;
using MongoDB.Driver.Core.Misc;

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

// constructors
/// <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="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>
/// <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 +101,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;
}
}
31 changes: 25 additions & 6 deletions src/MongoDB.Driver/IClientSessionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,35 @@
* limitations under the License.
*/

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

namespace MongoDB.Driver
{
// TODO: CSOT: Make it public when CSOT will be ready for GA
internal static class IClientSessionExtensions
/// <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 the following methods public when CSOT will be ready for GA
// TODO: Merge these extension methods in IClientSession interface on major release
public static void AbortTransaction(this IClientSession session, AbortTransactionOptions options, CancellationToken cancellationToken = default)
internal static void AbortTransaction(this IClientSession session, AbortTransactionOptions options, CancellationToken cancellationToken = default)
{
if (options?.Timeout == null || session.Options.DefaultTransactionOptions?.Timeout == options.Timeout)
{
Expand All @@ -33,7 +52,7 @@ public static void AbortTransaction(this IClientSession session, AbortTransactio
((IClientSessionInternal)session).AbortTransaction(options, cancellationToken);
}

public static Task AbortTransactionAsync(this IClientSession session, AbortTransactionOptions options, CancellationToken cancellationToken = default)
internal static Task AbortTransactionAsync(this IClientSession session, AbortTransactionOptions options, CancellationToken cancellationToken = default)
{
if (options?.Timeout == null || session.Options.DefaultTransactionOptions?.Timeout == options.Timeout)
{
Expand All @@ -43,7 +62,7 @@ public static Task AbortTransactionAsync(this IClientSession session, AbortTrans
return ((IClientSessionInternal)session).AbortTransactionAsync(options, cancellationToken);
}

public static void CommitTransaction(this IClientSession session, CommitTransactionOptions options, CancellationToken cancellationToken = default)
internal static void CommitTransaction(this IClientSession session, CommitTransactionOptions options, CancellationToken cancellationToken = default)
{
if (options?.Timeout == null || session.Options.DefaultTransactionOptions?.Timeout == options.Timeout)
{
Expand All @@ -54,7 +73,7 @@ public static void CommitTransaction(this IClientSession session, CommitTransact
((IClientSessionInternal)session).CommitTransaction(options, cancellationToken);
}

public static Task CommitTransactionAsync(this IClientSession session, CommitTransactionOptions options, CancellationToken cancellationToken = default)
internal static Task CommitTransactionAsync(this IClientSession session, CommitTransactionOptions options, CancellationToken cancellationToken = default)
{
if (options?.Timeout == null || session.Options.DefaultTransactionOptions?.Timeout == options.Timeout)
{
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 InvalidOperationException("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
37 changes: 37 additions & 0 deletions tests/MongoDB.Driver.Tests/ClientSessionExtensionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@
* limitations under the License.
*/

using System;
using System.Collections.Generic;
using FluentAssertions;
using MongoDB.Bson;
using MongoDB.Driver.Core.Bindings;
using Moq;
using Xunit;
Expand All @@ -35,6 +37,41 @@ public void GetEffectiveReadPreferenceTests(
result.Should().Be(expectedReadPreference);
}

[Fact]
public void GetSnapshotTime_on_snapshot_session_returns_expected_value()
{
var snapshotTime = new BsonTimestamp(1234567890, 1);
var coreSessionMock = new Mock<ICoreSessionHandle>();
coreSessionMock.SetupGet(s => s.IsSnapshot).Returns(true);
coreSessionMock.SetupGet(s => s.SnapshotTime).Returns(snapshotTime);

var session = new ClientSessionHandle(
Mock.Of<IMongoClient>(),
new ClientSessionOptions(),
coreSessionMock.Object);

var result = session.GetSnapshotTime();

result.Should().Be(snapshotTime);
}

[Fact]
public void GetSnapshotTime_on_non_snapshot_session_throws_InvalidOperationException()
{
var coreSessionMock = new Mock<ICoreSessionHandle>();
coreSessionMock.SetupGet(s => s.IsSnapshot).Returns(false);

var session = new ClientSessionHandle(
Mock.Of<IMongoClient>(),
new ClientSessionOptions(),
coreSessionMock.Object);

var exception = Record.Exception(() => session.GetSnapshotTime());

exception.Should().BeOfType<InvalidOperationException>();
exception.Message.Should().Contain("non-snapshot session");
}

public static IEnumerable<object[]> GetEffectiveReadPreferenceTestCases()
{
var noTransactionSession = CreateSessionMock(null);
Expand Down
13 changes: 13 additions & 0 deletions tests/MongoDB.Driver.Tests/ClientSessionHandleTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,19 @@ public void ServerSession_returns_expected_result()
result.Should().BeSameAs(subject._serverSession());
}

[Fact]
public void SnapshotTime_returns_expected_result()
{
var subject = CreateSubject();
var value = new BsonTimestamp(1234567890, 1);
var mockCoreSession = Mock.Get(subject.WrappedCoreSession);
mockCoreSession.SetupGet(m => m.SnapshotTime).Returns(value);

var result = subject.SnapshotTime;

result.Should().BeSameAs(value);
}

[Fact]
public void WrappedCoreSession_returns_expected_result()
{
Expand Down
54 changes: 49 additions & 5 deletions tests/MongoDB.Driver.Tests/ClientSessionOptionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
*/

using FluentAssertions;
using MongoDB.Bson;
using MongoDB.TestHelpers.XunitExtensions;
using Xunit;

Expand Down Expand Up @@ -79,34 +80,77 @@ public void DefaultTransactionOptions_set_should_have_expected_result(
subject.DefaultTransactionOptions.Should().BeSameAs(value);
}

[Theory]
[ParameterAttributeData]
public void SnapshotTime_get_should_return_expected_result(
[Values(false, true)] bool nullValue)
{
var value = nullValue ? null : new BsonTimestamp(1234567890, 1);
var subject = CreateSubject(snapshotTime: value);

var result = subject.SnapshotTime;

result.Should().Be(value);
}

[Theory]
[ParameterAttributeData]
public void SnapshotTime_set_should_have_expected_result(
[Values(false, true)] bool nullValue)
{
var subject = CreateSubject();
var value = nullValue ? null : new BsonTimestamp(1234567890, 1);

subject.SnapshotTime = value;

subject.SnapshotTime.Should().Be(value);
}

[Theory]
[ParameterAttributeData]
public void ToCore_should_return_expected_result(
[Values(null, false, true)] bool? causalConsistency,
[Values(false, true)] bool isImplicit,
[Values(false, true)] bool nullDefaultTransactionOptions)
[Values(false, true)] bool snapshot,
[Values(false, true)] bool nullDefaultTransactionOptions,
[Values(false, true)] bool nullSnapshotTime)
{
// Skip invalid combinations: snapshotTime can only be set if snapshot is true
if (!snapshot && !nullSnapshotTime)
{
return;
}

var defaultTransactionOptions = nullDefaultTransactionOptions ? null : new TransactionOptions();
var snapshotTime = nullSnapshotTime ? null : new BsonTimestamp(1234567890, 1);
var subject = CreateSubject(
causalConsistency: causalConsistency,
defaultTransactionOptions: defaultTransactionOptions);
defaultTransactionOptions: defaultTransactionOptions,
snapshot: snapshot,
snapshotTime: snapshotTime);

var result = subject.ToCore(isImplicit: isImplicit);

result.DefaultTransactionOptions.Should().BeSameAs(defaultTransactionOptions);
result.IsCausallyConsistent.Should().Be(causalConsistency ?? true);
result.IsCausallyConsistent.Should().Be(causalConsistency ?? !snapshot);
result.IsImplicit.Should().Be(isImplicit);
result.IsSnapshot.Should().Be(snapshot);
result.SnapshotTime.Should().Be(snapshotTime);
}

// private methods
private ClientSessionOptions CreateSubject(
bool? causalConsistency = null,
TransactionOptions defaultTransactionOptions = null)
TransactionOptions defaultTransactionOptions = null,
bool snapshot = false,
BsonTimestamp snapshotTime = null)
{
return new ClientSessionOptions
{
CausalConsistency = causalConsistency,
DefaultTransactionOptions = defaultTransactionOptions
DefaultTransactionOptions = defaultTransactionOptions,
Snapshot = snapshot,
SnapshotTime = snapshotTime
};
}
}
Expand Down
Loading