diff --git a/README.md b/README.md index 0b20eec..756cbd9 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -## [![Nuget](https://img.shields.io/nuget/v/MongoDB.Extensions.Context.svg?style=flat)](https://www.nuget.org/packages/MongoDB.Extensions.Context) [![GitHub Release](https://img.shields.io/github/release/SwissLife-OSS/mongo-extensions.svg?style=flat)](https://github.com/SwissLife-OSS/Mongo-extensions/releases/latest) [![Build Status](https://dev.azure.com/swisslife-oss/swisslife-oss/_apis/build/status/MongoDB.Extensions.Release?branchName=master)](https://dev.azure.com/swisslife-oss/swisslife-oss/_build/latest?definitionId=11&branchName=master) +## [![Nuget](https://img.shields.io/nuget/v/MongoDB.Extensions.Context.svg?style=flat)](https://www.nuget.org/packages/MongoDB.Extensions.Context) [![GitHub Release](https://img.shields.io/github/release/SwissLife-OSS/mongo-extensions.svg?style=flat)](https://github.com/SwissLife-OSS/Mongo-extensions/releases/latest) [![Build Status](https://github.com/SwissLife-OSS/mongo-extensions/actions/workflows/release.yml/badge.svg)](https://github.com/SwissLife-OSS/mongo-extensions/actions/workflows/release.yml) **MongoDB.Extensions provides a set of utility libraries for MongoDB.** diff --git a/src/Session.Tests/MongoSessionProviderTests.cs b/src/Session.Tests/MongoSessionProviderTests.cs new file mode 100644 index 0000000..4a0aa0d --- /dev/null +++ b/src/Session.Tests/MongoSessionProviderTests.cs @@ -0,0 +1,142 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using MongoDB.Bson; +using MongoDB.Driver; +using MongoDB.Extensions.Context; +using Squadron; +using Xunit; + +namespace MongoDB.Extensions.Session.Tests; + +public class MongoSessionProviderTests : IClassFixture +{ + private readonly IServiceProvider _serviceProvider; + + public MongoSessionProviderTests(MongoReplicaSetResource mongoResource) + { + var mongoOptions = new MongoOptions + { + ConnectionString = mongoResource.ConnectionString, + DatabaseName = mongoResource.CreateDatabase().DatabaseNamespace.DatabaseName + }; + + _serviceProvider = new ServiceCollection() + .AddSingleton(mongoOptions) + .AddMongoSessionProvider() + .BuildServiceProvider(); + } + + [Fact] + public async Task BeginTransactionAsync_ShouldBeginTransaction() + { + // Arrange + ISessionProvider sessionProvider = _serviceProvider + .GetRequiredService>(); + + // Act + ITransactionSession transactionSession = await sessionProvider + .BeginTransactionAsync(CancellationToken.None); + + // Assert + transactionSession.Should().NotBeNull(); + IClientSessionHandle clientSessionHandle = transactionSession.GetSessionHandle(); + clientSessionHandle.ServerSession.Id["id"].AsGuid.Should().NotBeEmpty(); + clientSessionHandle.IsInTransaction.Should().BeTrue(); + } + + [Fact] + public async Task StartSessionAsync_ShouldStartSession() + { + // Arrange + ISessionProvider sessionProvider = _serviceProvider + .GetRequiredService>(); + + // Act + ISession session = await sessionProvider + .StartSessionAsync(CancellationToken.None); + + // Assert + session.Should().NotBeNull(); + IClientSessionHandle clientSessionHandle = session.GetSessionHandle(); + clientSessionHandle.ServerSession.Id["id"].AsGuid.Should().NotBeEmpty(); + clientSessionHandle.IsInTransaction.Should().BeFalse(); + } + + [Fact] + public async Task MongoSession_Dispose_ShouldDisposeSession() + { + // Arrange + ISessionProvider sessionProvider = _serviceProvider + .GetRequiredService>(); + + ISession session = await sessionProvider + .StartSessionAsync(CancellationToken.None); + + // Act + session.Dispose(); + + // Assert + IClientSessionHandle clientSessionHandle = session.GetSessionHandle(); + Assert.Throws(() => clientSessionHandle.ServerSession); + } + + [Fact] + public async Task MongoTransactionSession_Dispose_ShouldDisposeSession() + { + // Arrange + ISessionProvider sessionProvider = _serviceProvider + .GetRequiredService>(); + + ITransactionSession transactionSession = await sessionProvider + .BeginTransactionAsync(CancellationToken.None); + + // Act + transactionSession.Dispose(); + + // Assert + IClientSessionHandle clientSessionHandle = transactionSession.GetSessionHandle(); + Assert.Throws(() => clientSessionHandle.ServerSession); + } + + [Fact] + public async Task MongoTransactionSession_NotCommitting_ShouldNotAffectDatabase() + { + // Arrange + ISessionProvider sessionProvider = _serviceProvider + .GetRequiredService>(); + + ITransactionSession transactionSession = await sessionProvider + .BeginTransactionAsync(CancellationToken.None); + TestDbContext context = _serviceProvider.GetRequiredService(); + IMongoCollection collection = context.CreateCollection(); + await collection.InsertOneAsync(transactionSession.GetSessionHandle(), new BsonDocument()); + + // Act + // Not committing the transaction + + // Assert + (await collection + .Find(FilterDefinition.Empty) + .ToListAsync()) + .Count.Should().Be(0); + } + + private class TestDbContext : MongoDbContext + { + public TestDbContext(MongoOptions mongoOptions) + : base(mongoOptions) + { + } + + protected override void OnConfiguring(IMongoDatabaseBuilder mongoDatabaseBuilder) + { + } + } + + private interface ITestScope + { + } +} diff --git a/src/Session/ISession.cs b/src/Session/ISession.cs new file mode 100644 index 0000000..81a7195 --- /dev/null +++ b/src/Session/ISession.cs @@ -0,0 +1,12 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace MongoDB.Extensions.Session; + +public interface ISession : IDisposable +{ + Task WithTransactionAsync( + Func> action, + CancellationToken cancellationToken); +} diff --git a/src/Session/ISessionProvider.cs b/src/Session/ISessionProvider.cs new file mode 100644 index 0000000..d4f9bcb --- /dev/null +++ b/src/Session/ISessionProvider.cs @@ -0,0 +1,13 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace MongoDB.Extensions.Session; + +public interface ISessionProvider +{ + Task BeginTransactionAsync( + CancellationToken cancellationToken); + + Task StartSessionAsync( + CancellationToken cancellationToken); +} diff --git a/src/Session/ITransactionSession.cs b/src/Session/ITransactionSession.cs new file mode 100644 index 0000000..65157d6 --- /dev/null +++ b/src/Session/ITransactionSession.cs @@ -0,0 +1,9 @@ +using System; +using System.Threading.Tasks; + +namespace MongoDB.Extensions.Session; + +public interface ITransactionSession : IDisposable +{ + Task CommitAsync(); +} diff --git a/src/Session/Internal/MongoSession.cs b/src/Session/Internal/MongoSession.cs new file mode 100644 index 0000000..e149d63 --- /dev/null +++ b/src/Session/Internal/MongoSession.cs @@ -0,0 +1,53 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using MongoDB.Driver; + +namespace MongoDB.Extensions.Session; + +internal sealed class MongoSession : ISession +{ + private bool _disposed; + + private static TransactionOptions TransactionOptions { get; } = new( + ReadConcern.Majority, + ReadPreference.Primary, + WriteConcern.WMajority.With(journal: true), + TimeSpan.FromSeconds(180)); + + public MongoSession(IClientSessionHandle clientSession) + { + Session = clientSession; + } + + public IClientSessionHandle Session { get; } + + public Task WithTransactionAsync( + Func> action, + CancellationToken cancellationToken) + { + return Session.WithTransactionAsync( + (_, ct) => action(this, ct), + TransactionOptions, + cancellationToken); + } + + private void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + Session.Dispose(); + } + + _disposed = true; + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } +} diff --git a/src/Session/Internal/MongoSessionProvider.cs b/src/Session/Internal/MongoSessionProvider.cs new file mode 100644 index 0000000..e12d049 --- /dev/null +++ b/src/Session/Internal/MongoSessionProvider.cs @@ -0,0 +1,51 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using MongoDB.Driver; +using MongoDB.Extensions.Context; + +namespace MongoDB.Extensions.Session; + +public class MongoSessionProvider : ISessionProvider + where TContext : IMongoDbContext +{ + private readonly IMongoClient _mongoClient; + + public MongoSessionProvider(TContext context) + { + _mongoClient = context.Client; + } + + public Task BeginTransactionAsync( + CancellationToken cancellationToken) + { + return BeginTransactionAsync(true, cancellationToken); + } + + private async Task BeginTransactionAsync( + bool safeModeEnabled, + CancellationToken cancellationToken) + { + IClientSessionHandle clientSession = await _mongoClient + .StartSessionAsync(cancellationToken: cancellationToken); + + var transactionOptions = new TransactionOptions( + ReadConcern.Majority, + ReadPreference.Primary, + WriteConcern.WMajority.With(journal: safeModeEnabled), + TimeSpan.FromSeconds(180)); + + clientSession.StartTransaction(transactionOptions); + + return new MongoTransactionSession(clientSession, cancellationToken); + } + + public async Task StartSessionAsync( + CancellationToken cancellationToken) + { + IClientSessionHandle clientSession = await _mongoClient + .StartSessionAsync(cancellationToken: cancellationToken); + + return new MongoSession(clientSession); + } +} diff --git a/src/Session/Internal/MongoTransactionSession.cs b/src/Session/Internal/MongoTransactionSession.cs new file mode 100644 index 0000000..9835aa7 --- /dev/null +++ b/src/Session/Internal/MongoTransactionSession.cs @@ -0,0 +1,46 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using MongoDB.Driver; + +namespace MongoDB.Extensions.Session; + +internal class MongoTransactionSession : ITransactionSession +{ + private readonly CancellationToken _cancellationToken; + private bool _disposed; + + public MongoTransactionSession( + IClientSessionHandle clientSession, + CancellationToken cancellationToken) + { + Session = clientSession; + _cancellationToken = cancellationToken; + } + + public IClientSessionHandle Session { get; } + + public async Task CommitAsync() + { + await Session.CommitTransactionAsync(_cancellationToken); + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + Session.Dispose(); + } + + _disposed = true; + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } +} diff --git a/src/Session/ServiceCollectionExtensions.cs b/src/Session/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..2ce6a3e --- /dev/null +++ b/src/Session/ServiceCollectionExtensions.cs @@ -0,0 +1,18 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using MongoDB.Extensions.Context; + +namespace MongoDB.Extensions.Session; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddMongoSessionProvider( + this IServiceCollection services) + where TContext : class, IMongoDbContext + { + services.TryAddSingleton(); + services.TryAddSingleton, MongoSessionProvider>(); + + return services; + } +} diff --git a/src/Session/Session.csproj b/src/Session/Session.csproj index 02b41b9..503059f 100644 --- a/src/Session/Session.csproj +++ b/src/Session/Session.csproj @@ -9,6 +9,11 @@ + + + + + diff --git a/src/Session/TransactionSessionExtensions.cs b/src/Session/TransactionSessionExtensions.cs new file mode 100644 index 0000000..97d7eb3 --- /dev/null +++ b/src/Session/TransactionSessionExtensions.cs @@ -0,0 +1,31 @@ +using System; +using MongoDB.Driver; + +namespace MongoDB.Extensions.Session; + +public static class TransactionSessionExtensions +{ + public static IClientSessionHandle GetSessionHandle( + this ITransactionSession session) + { + if (session is MongoTransactionSession mongoTransactionSession) + { + return mongoTransactionSession.Session; + } + + throw new InvalidOperationException( + $"Unknown session type {session.GetType().Name}"); + } + + public static IClientSessionHandle GetSessionHandle( + this ISession session) + { + if (session is MongoSession mongoSession) + { + return mongoSession.Session; + } + + throw new InvalidOperationException( + $"Unknown session type {session.GetType().Name}"); + } +}