diff --git a/framework/src/Volo.Abp.MongoDB/Volo.Abp.MongoDB.csproj b/framework/src/Volo.Abp.MongoDB/Volo.Abp.MongoDB.csproj
index 2b9e46f43d..8d2942c145 100644
--- a/framework/src/Volo.Abp.MongoDB/Volo.Abp.MongoDB.csproj
+++ b/framework/src/Volo.Abp.MongoDB/Volo.Abp.MongoDB.csproj
@@ -15,7 +15,7 @@
-
+
diff --git a/framework/src/Volo.Abp.MongoDB/Volo/Abp/Domain/Repositories/MongoDB/MongoDbRepository.cs b/framework/src/Volo.Abp.MongoDB/Volo/Abp/Domain/Repositories/MongoDB/MongoDbRepository.cs
index 8676ac0f34..06ed98846d 100644
--- a/framework/src/Volo.Abp.MongoDB/Volo/Abp/Domain/Repositories/MongoDB/MongoDbRepository.cs
+++ b/framework/src/Volo.Abp.MongoDB/Volo/Abp/Domain/Repositories/MongoDB/MongoDbRepository.cs
@@ -29,6 +29,8 @@ namespace Volo.Abp.Domain.Repositories.MongoDB
public virtual IMongoDatabase Database => DbContext.Database;
+ public virtual IClientSessionHandle SessionHandle => DbContext.SessionHandle;
+
public virtual TMongoDbContext DbContext => DbContextProvider.GetDbContext();
protected IMongoDbContextProvider DbContextProvider { get; }
@@ -60,10 +62,21 @@ namespace Volo.Abp.Domain.Repositories.MongoDB
{
await ApplyAbpConceptsForAddedEntityAsync(entity);
- await Collection.InsertOneAsync(
- entity,
- cancellationToken: GetCancellationToken(cancellationToken)
- );
+ if (SessionHandle != null)
+ {
+ await Collection.InsertOneAsync(
+ SessionHandle,
+ entity,
+ cancellationToken: GetCancellationToken(cancellationToken)
+ );
+ }
+ else
+ {
+ await Collection.InsertOneAsync(
+ entity,
+ cancellationToken: GetCancellationToken(cancellationToken)
+ );
+ }
return entity;
}
@@ -88,12 +101,25 @@ namespace Volo.Abp.Domain.Repositories.MongoDB
await TriggerDomainEventsAsync(entity);
var oldConcurrencyStamp = SetNewConcurrencyStamp(entity);
+ ReplaceOneResult result;
- var result = await Collection.ReplaceOneAsync(
- CreateEntityFilter(entity, true, oldConcurrencyStamp),
- entity,
- cancellationToken: GetCancellationToken(cancellationToken)
- );
+ if (SessionHandle != null)
+ {
+ result = await Collection.ReplaceOneAsync(
+ SessionHandle,
+ CreateEntityFilter(entity, true, oldConcurrencyStamp),
+ entity,
+ cancellationToken: GetCancellationToken(cancellationToken)
+ );
+ }
+ else
+ {
+ result = await Collection.ReplaceOneAsync(
+ CreateEntityFilter(entity, true, oldConcurrencyStamp),
+ entity,
+ cancellationToken: GetCancellationToken(cancellationToken)
+ );
+ }
if (result.MatchedCount <= 0)
{
@@ -114,11 +140,25 @@ namespace Volo.Abp.Domain.Repositories.MongoDB
if (entity is ISoftDelete softDeleteEntity && !IsHardDeleted(entity))
{
softDeleteEntity.IsDeleted = true;
- var result = await Collection.ReplaceOneAsync(
- CreateEntityFilter(entity, true, oldConcurrencyStamp),
- entity,
- cancellationToken: GetCancellationToken(cancellationToken)
- );
+ ReplaceOneResult result;
+
+ if (SessionHandle != null)
+ {
+ result = await Collection.ReplaceOneAsync(
+ SessionHandle,
+ CreateEntityFilter(entity, true, oldConcurrencyStamp),
+ entity,
+ cancellationToken: GetCancellationToken(cancellationToken)
+ );
+ }
+ else
+ {
+ result = await Collection.ReplaceOneAsync(
+ CreateEntityFilter(entity, true, oldConcurrencyStamp),
+ entity,
+ cancellationToken: GetCancellationToken(cancellationToken)
+ );
+ }
if (result.MatchedCount <= 0)
{
@@ -127,10 +167,23 @@ namespace Volo.Abp.Domain.Repositories.MongoDB
}
else
{
- var result = await Collection.DeleteOneAsync(
- CreateEntityFilter(entity, true, oldConcurrencyStamp),
- GetCancellationToken(cancellationToken)
- );
+ DeleteResult result;
+
+ if (SessionHandle != null)
+ {
+ result = await Collection.DeleteOneAsync(
+ SessionHandle,
+ CreateEntityFilter(entity, true, oldConcurrencyStamp),
+ cancellationToken: GetCancellationToken(cancellationToken)
+ );
+ }
+ else
+ {
+ result = await Collection.DeleteOneAsync(
+ CreateEntityFilter(entity, true, oldConcurrencyStamp),
+ GetCancellationToken(cancellationToken)
+ );
+ }
if (result.DeletedCount <= 0)
{
@@ -404,6 +457,13 @@ namespace Volo.Abp.Domain.Repositories.MongoDB
bool includeDetails = true,
CancellationToken cancellationToken = default)
{
+ if (SessionHandle != null)
+ {
+ return await Collection
+ .Find(SessionHandle, RepositoryFilterer.CreateEntityFilter(id, true))
+ .FirstOrDefaultAsync(GetCancellationToken(cancellationToken));
+ }
+
return await Collection
.Find(RepositoryFilterer.CreateEntityFilter(id, true))
.FirstOrDefaultAsync(GetCancellationToken(cancellationToken));
diff --git a/framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/AbpMongoDbContext.cs b/framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/AbpMongoDbContext.cs
index bfa4b87da1..0dea1710d9 100644
--- a/framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/AbpMongoDbContext.cs
+++ b/framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/AbpMongoDbContext.cs
@@ -1,4 +1,5 @@
using System.Collections.Generic;
+using MongoDB.Bson;
using MongoDB.Driver;
using Volo.Abp.DependencyInjection;
@@ -10,18 +11,23 @@ namespace Volo.Abp.MongoDB
public IMongoDatabase Database { get; private set; }
+ public IClientSessionHandle SessionHandle { get; private set; }
+
protected internal virtual void CreateModel(IMongoModelBuilder modelBuilder)
{
}
- public virtual void InitializeDatabase(IMongoDatabase database)
+ public virtual void InitializeDatabase(IMongoDatabase database, IClientSessionHandle sessionHandle)
{
Database = database;
+ SessionHandle = sessionHandle;
}
public virtual IMongoCollection Collection()
{
+ CreateCollectionIfNotExists();
+
return Database.GetCollection(GetCollectionName());
}
@@ -41,5 +47,23 @@ namespace Volo.Abp.MongoDB
return model;
}
+
+ protected virtual void CreateCollectionIfNotExists()
+ {
+ var collectionName = GetCollectionName();
+
+ if (!CollectionExists(collectionName))
+ {
+ Database.CreateCollection(collectionName);
+ }
+ }
+
+ protected virtual bool CollectionExists(string collectionName)
+ {
+ var filter = new BsonDocument("name", collectionName);
+ var options = new ListCollectionNamesOptions { Filter = filter };
+
+ return Database.ListCollectionNames(options).Any();
+ }
}
-}
\ No newline at end of file
+}
diff --git a/framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/IAbpMongoDbContext.cs b/framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/IAbpMongoDbContext.cs
index a2f48fd323..c4049e1389 100644
--- a/framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/IAbpMongoDbContext.cs
+++ b/framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/IAbpMongoDbContext.cs
@@ -7,5 +7,7 @@ namespace Volo.Abp.MongoDB
IMongoDatabase Database { get; }
IMongoCollection Collection();
+
+ IClientSessionHandle SessionHandle { get; }
}
-}
\ No newline at end of file
+}
diff --git a/framework/src/Volo.Abp.MongoDB/Volo/Abp/Uow/MongoDB/MongoDbTransactionApi.cs b/framework/src/Volo.Abp.MongoDB/Volo/Abp/Uow/MongoDB/MongoDbTransactionApi.cs
new file mode 100644
index 0000000000..6c3ab76ee5
--- /dev/null
+++ b/framework/src/Volo.Abp.MongoDB/Volo/Abp/Uow/MongoDB/MongoDbTransactionApi.cs
@@ -0,0 +1,42 @@
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using MongoDB.Driver;
+
+namespace Volo.Abp.Uow.MongoDB
+{
+ public class MongoDbTransactionApi : ITransactionApi, ISupportsRollback
+ {
+ public IClientSessionHandle SessionHandle { get; }
+
+ public MongoDbTransactionApi(IClientSessionHandle sessionHandle)
+ {
+ SessionHandle = sessionHandle;
+ }
+
+ public async Task CommitAsync()
+ {
+ await SessionHandle.CommitTransactionAsync();
+ }
+
+ protected void Commit()
+ {
+ SessionHandle.CommitTransaction();
+ }
+
+ public void Dispose()
+ {
+ SessionHandle.Dispose();
+ }
+
+ public void Rollback()
+ {
+ SessionHandle.AbortTransaction();
+ }
+
+ public async Task RollbackAsync(CancellationToken cancellationToken)
+ {
+ await SessionHandle.AbortTransactionAsync(cancellationToken);
+ }
+ }
+}
diff --git a/framework/src/Volo.Abp.MongoDB/Volo/Abp/Uow/MongoDB/UnitOfWorkMongoDbContextProvider.cs b/framework/src/Volo.Abp.MongoDB/Volo/Abp/Uow/MongoDB/UnitOfWorkMongoDbContextProvider.cs
index 0d527efc4c..893d80488d 100644
--- a/framework/src/Volo.Abp.MongoDB/Volo/Abp/Uow/MongoDB/UnitOfWorkMongoDbContextProvider.cs
+++ b/framework/src/Volo.Abp.MongoDB/Volo/Abp/Uow/MongoDB/UnitOfWorkMongoDbContextProvider.cs
@@ -1,5 +1,6 @@
using System;
using Microsoft.Extensions.DependencyInjection;
+using MongoDB.Bson;
using MongoDB.Driver;
using Volo.Abp.Data;
using Volo.Abp.MongoDB;
@@ -25,7 +26,8 @@ namespace Volo.Abp.Uow.MongoDB
var unitOfWork = _unitOfWorkManager.Current;
if (unitOfWork == null)
{
- throw new AbpException($"A {nameof(IMongoDatabase)} instance can only be created inside a unit of work!");
+ throw new AbpException(
+ $"A {nameof(IMongoDatabase)} instance can only be created inside a unit of work!");
}
var connectionString = _connectionStringResolver.Resolve();
@@ -41,18 +43,42 @@ namespace Volo.Abp.Uow.MongoDB
//TODO: Create only single MongoDbClient per connection string in an application (extract MongoClientCache for example).
var databaseApi = unitOfWork.GetOrAddDatabaseApi(
dbContextKey,
- () =>
+ () => new MongoDbDatabaseApi(CreateDbContext(unitOfWork,mongoUrl,databaseName)));
+
+ return ((MongoDbDatabaseApi) databaseApi).DbContext;
+ }
+
+ private TMongoDbContext CreateDbContext(IUnitOfWork unitOfWork, MongoUrl mongoUrl, string databaseName)
+ {
+ var client = new MongoClient(mongoUrl);
+ var database = client.GetDatabase(databaseName);
+ var dbContext = unitOfWork.ServiceProvider.GetRequiredService();
+
+ if (unitOfWork.Options.IsTransactional)
+ {
+ var session = client.StartSession();
+
+ if (unitOfWork.Options.Timeout.HasValue)
{
- var database = new MongoClient(mongoUrl).GetDatabase(databaseName);
+ session.AdvanceOperationTime(new BsonTimestamp(unitOfWork.Options.Timeout.Value));
+ }
- var dbContext = unitOfWork.ServiceProvider.GetRequiredService();
+ session.StartTransaction();
- dbContext.ToAbpMongoDbContext().InitializeDatabase(database);
+ var transactionApiKey = $"MongoDb_{mongoUrl}";
+ unitOfWork.AddTransactionApi(
+ transactionApiKey,
+ new MongoDbTransactionApi(session)
+ );
- return new MongoDbDatabaseApi(dbContext);
- });
+ dbContext.ToAbpMongoDbContext().InitializeDatabase(database, session);
+ }
+ else
+ {
+ dbContext.ToAbpMongoDbContext().InitializeDatabase(database, null);
+ }
- return ((MongoDbDatabaseApi)databaseApi).DbContext;
+ return dbContext;
}
}
-}
\ No newline at end of file
+}
diff --git a/framework/test/Volo.Abp.MongoDB.Tests/Volo/Abp/MongoDB/AbpMongoDbTestModule.cs b/framework/test/Volo.Abp.MongoDB.Tests/Volo/Abp/MongoDB/AbpMongoDbTestModule.cs
index cfa7b033b7..f2b04a3a7f 100644
--- a/framework/test/Volo.Abp.MongoDB.Tests/Volo/Abp/MongoDB/AbpMongoDbTestModule.cs
+++ b/framework/test/Volo.Abp.MongoDB.Tests/Volo/Abp/MongoDB/AbpMongoDbTestModule.cs
@@ -16,9 +16,11 @@ namespace Volo.Abp.MongoDB
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
- var connectionString = MongoDbFixture.ConnectionString.EnsureEndsWith('/') +
+ var stringArray = MongoDbFixture.ConnectionString.Split('?');
+
+ var connectionString = stringArray[0].EnsureEndsWith('/') +
"Db_" +
- Guid.NewGuid().ToString("N");
+ Guid.NewGuid().ToString("N") + "/?" + stringArray[1];
Configure(options =>
{
diff --git a/framework/test/Volo.Abp.MongoDB.Tests/Volo/Abp/MongoDB/MongoClientExtension.cs b/framework/test/Volo.Abp.MongoDB.Tests/Volo/Abp/MongoDB/MongoClientExtension.cs
new file mode 100644
index 0000000000..8b7508dabf
--- /dev/null
+++ b/framework/test/Volo.Abp.MongoDB.Tests/Volo/Abp/MongoDB/MongoClientExtension.cs
@@ -0,0 +1,84 @@
+using System;
+using System.Threading;
+using MongoDB.Driver;
+
+namespace Volo.Abp.MongoDB
+{
+ ///
+ /// TODO It can be removed, when Mongo2Go solves this issue : https://github.com/Mongo2Go/Mongo2Go/issues/89
+ ///
+ internal static class MongoClientExtension
+ {
+ private static readonly TimeSpan InitialDelay = TimeSpan.FromMilliseconds(500);
+ private static readonly TimeSpan MaxDelay = TimeSpan.FromSeconds(5000);
+
+
+ public static void EnsureReplicationSetReady(this IMongoClient mongoClient)
+ {
+ var delay = InitialDelay;
+ var database = mongoClient.GetDatabase("__dummy-db");
+ try
+ {
+ while (true)
+ {
+ try
+ {
+ _ = database.GetCollection("__dummy");
+ database.DropCollection("__dummy");
+
+ var session = mongoClient.StartSession();
+
+ try
+ {
+ session.StartTransaction();
+ session.AbortTransaction();
+ }
+ finally
+ {
+ session.Dispose();
+ }
+ break;
+ }
+ catch (NotSupportedException) { }
+
+ Thread.Sleep(delay);
+ delay = Min(Double(delay), MaxDelay);
+ }
+ }
+ finally
+ {
+ mongoClient.DropDatabase("__dummy-db");
+ }
+ }
+
+ private static TimeSpan Min(TimeSpan left, TimeSpan right)
+ {
+ return new TimeSpan(Math.Min(left.Ticks, right.Ticks));
+ }
+
+ private static TimeSpan Double(TimeSpan timeSpan)
+ {
+ long ticks;
+ try
+ {
+ ticks = checked(timeSpan.Ticks * 2);
+ }
+ catch (OverflowException)
+ {
+ if (timeSpan.Ticks >= 0)
+ {
+ return TimeSpan.MaxValue;
+ }
+
+ return TimeSpan.MinValue;
+ }
+
+ return new TimeSpan(ticks);
+ }
+
+ private sealed class DummyEntry
+ {
+ public int Id { get; set; }
+ }
+ }
+}
diff --git a/framework/test/Volo.Abp.MongoDB.Tests/Volo/Abp/MongoDB/MongoDbFixture.cs b/framework/test/Volo.Abp.MongoDB.Tests/Volo/Abp/MongoDB/MongoDbFixture.cs
index 3fa239ebbb..dbb872bee6 100644
--- a/framework/test/Volo.Abp.MongoDB.Tests/Volo/Abp/MongoDB/MongoDbFixture.cs
+++ b/framework/test/Volo.Abp.MongoDB.Tests/Volo/Abp/MongoDB/MongoDbFixture.cs
@@ -1,5 +1,6 @@
using System;
using Mongo2Go;
+using MongoDB.Driver;
namespace Volo.Abp.MongoDB
{
@@ -10,8 +11,12 @@ namespace Volo.Abp.MongoDB
static MongoDbFixture()
{
- MongoDbRunner = MongoDbRunner.Start();
+ MongoDbRunner = MongoDbRunner.Start(singleNodeReplSet: true, singleNodeReplSetWaitTimeout: 10);
ConnectionString = MongoDbRunner.ConnectionString;
+
+ //TODO It can be removed, when Mongo2Go solves this issue : https://github.com/Mongo2Go/Mongo2Go/issues/89
+ var client = new MongoClient(MongoDbRunner.ConnectionString);
+ client.EnsureReplicationSetReady();
}
public void Dispose()
diff --git a/framework/test/Volo.Abp.MongoDB.Tests/Volo/Abp/MongoDB/Transactions/Transaction_Tests.cs b/framework/test/Volo.Abp.MongoDB.Tests/Volo/Abp/MongoDB/Transactions/Transaction_Tests.cs
new file mode 100644
index 0000000000..6c82b44f93
--- /dev/null
+++ b/framework/test/Volo.Abp.MongoDB.Tests/Volo/Abp/MongoDB/Transactions/Transaction_Tests.cs
@@ -0,0 +1,95 @@
+using System;
+using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
+using Shouldly;
+using Volo.Abp.Domain.Repositories;
+using Volo.Abp.TestApp.Domain;
+using Volo.Abp.TestApp.Testing;
+using Volo.Abp.Uow;
+using Xunit;
+
+namespace Volo.Abp.MongoDB.Transactions
+{
+ public class Transaction_Tests : TestAppTestBase
+ {
+
+ private readonly IBasicRepository _personRepository;
+ private readonly IBasicRepository _cityRepository;
+ private readonly IUnitOfWorkManager _unitOfWorkManager;
+
+ public Transaction_Tests()
+ {
+ _personRepository = ServiceProvider.GetRequiredService>();
+ _cityRepository = ServiceProvider.GetRequiredService>();
+ _unitOfWorkManager = ServiceProvider.GetRequiredService();
+ }
+
+ [Fact]
+ public async Task Should_Rollback_Transaction_When_An_Exception_Is_Thrown()
+ {
+ var personId = Guid.NewGuid();
+ const string exceptionMessage = "thrown to rollback the transaction!";
+
+ try
+ {
+ await WithUnitOfWorkAsync(new AbpUnitOfWorkOptions {IsTransactional = true}, async () =>
+ {
+ await _personRepository.InsertAsync(new Person(personId, "Adam", 42));
+ throw new Exception(exceptionMessage);
+ });
+ }
+ catch (Exception e) when (e.Message == exceptionMessage)
+ {
+
+ }
+
+ var person = await _personRepository.FindAsync(personId);
+ person.ShouldBeNull();
+ }
+
+ [Fact]
+ public async Task Should_Rollback_Transaction_Manually()
+ {
+ var personId = Guid.NewGuid();
+
+ await WithUnitOfWorkAsync(new AbpUnitOfWorkOptions {IsTransactional = true}, async () =>
+ {
+ _unitOfWorkManager.Current.ShouldNotBeNull();
+
+ await _personRepository.InsertAsync(new Person(personId, "Adam", 42));
+
+ await _unitOfWorkManager.Current.RollbackAsync();
+ });
+
+ var person = await _personRepository.FindAsync(personId);
+ person.ShouldBeNull();
+ }
+
+ [Fact]
+ public async Task Should_Rollback_Transaction_Manually_With_Double_DbContext_Transaction()
+ {
+ var personId = Guid.NewGuid();
+ var cityId = Guid.NewGuid();
+
+ using (var scope = ServiceProvider.CreateScope())
+ {
+ var uowManager = scope.ServiceProvider.GetRequiredService();
+
+ using (uowManager.Begin(new AbpUnitOfWorkOptions {IsTransactional = true}))
+ {
+ _unitOfWorkManager.Current.ShouldNotBeNull();
+
+ await _personRepository.InsertAsync(new Person(personId, "Adam", 42));
+ await _cityRepository.InsertAsync(new City(cityId, cityId.ToString()));
+
+ await _unitOfWorkManager.Current.SaveChangesAsync();
+
+ //Will automatically rollback since not called the Complete!
+ }
+ }
+
+ (await _personRepository.FindAsync(personId)).ShouldBeNull();
+ (await _cityRepository.FindAsync(cityId)).ShouldBeNull();
+ }
+ }
+}