From 0d34b8ed73f1bd1c3c08b9f20b6951c2da1f5021 Mon Sep 17 00:00:00 2001 From: Normen Scheiber <46715105+nscheibe@users.noreply.github.com> Date: Thu, 1 Dec 2022 16:41:29 +0100 Subject: [PATCH] Added transaction context for mongo database context. (#71) --- src/Context.Tests/Context.Tests.csproj | 1 + src/Context.Tests/Helpers/Bar.cs | 2 + src/Context.Tests/Helpers/Foo.cs | 2 + .../MongoTransactionDbContextTests.cs | 267 ++++++++++++++++++ ...ionAsync_AddFooBarWithCommit_AllSaved.snap | 32 +++ ...nc_AddFooBarWithRollback_NothingSaved.snap | 1 + ...ewTransactionDbContext_OptionsCorrect.snap | 23 ++ ...tionTransactionOptions_OptionsCorrect.snap | 23 ++ ...nContextWithDifferentObjects_AllSaved.snap | 27 ++ ...eObjects_ConcurrencyExceptionAndSaved.snap | 17 ++ src/Context/Context.csproj | 4 + src/Context/DefaultDefinitions.cs | 15 + src/Context/IMongoDbTransaction.cs | 13 + src/Context/IMongoTransactionDbContext.cs | 16 ++ src/Context/Internal/MongoDbContextData.cs | 6 +- src/Context/MongoDbContext.cs | 120 ++++---- src/Context/MongoTransactionDbContext.cs | 112 ++++++++ .../ClientSessionHandleExtensions.cs | 12 + .../MongoCollectionExtensions.cs | 17 ++ .../MongoDatabaseExtensions.cs | 215 +++++++------- src/Session/ClientSessionHandleExtensions.cs | 1 + 21 files changed, 776 insertions(+), 150 deletions(-) create mode 100644 src/Context.Tests/MongoTransactionDbContextTests.cs create mode 100644 src/Context.Tests/__snapshots__/MongoTransactionDbContextTests.StartNewTransactionAsync_AddFooBarWithCommit_AllSaved.snap create mode 100644 src/Context.Tests/__snapshots__/MongoTransactionDbContextTests.StartNewTransactionAsync_AddFooBarWithRollback_NothingSaved.snap create mode 100644 src/Context.Tests/__snapshots__/MongoTransactionDbContextTests.StartNewTransactionAsync_CreateNewTransactionDbContext_OptionsCorrect.snap create mode 100644 src/Context.Tests/__snapshots__/MongoTransactionDbContextTests.StartNewTransactionAsync_SetTransactionTransactionOptions_OptionsCorrect.snap create mode 100644 src/Context.Tests/__snapshots__/MongoTransactionDbContextTests.StartNewTransactionAsync_TwoTransactionContextWithDifferentObjects_AllSaved.snap create mode 100644 src/Context.Tests/__snapshots__/MongoTransactionDbContextTests.StartNewTransactionAsync_TwoTransactionContextWithSameObjects_ConcurrencyExceptionAndSaved.snap create mode 100644 src/Context/DefaultDefinitions.cs create mode 100644 src/Context/IMongoDbTransaction.cs create mode 100644 src/Context/IMongoTransactionDbContext.cs create mode 100644 src/Context/MongoTransactionDbContext.cs create mode 100644 src/Prime.Extensions/ClientSessionHandleExtensions.cs diff --git a/src/Context.Tests/Context.Tests.csproj b/src/Context.Tests/Context.Tests.csproj index f1afb72..4fec853 100644 --- a/src/Context.Tests/Context.Tests.csproj +++ b/src/Context.Tests/Context.Tests.csproj @@ -8,6 +8,7 @@ + diff --git a/src/Context.Tests/Helpers/Bar.cs b/src/Context.Tests/Helpers/Bar.cs index 3d506df..0bfa81a 100644 --- a/src/Context.Tests/Helpers/Bar.cs +++ b/src/Context.Tests/Helpers/Bar.cs @@ -2,5 +2,7 @@ namespace MongoDB.Extensions.Context.Tests { public class Bar { + public int Id { get; set; } + public string? BarName { get; set; } } } diff --git a/src/Context.Tests/Helpers/Foo.cs b/src/Context.Tests/Helpers/Foo.cs index dda2a87..90560dc 100644 --- a/src/Context.Tests/Helpers/Foo.cs +++ b/src/Context.Tests/Helpers/Foo.cs @@ -2,5 +2,7 @@ namespace MongoDB.Extensions.Context.Tests { public class Foo { + public int Id { get; set; } + public string? FooName { get; set; } } } diff --git a/src/Context.Tests/MongoTransactionDbContextTests.cs b/src/Context.Tests/MongoTransactionDbContextTests.cs new file mode 100644 index 0000000..d41337e --- /dev/null +++ b/src/Context.Tests/MongoTransactionDbContextTests.cs @@ -0,0 +1,267 @@ +using System; +using System.Reflection.Metadata; +using System.Threading.Tasks; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Serializers; +using MongoDB.Driver; +using MongoDB.Prime.Extensions; +using Snapshooter.Xunit; +using Squadron; +using Xunit; + +namespace MongoDB.Extensions.Context.Tests +{ + public class MongoTransactionDbContextTests : IClassFixture + { + private readonly MongoOptions _mongoOptions; + private readonly IMongoDatabase _mongoDatabase; + + public MongoTransactionDbContextTests(MongoReplicaSetResource mongoResource) + { + _mongoDatabase = mongoResource.CreateDatabase(); + _mongoOptions = new MongoOptions + { + ConnectionString = mongoResource.ConnectionString, + DatabaseName = _mongoDatabase.DatabaseNamespace.DatabaseName + }; + } + + #region StartNewTransactionAsync Tests + + [Fact] + public async Task StartNewTransactionAsync_CreateNewTransactionDbContext_OptionsCorrect() + { + // Arrange + var testMongoDbContext = new TestMongoDbContext(_mongoOptions); + + // Act + using IMongoTransactionDbContext transactionDbContext = + await testMongoDbContext.StartNewTransactionAsync(); + + // Assert + Snapshot.Match(transactionDbContext.TransactionOptions); + } + + [Fact] + public async Task StartNewTransactionAsync_SetTransactionTransactionOptions_OptionsCorrect() + { + // Arrange + var testMongoDbContext = new TestMongoDbContext(_mongoOptions); + + TransactionOptions transactionOptions = + new TransactionOptions( + ReadConcern.Local, + ReadPreference.Secondary, + WriteConcern.W3.With(journal: false), + TimeSpan.FromSeconds(300)); + + // Act + IMongoTransactionDbContext transactionDbContext = + await testMongoDbContext.StartNewTransactionAsync(transactionOptions); + + // Assert + Snapshot.Match(transactionDbContext.TransactionOptions); + } + + [Fact] + public async Task StartNewTransactionAsync_ClientSessionIdCompare_DifferentIds() + { + // Arrange + var testMongoDbContext = new TestMongoDbContext(_mongoOptions); + IClientSessionHandle baseSession = + await testMongoDbContext.Client.StartSessionAsync(); + + // Act + using MongoTransactionDbContext transactionDbContext = (MongoTransactionDbContext) + await testMongoDbContext.StartNewTransactionAsync(); + + // Assert + Assert.NotEqual( + baseSession.GetSessionId(), + transactionDbContext.ClientSession.GetSessionId()); + } + + [Fact] + public async Task StartNewTransactionAsync_GetTwicSameCollections_CollectionCached() + { + // Arrange + var testMongoDbContext = new TestMongoDbContext(_mongoOptions); + + using IMongoTransactionDbContext transactionDbContext = + await testMongoDbContext.StartNewTransactionAsync(); + + // Act + IMongoCollection ref1 = transactionDbContext.CreateCollection(); + IMongoCollection ref2 = transactionDbContext.CreateCollection(); + IMongoCollection ref3 = transactionDbContext.GetCollection(); + + // Assert + Assert.Same(ref1, ref2); + Assert.Same(ref2, ref3); + } + + [Fact] + public async Task StartNewTransactionAsync_AddFooBarWithoutCommit_NothingSaved() + { + // Arrange + var testMongoDbContext = new TestMongoDbContext(_mongoOptions); + + using IMongoTransactionDbContext transactionDbContext = + await testMongoDbContext.StartNewTransactionAsync(); + + // Act + transactionDbContext.GetCollection().InsertOne(new Foo { Id = 1, FooName = "Foo1" }); + transactionDbContext.GetCollection().InsertOne(new Bar { Id = 1, BarName = "Bar1" }); + + // Assert + Assert.Empty(_mongoDatabase.GetCollection().Dump()); + Assert.Empty(_mongoDatabase.GetCollection().Dump()); + } + + [Fact] + public async Task StartNewTransactionAsync_AddFooBarWithCommit_AllSaved() + { + // Arrange + var testMongoDbContext = new TestMongoDbContext(_mongoOptions); + + using IMongoTransactionDbContext transactionDbContext = + await testMongoDbContext.StartNewTransactionAsync(); + + // Act + transactionDbContext.GetCollection().InsertOne(new Foo { Id = 1, FooName = "Foo1" }); + transactionDbContext.GetCollection().InsertOne(new Bar { Id = 1, BarName = "Bar1" }); + await transactionDbContext.CommitAsync(); + + // Assert + Snapshot.Match(_mongoDatabase.DumpAllCollections()); + } + + [Fact] + public async Task StartNewTransactionAsync_AddFooBarWithRollback_NothingSaved() + { + // Arrange + var testMongoDbContext = new TestMongoDbContext(_mongoOptions); + + using IMongoTransactionDbContext transactionDbContext = + await testMongoDbContext.StartNewTransactionAsync(); + + // Act + transactionDbContext.GetCollection().InsertOne(new Foo { Id = 1, FooName = "Foo1" }); + transactionDbContext.GetCollection().InsertOne(new Bar { Id = 1, BarName = "Bar1" }); + await transactionDbContext.RollbackAsync(); + + // Assert + Assert.Empty(_mongoDatabase.GetCollection().Dump()); + Assert.Empty(_mongoDatabase.GetCollection().Dump()); + } + + [Fact] + public async Task StartNewTransactionAsync_TwoTransactionContextWithSameObjects_ConcurrencyExceptionAndSaved() + { + // Arrange + var testMongoDbContext = new TestMongoDbContext(_mongoOptions); + + using IMongoTransactionDbContext transactionDbContext1 = + await testMongoDbContext.StartNewTransactionAsync(); + + using IMongoTransactionDbContext transactionDbContext2 = + await testMongoDbContext.StartNewTransactionAsync(); + + IMongoCollection transactionCollection1 = + transactionDbContext1.GetCollection(); + + IMongoCollection transactionCollection2 = + transactionDbContext2.GetCollection(); + + // Act + transactionCollection1.InsertOne(new Foo { Id = 1, FooName = "Foo1a" }); + transactionCollection2.InsertOne(new Foo { Id = 1, FooName = "Foo1b" }); + await transactionDbContext1.CommitAsync(); + Func commit2Action = async () => await transactionDbContext2.CommitAsync(); + + // Assert + await Assert.ThrowsAsync(commit2Action); + Snapshot.Match(_mongoDatabase.DumpAllCollections()); + } + + [Fact] + public async Task StartNewTransactionAsync_TwoTransactionContextWithDifferentObjectsNoCommit_NothingSaved() + { + // Arrange + var testMongoDbContext = new TestMongoDbContext(_mongoOptions); + testMongoDbContext.CreateCollection(); + + using IMongoTransactionDbContext transactionDbContext1 = + await testMongoDbContext.StartNewTransactionAsync(); + + using IMongoTransactionDbContext transactionDbContext2 = + await testMongoDbContext.StartNewTransactionAsync(); + + IMongoCollection transactionCollection1 = + transactionDbContext1.GetCollection(); + + IMongoCollection transactionCollection2 = + transactionDbContext2.GetCollection(); + + // Act + transactionCollection1.InsertOne(new Foo { Id = 1, FooName = "Foo1a" }); + transactionCollection2.InsertOne(new Foo { Id = 2, FooName = "Foo1b" }); + + // Assert + Assert.Empty(_mongoDatabase.GetCollection().Dump()); + Assert.Empty(_mongoDatabase.GetCollection().Dump()); + } + + [Fact] + public async Task StartNewTransactionAsync_TwoTransactionContextWithDifferentObjects_AllSaved() + { + // Arrange + var testMongoDbContext = new TestMongoDbContext(_mongoOptions); + testMongoDbContext.CreateCollection(); + + using IMongoTransactionDbContext transactionDbContext1 = + await testMongoDbContext.StartNewTransactionAsync(); + + using IMongoTransactionDbContext transactionDbContext2 = + await testMongoDbContext.StartNewTransactionAsync(); + + IMongoCollection transactionCollection1 = + transactionDbContext1.GetCollection(); + + IMongoCollection transactionCollection2 = + transactionDbContext2.GetCollection(); + + // Act + transactionCollection1.InsertOne(new Foo { Id = 1, FooName = "Foo1a" }); + await transactionDbContext1.CommitAsync(); + + transactionCollection2.InsertOne(new Foo { Id = 544656454, FooName = "Foo1b" }); + await transactionDbContext2.CommitAsync(); + + // Assert + Snapshot.Match(_mongoDatabase.DumpAllCollections()); + } + + #endregion + + #region Private Helpers + + private class TestMongoDbContext : MongoDbContext + { + public TestMongoDbContext(MongoOptions mongoOptions) : base(mongoOptions) + { + } + + public TestMongoDbContext(MongoOptions mongoOptions, bool enableAutoInit) + : base(mongoOptions, enableAutoInit) + { + } + + protected override void OnConfiguring(IMongoDatabaseBuilder mongoDatabaseBuilder) + { + } + } + + #endregion + } +} diff --git a/src/Context.Tests/__snapshots__/MongoTransactionDbContextTests.StartNewTransactionAsync_AddFooBarWithCommit_AllSaved.snap b/src/Context.Tests/__snapshots__/MongoTransactionDbContextTests.StartNewTransactionAsync_AddFooBarWithCommit_AllSaved.snap new file mode 100644 index 0000000..efe4f51 --- /dev/null +++ b/src/Context.Tests/__snapshots__/MongoTransactionDbContextTests.StartNewTransactionAsync_AddFooBarWithCommit_AllSaved.snap @@ -0,0 +1,32 @@ +[ + { + "Key": "Bar", + "Value": [ + [ + { + "Name": "_id", + "Value": 1 + }, + { + "Name": "BarName", + "Value": "Bar1" + } + ] + ] + }, + { + "Key": "Foo", + "Value": [ + [ + { + "Name": "_id", + "Value": 1 + }, + { + "Name": "FooName", + "Value": "Foo1" + } + ] + ] + } +] diff --git a/src/Context.Tests/__snapshots__/MongoTransactionDbContextTests.StartNewTransactionAsync_AddFooBarWithRollback_NothingSaved.snap b/src/Context.Tests/__snapshots__/MongoTransactionDbContextTests.StartNewTransactionAsync_AddFooBarWithRollback_NothingSaved.snap new file mode 100644 index 0000000..f117788 --- /dev/null +++ b/src/Context.Tests/__snapshots__/MongoTransactionDbContextTests.StartNewTransactionAsync_AddFooBarWithRollback_NothingSaved.snap @@ -0,0 +1 @@ +{} diff --git a/src/Context.Tests/__snapshots__/MongoTransactionDbContextTests.StartNewTransactionAsync_CreateNewTransactionDbContext_OptionsCorrect.snap b/src/Context.Tests/__snapshots__/MongoTransactionDbContextTests.StartNewTransactionAsync_CreateNewTransactionDbContext_OptionsCorrect.snap new file mode 100644 index 0000000..138f9fc --- /dev/null +++ b/src/Context.Tests/__snapshots__/MongoTransactionDbContextTests.StartNewTransactionAsync_CreateNewTransactionDbContext_OptionsCorrect.snap @@ -0,0 +1,23 @@ +{ + "MaxCommitTime": "00:01:00", + "ReadConcern": { + "IsServerDefault": false, + "Level": "Majority" + }, + "ReadPreference": { + "Hedge": null, + "MaxStaleness": null, + "ReadPreferenceMode": "Primary", + "TagSets": [] + }, + "WriteConcern": { + "FSync": null, + "IsAcknowledged": true, + "IsServerDefault": false, + "Journal": true, + "W": { + "Value": "majority" + }, + "WTimeout": null + } +} diff --git a/src/Context.Tests/__snapshots__/MongoTransactionDbContextTests.StartNewTransactionAsync_SetTransactionTransactionOptions_OptionsCorrect.snap b/src/Context.Tests/__snapshots__/MongoTransactionDbContextTests.StartNewTransactionAsync_SetTransactionTransactionOptions_OptionsCorrect.snap new file mode 100644 index 0000000..aad5bf9 --- /dev/null +++ b/src/Context.Tests/__snapshots__/MongoTransactionDbContextTests.StartNewTransactionAsync_SetTransactionTransactionOptions_OptionsCorrect.snap @@ -0,0 +1,23 @@ +{ + "MaxCommitTime": "00:05:00", + "ReadConcern": { + "IsServerDefault": false, + "Level": "Local" + }, + "ReadPreference": { + "Hedge": null, + "MaxStaleness": null, + "ReadPreferenceMode": "Secondary", + "TagSets": [] + }, + "WriteConcern": { + "FSync": null, + "IsAcknowledged": true, + "IsServerDefault": false, + "Journal": false, + "W": { + "Value": 3 + }, + "WTimeout": null + } +} diff --git a/src/Context.Tests/__snapshots__/MongoTransactionDbContextTests.StartNewTransactionAsync_TwoTransactionContextWithDifferentObjects_AllSaved.snap b/src/Context.Tests/__snapshots__/MongoTransactionDbContextTests.StartNewTransactionAsync_TwoTransactionContextWithDifferentObjects_AllSaved.snap new file mode 100644 index 0000000..6e9c350 --- /dev/null +++ b/src/Context.Tests/__snapshots__/MongoTransactionDbContextTests.StartNewTransactionAsync_TwoTransactionContextWithDifferentObjects_AllSaved.snap @@ -0,0 +1,27 @@ +[ + { + "Key": "Foo", + "Value": [ + [ + { + "Name": "_id", + "Value": 1 + }, + { + "Name": "FooName", + "Value": "Foo1a" + } + ], + [ + { + "Name": "_id", + "Value": 544656454 + }, + { + "Name": "FooName", + "Value": "Foo1b" + } + ] + ] + } +] diff --git a/src/Context.Tests/__snapshots__/MongoTransactionDbContextTests.StartNewTransactionAsync_TwoTransactionContextWithSameObjects_ConcurrencyExceptionAndSaved.snap b/src/Context.Tests/__snapshots__/MongoTransactionDbContextTests.StartNewTransactionAsync_TwoTransactionContextWithSameObjects_ConcurrencyExceptionAndSaved.snap new file mode 100644 index 0000000..821719f --- /dev/null +++ b/src/Context.Tests/__snapshots__/MongoTransactionDbContextTests.StartNewTransactionAsync_TwoTransactionContextWithSameObjects_ConcurrencyExceptionAndSaved.snap @@ -0,0 +1,17 @@ +[ + { + "Key": "Foo", + "Value": [ + [ + { + "Name": "_id", + "Value": 1 + }, + { + "Name": "FooName", + "Value": "Foo1a" + } + ] + ] + } +] diff --git a/src/Context/Context.csproj b/src/Context/Context.csproj index b62c316..e720159 100644 --- a/src/Context/Context.csproj +++ b/src/Context/Context.csproj @@ -42,4 +42,8 @@ + + + + diff --git a/src/Context/DefaultDefinitions.cs b/src/Context/DefaultDefinitions.cs new file mode 100644 index 0000000..c722861 --- /dev/null +++ b/src/Context/DefaultDefinitions.cs @@ -0,0 +1,15 @@ +using System; +using MongoDB.Driver; + +namespace MongoDB.Extensions.Context +{ + public static class DefaultDefinitions + { + public static readonly TransactionOptions DefaultTransactionOptions = + new TransactionOptions( + ReadConcern.Majority, + ReadPreference.Primary, + WriteConcern.WMajority.With(journal: true), + TimeSpan.FromSeconds(60)); + } +} diff --git a/src/Context/IMongoDbTransaction.cs b/src/Context/IMongoDbTransaction.cs new file mode 100644 index 0000000..0575b48 --- /dev/null +++ b/src/Context/IMongoDbTransaction.cs @@ -0,0 +1,13 @@ +using System.Threading.Tasks; +using System.Threading; +using MongoDB.Driver; + +namespace MongoDB.Extensions.Context +{ + public interface IMongoDbTransaction + { + Task StartNewTransactionAsync( + TransactionOptions? transactionOptions = null, + CancellationToken cancellationToken = default); + } +} diff --git a/src/Context/IMongoTransactionDbContext.cs b/src/Context/IMongoTransactionDbContext.cs new file mode 100644 index 0000000..dcfdb60 --- /dev/null +++ b/src/Context/IMongoTransactionDbContext.cs @@ -0,0 +1,16 @@ +using System; +using System.Threading.Tasks; +using System.Threading; +using MongoDB.Driver; + +namespace MongoDB.Extensions.Context; + +public interface IMongoTransactionDbContext : IMongoDbContext, IDisposable +{ + TransactionOptions TransactionOptions { get; } + + IMongoCollection GetCollection() where TDocument : class; + + Task CommitAsync(CancellationToken cancellationToken = default); + Task RollbackAsync(CancellationToken cancellationToken = default); +} diff --git a/src/Context/Internal/MongoDbContextData.cs b/src/Context/Internal/MongoDbContextData.cs index f72c395..c164303 100644 --- a/src/Context/Internal/MongoDbContextData.cs +++ b/src/Context/Internal/MongoDbContextData.cs @@ -37,9 +37,13 @@ private IMongoCollection GetConfiguredCollection() { lock (_lockObject) { + configuredCollection = + _mongoCollections.TryGetCollection(); + if (configuredCollection == null) { - return AddDefaultCollection(); + configuredCollection = + AddDefaultCollection(); } } } diff --git a/src/Context/MongoDbContext.cs b/src/Context/MongoDbContext.cs index 2e6e90d..056542a 100644 --- a/src/Context/MongoDbContext.cs +++ b/src/Context/MongoDbContext.cs @@ -1,86 +1,102 @@ using System; +using System.Threading; +using System.Threading.Tasks; using MongoDB.Driver; -namespace MongoDB.Extensions.Context -{ - public abstract class MongoDbContext : IMongoDbContext - { - private MongoDbContextData? _mongoDbContextData; +namespace MongoDB.Extensions.Context; - private readonly object _lockObject = new object(); +public abstract class MongoDbContext : IMongoDbContext, IMongoDbTransaction +{ + private MongoDbContextData? _mongoDbContextData; - public MongoDbContext(MongoOptions mongoOptions) : this(mongoOptions, true) - { - } + private readonly object _lockObject = new object(); - public MongoDbContext(MongoOptions mongoOptions, bool enableAutoInitialize) - { - MongoOptions = mongoOptions.Validate(); + public MongoDbContext(MongoOptions mongoOptions) : this(mongoOptions, true) + { + } - if (enableAutoInitialize) - { - Initialize(); - } - } - public MongoOptions MongoOptions { get; } + public MongoDbContext(MongoOptions mongoOptions, bool enableAutoInitialize) + { + MongoOptions = mongoOptions.Validate(); - public IMongoClient Client + if (enableAutoInitialize) { - get - { - EnsureInitialized(); - return _mongoDbContextData!.Client; - } + Initialize(); } + } + public MongoOptions MongoOptions { get; } - public IMongoDatabase Database + public IMongoClient Client + { + get { - get - { - EnsureInitialized(); - return _mongoDbContextData!.Database; - } + EnsureInitialized(); + return _mongoDbContextData!.Client; } - - public IMongoCollection CreateCollection() - where TDocument : class + } + + public IMongoDatabase Database + { + get { EnsureInitialized(); - return _mongoDbContextData!.GetCollection(); + return _mongoDbContextData!.Database; } + } + + public IMongoCollection CreateCollection() + where TDocument : class + { + EnsureInitialized(); + return _mongoDbContextData!.GetCollection(); + } - protected abstract void OnConfiguring(IMongoDatabaseBuilder mongoDatabaseBuilder); - - public virtual void Initialize() + protected abstract void OnConfiguring(IMongoDatabaseBuilder mongoDatabaseBuilder); + + public virtual void Initialize() + { + if(_mongoDbContextData == null) { - if(_mongoDbContextData == null) + lock (_lockObject) { - lock (_lockObject) + if (_mongoDbContextData == null) { - if (_mongoDbContextData == null) - { - var mongoDatabaseBuilder = new MongoDatabaseBuilder(MongoOptions); + var mongoDatabaseBuilder = new MongoDatabaseBuilder(MongoOptions); - OnConfiguring(mongoDatabaseBuilder); + OnConfiguring(mongoDatabaseBuilder); - _mongoDbContextData = mongoDatabaseBuilder.Build(); - } + _mongoDbContextData = mongoDatabaseBuilder.Build(); } } } + } - private void EnsureInitialized() + private void EnsureInitialized() + { + if (_mongoDbContextData == null) { - if (_mongoDbContextData == null) + lock (_lockObject) { - lock (_lockObject) + if (_mongoDbContextData == null) { - if (_mongoDbContextData == null) - { - throw new InvalidOperationException("MongoDbContext not initialized."); - } + throw new InvalidOperationException("MongoDbContext not initialized."); } } } } + + public async Task StartNewTransactionAsync( + TransactionOptions? transactionOptions = null, + CancellationToken cancellationToken = default) + { + transactionOptions ??= DefaultDefinitions.DefaultTransactionOptions; + + IClientSessionHandle clientSession = await Client + .StartSessionAsync(cancellationToken: cancellationToken); + + clientSession.StartTransaction(transactionOptions); + + return new MongoTransactionDbContext( + clientSession, transactionOptions, this); + } } diff --git a/src/Context/MongoTransactionDbContext.cs b/src/Context/MongoTransactionDbContext.cs new file mode 100644 index 0000000..800a5de --- /dev/null +++ b/src/Context/MongoTransactionDbContext.cs @@ -0,0 +1,112 @@ +using System; +using System.Threading.Tasks; +using System.Threading; +using MongoDB.Driver; +using System.Collections.Concurrent; +using MongoDB.Extensions.Transactions; +using MongoDB.Extensions.Context.Internal; + +namespace MongoDB.Extensions.Context; + +public class MongoTransactionDbContext : IMongoTransactionDbContext +{ + private readonly MongoDbContext _mongoDbContext; + private readonly MongoCollections _mongoCollections; + private readonly object _lockObject = new object(); + + public MongoTransactionDbContext( + IClientSessionHandle clientSession, + TransactionOptions transactionOptions, + MongoDbContext mongoDbContext) + { + _mongoDbContext = mongoDbContext; + _mongoCollections = new MongoCollections(); + + ClientSession = clientSession; + TransactionOptions = transactionOptions; + MongoOptions = mongoDbContext.MongoOptions; + Client = mongoDbContext.Client.AsTransactionClient(clientSession); + Database = mongoDbContext.Database.AsTransactionDatabase(clientSession); + } + + public TransactionOptions TransactionOptions { get; } + + public MongoOptions MongoOptions { get; } + + public IMongoClient Client { get; } + + public IMongoDatabase Database { get; } + + public IClientSessionHandle ClientSession { get; } + + public IMongoCollection GetCollection() + where TDocument : class => CreateCollection(); + + public IMongoCollection CreateCollection() + where TDocument : class + { + IMongoCollection? collection = + _mongoCollections.TryGetCollection(); + + if (collection is { }) + { + return collection; + } + + lock(_lockObject) + { + collection = _mongoCollections + .TryGetCollection(); + + if (collection is { }) + { + return collection; + } + + collection = _mongoDbContext + .CreateCollection() + .AsTransactionCollection(ClientSession); + + _mongoCollections.Add(collection); + } + + return collection; + } + + public async Task CommitAsync(CancellationToken cancellationToken = default) + { + await ClientSession + .CommitTransactionAsync(cancellationToken); + } + + public async Task RollbackAsync(CancellationToken cancellationToken = default) + { + await ClientSession + .AbortTransactionAsync(cancellationToken); + } + + #region IDisposable + + private bool _disposed; + + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + ClientSession.Dispose(); + } + + _disposed = true; + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + #endregion +} diff --git a/src/Prime.Extensions/ClientSessionHandleExtensions.cs b/src/Prime.Extensions/ClientSessionHandleExtensions.cs new file mode 100644 index 0000000..7061ccc --- /dev/null +++ b/src/Prime.Extensions/ClientSessionHandleExtensions.cs @@ -0,0 +1,12 @@ +using System; +using MongoDB.Driver; + +namespace MongoDB.Prime.Extensions; + +public static class ClientSessionHandleExtensions +{ + public static Guid GetSessionId(this IClientSessionHandle clientSessionHandle) + { + return clientSessionHandle.ServerSession.Id["id"].AsGuid; + } +} diff --git a/src/Prime.Extensions/MongoCollectionExtensions.cs b/src/Prime.Extensions/MongoCollectionExtensions.cs index 12c2917..73fee20 100644 --- a/src/Prime.Extensions/MongoCollectionExtensions.cs +++ b/src/Prime.Extensions/MongoCollectionExtensions.cs @@ -62,6 +62,23 @@ public static Task CountDocumentsAsync( FilterDefinition.Empty, options, cancellationToken); } + /// + /// Reads all entries of a collection and returns a list of it. + /// + /// The type of the document. + /// The collection to dump. + public static IEnumerable Dump( + this IMongoCollection collection) + { + SortDefinition sortDefinition = + Builders.Sort.Ascending("_id"); + + return collection + .Find(FilterDefinition.Empty) + .Sort(sortDefinition) + .ToList(); + } + public static Task InsertOneAsync( this IMongoCollection collection, TDocument document, diff --git a/src/Prime.Extensions/MongoDatabaseExtensions.cs b/src/Prime.Extensions/MongoDatabaseExtensions.cs index 2c1445f..c51afda 100644 --- a/src/Prime.Extensions/MongoDatabaseExtensions.cs +++ b/src/Prime.Extensions/MongoDatabaseExtensions.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using System.Linq; -using System.Runtime.CompilerServices; using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -8,120 +7,142 @@ using MongoDB.Bson.IO; using MongoDB.Driver; -namespace MongoDB.Prime.Extensions +namespace MongoDB.Prime.Extensions; + +public static class MongoDatabaseExtensions { - public static class MongoDatabaseExtensions + public static void EnableProfiling( + this IMongoDatabase mongoDatabase, + ProfileLevel profileLevel = ProfileLevel.All) { - public static void EnableProfiling( - this IMongoDatabase mongoDatabase, - ProfileLevel profileLevel = ProfileLevel.All) - { - var profileCommand = new BsonDocument("profile", (int)profileLevel); + var profileCommand = new BsonDocument("profile", (int)profileLevel); - mongoDatabase.RunCommand(profileCommand); - } + mongoDatabase.RunCommand(profileCommand); + } - public static ProfilingStatus GetProfilingStatus( - this IMongoDatabase mongoDatabase) - { - var profileStatusCommand = new BsonDocument("profile", -1); + public static ProfilingStatus GetProfilingStatus( + this IMongoDatabase mongoDatabase) + { + var profileStatusCommand = new BsonDocument("profile", -1); - BsonDocument profileBsonDocument = - mongoDatabase.RunCommand(profileStatusCommand); + BsonDocument profileBsonDocument = + mongoDatabase.RunCommand(profileStatusCommand); - return CreateProfilingStatus(profileBsonDocument); - } + return CreateProfilingStatus(profileBsonDocument); + } - private static ProfilingStatus CreateProfilingStatus( - BsonDocument profileBsonDocument) - { - return new ProfilingStatus( - level: (ProfileLevel)profileBsonDocument["was"].AsInt32, - slowMs: profileBsonDocument["slowms"].AsInt32, - sampleRate: profileBsonDocument["sampleRate"].AsDouble, - filter: profileBsonDocument["ok"].AsDouble.ToString()); - } + private static ProfilingStatus CreateProfilingStatus( + BsonDocument profileBsonDocument) + { + return new ProfilingStatus( + level: (ProfileLevel)profileBsonDocument["was"].AsInt32, + slowMs: profileBsonDocument["slowms"].AsInt32, + sampleRate: profileBsonDocument["sampleRate"].AsDouble, + filter: profileBsonDocument["ok"].AsDouble.ToString()); + } - public static IEnumerable GetProfiledOperations( - this IMongoDatabase mongoDatabase) - { - IMongoCollection collection = mongoDatabase - .GetCollection("system.profile"); - - List docs = collection - .Find(new BsonDocument()) - .ToList(); - - IEnumerable jsons = docs.Select(bson => bson.ToJson( - new JsonWriterSettings - { - OutputMode = JsonOutputMode.RelaxedExtendedJson - })); - - IEnumerable normalizedJson = jsons - .Select(json => JsonSerializer - .Serialize( - JsonDocument.Parse(json).RootElement, - new JsonSerializerOptions() - { - WriteIndented = true - })); - - return normalizedJson; - } + public static IEnumerable GetProfiledOperations( + this IMongoDatabase mongoDatabase) + { + IMongoCollection collection = mongoDatabase + .GetCollection("system.profile"); + + List docs = collection + .Find(new BsonDocument()) + .ToList(); + + IEnumerable jsons = docs.Select(bson => bson.ToJson( + new JsonWriterSettings + { + OutputMode = JsonOutputMode.RelaxedExtendedJson + })); + + IEnumerable normalizedJson = jsons + .Select(json => JsonSerializer + .Serialize( + JsonDocument.Parse(json).RootElement, + new JsonSerializerOptions() + { + WriteIndented = true + })); + + return normalizedJson; + } + + /// + /// Gets the collection by Type name. The name of the collection + /// will be the typeof(TDocument).Name. + /// + /// The type of document. + /// The mongo database. + /// The mongo collection settings. + /// The mongo collection with the name of the document type. + public static IMongoCollection GetCollection( + this IMongoDatabase mongoDatabase, + MongoCollectionSettings? settings = null) + { + return mongoDatabase + .GetCollection(typeof(TDocument).Name, settings); + } - /// - /// Gets the collection by Type name. The name of the collection - /// will be the typeof(TDocument).Name. - /// - /// The type of document. - /// The mongo database. - /// The mongo collection settings. - /// The mongo collection with the name of the document type. - public static IMongoCollection GetCollection( - this IMongoDatabase mongoDatabase, - MongoCollectionSettings? settings = null) + /// + /// Deletes all entries of every collection of the mongo database. + /// The collections will NOT be dropped and the indexes stay unmodified. + /// + /// The database to clean the collections. + public static void CleanAllCollections( + this IMongoDatabase mongoDatabase) + { + foreach (var name in mongoDatabase.ListCollectionNames().ToList()) { - return mongoDatabase - .GetCollection(typeof(TDocument).Name, settings); + IMongoCollection collection = + mongoDatabase.GetCollection(name); + + collection.CleanCollection(); } + } - /// - /// Deletes all entries of every collection of the mongo database. - /// The collections will NOT be dropped and the indexes stay unmodified. - /// - /// The database to clean the collections. - public static void CleanAllCollections( - this IMongoDatabase mongoDatabase) + /// + /// Deletes all entries of every collection of the mongo database. + /// The collections will NOT be dropped and the indexes stay unmodified. + /// + /// The database to clean the collections. + public static async Task CleanAllCollectionsAsync( + this IMongoDatabase mongoDatabase, + CancellationToken cancellationToken = default) + { + IAsyncCursor cursor = await mongoDatabase + .ListCollectionNamesAsync(cancellationToken: cancellationToken); + + foreach (var name in await cursor.ToListAsync(cancellationToken)) { - foreach (var name in mongoDatabase.ListCollectionNames().ToList()) - { - IMongoCollection collection = - mongoDatabase.GetCollection(name); + IMongoCollection collection = + mongoDatabase.GetCollection(name); - collection.CleanCollection(); - } + await collection.CleanCollectionAsync(cancellationToken); } + } - /// - /// Deletes all entries of every collection of the mongo database. - /// The collections will NOT be dropped and the indexes stay unmodified. - /// - /// The database to clean the collections. - public static async Task CleanAllCollectionsAsync( - this IMongoDatabase mongoDatabase, - CancellationToken cancellationToken = default) - { - IAsyncCursor cursor = await mongoDatabase - .ListCollectionNamesAsync(cancellationToken: cancellationToken); + /// + /// Dumps all collections and returns it in a Dictionary. + /// + /// The database to dump all collections. + public static IEnumerable>> DumpAllCollections( + this IMongoDatabase mongoDatabase) + { + List>> dumpedCollections = + new List>>(); - foreach (var name in await cursor.ToListAsync(cancellationToken)) - { - IMongoCollection collection = - mongoDatabase.GetCollection(name); + foreach (var name in mongoDatabase.ListCollectionNames().ToList()) + { + IEnumerable dumpedCollection = + mongoDatabase.GetCollection(name).Dump(); - await collection.CleanCollectionAsync(cancellationToken); - } + dumpedCollections.Add( + new KeyValuePair>( + name, dumpedCollection)); } + + return dumpedCollections.OrderBy(entry => entry.Key); } } diff --git a/src/Session/ClientSessionHandleExtensions.cs b/src/Session/ClientSessionHandleExtensions.cs index d996b47..bf462f1 100644 --- a/src/Session/ClientSessionHandleExtensions.cs +++ b/src/Session/ClientSessionHandleExtensions.cs @@ -5,6 +5,7 @@ namespace MongoDB.Extensions.Session { public static class ClientSessionHandleExtensions { + [Obsolete("This method has been moved to MongoDB.Prime.Extensions")] public static Guid GetSessionId(this IClientSessionHandle clientSessionHandle) { return clientSessionHandle.ServerSession.Id["id"].AsGuid;