diff --git a/Backend.Tests/Controllers/MergeControllerTests.cs b/Backend.Tests/Controllers/MergeControllerTests.cs index 85e797f7e2..26864e52cb 100644 --- a/Backend.Tests/Controllers/MergeControllerTests.cs +++ b/Backend.Tests/Controllers/MergeControllerTests.cs @@ -11,6 +11,7 @@ namespace Backend.Tests.Controllers { public class MergeControllerTests : IDisposable { + private IMongoDbContext _mongoDbContext = null!; private IMergeBlacklistRepository _mergeBlacklistRepo = null!; private IMergeGraylistRepository _mergeGraylistRepo = null!; private IWordRepository _wordRepo = null!; @@ -38,11 +39,13 @@ protected virtual void Dispose(bool disposing) [SetUp] public void Setup() { + _mongoDbContext = new MongoDbContextMock(); _mergeBlacklistRepo = new MergeBlacklistRepositoryMock(); _mergeGraylistRepo = new MergeGraylistRepositoryMock(); _wordRepo = new WordRepositoryMock(); _wordService = new WordService(_wordRepo); - _mergeService = new MergeService(_mergeBlacklistRepo, _mergeGraylistRepo, _wordRepo, _wordService); + _mergeService = new MergeService( + _mongoDbContext, _mergeBlacklistRepo, _mergeGraylistRepo, _wordRepo, _wordService); _permissionService = new PermissionServiceMock(); _mergeController = new MergeController(_mergeService, _permissionService); } diff --git a/Backend.Tests/Mocks/MongoDbContextMock.cs b/Backend.Tests/Mocks/MongoDbContextMock.cs new file mode 100644 index 0000000000..cbc3b028a7 --- /dev/null +++ b/Backend.Tests/Mocks/MongoDbContextMock.cs @@ -0,0 +1,32 @@ +using System; +using System.Threading.Tasks; +using BackendFramework.Interfaces; +using MongoDB.Driver; + +namespace Backend.Tests.Mocks; + +public class MongoDbContextMock : IMongoDbContext +{ + public IMongoDatabase Db => throw new NotSupportedException(); + public Task BeginTransaction() + { + return Task.FromResult(new MongoTransactionMock()); + } + + private sealed class MongoTransactionMock : IMongoTransaction + { + public Task CommitTransactionAsync() + { + return Task.CompletedTask; + } + + public Task AbortTransactionAsync() + { + return Task.CompletedTask; + } + + public void Dispose() + { + } + } +} diff --git a/Backend.Tests/Services/MergeServiceTests.cs b/Backend.Tests/Services/MergeServiceTests.cs index ab25ee2739..72bcbb5cba 100644 --- a/Backend.Tests/Services/MergeServiceTests.cs +++ b/Backend.Tests/Services/MergeServiceTests.cs @@ -10,6 +10,7 @@ namespace Backend.Tests.Services { public class MergeServiceTests { + private IMongoDbContext _mongoDbContext = null!; private IMergeBlacklistRepository _mergeBlacklistRepo = null!; private IMergeGraylistRepository _mergeGraylistRepo = null!; private IWordRepository _wordRepo = null!; @@ -22,11 +23,13 @@ public class MergeServiceTests [SetUp] public void Setup() { + _mongoDbContext = new MongoDbContextMock(); _mergeBlacklistRepo = new MergeBlacklistRepositoryMock(); _mergeGraylistRepo = new MergeGraylistRepositoryMock(); _wordRepo = new WordRepositoryMock(); _wordService = new WordService(_wordRepo); - _mergeService = new MergeService(_mergeBlacklistRepo, _mergeGraylistRepo, _wordRepo, _wordService); + _mergeService = new MergeService( + _mongoDbContext, _mergeBlacklistRepo, _mergeGraylistRepo, _wordRepo, _wordService); } [Test] diff --git a/Backend/Contexts/BannerContext.cs b/Backend/Contexts/BannerContext.cs index ace2284f9f..15d123d25b 100644 --- a/Backend/Contexts/BannerContext.cs +++ b/Backend/Contexts/BannerContext.cs @@ -1,7 +1,6 @@ using System.Diagnostics.CodeAnalysis; using BackendFramework.Interfaces; using BackendFramework.Models; -using Microsoft.Extensions.Options; using MongoDB.Driver; namespace BackendFramework.Contexts @@ -9,14 +8,13 @@ namespace BackendFramework.Contexts [ExcludeFromCodeCoverage] public class BannerContext : IBannerContext { - private readonly IMongoDatabase _db; + private readonly IMongoDbContext _mongoDbContext; - public BannerContext(IOptions options) + public BannerContext(IMongoDbContext mongoDbContext) { - var client = new MongoClient(options.Value.ConnectionString); - _db = client.GetDatabase(options.Value.CombineDatabase); + _mongoDbContext = mongoDbContext; } - public IMongoCollection Banners => _db.GetCollection("BannerCollection"); + public IMongoCollection Banners => _mongoDbContext.Db.GetCollection("BannerCollection"); } } diff --git a/Backend/Contexts/MergeBlacklistContext.cs b/Backend/Contexts/MergeBlacklistContext.cs index f977bf6d70..52c1c65f1c 100644 --- a/Backend/Contexts/MergeBlacklistContext.cs +++ b/Backend/Contexts/MergeBlacklistContext.cs @@ -1,7 +1,6 @@ using System.Diagnostics.CodeAnalysis; using BackendFramework.Interfaces; using BackendFramework.Models; -using Microsoft.Extensions.Options; using MongoDB.Driver; namespace BackendFramework.Contexts @@ -9,15 +8,13 @@ namespace BackendFramework.Contexts [ExcludeFromCodeCoverage] public class MergeBlacklistContext : IMergeBlacklistContext { - private readonly IMongoDatabase _db; - - public MergeBlacklistContext(IOptions options) + private readonly IMongoDbContext _mongoDbContext; + public MergeBlacklistContext(IMongoDbContext mongoDbContext) { - var client = new MongoClient(options.Value.ConnectionString); - _db = client.GetDatabase(options.Value.CombineDatabase); + _mongoDbContext = mongoDbContext; } - public IMongoCollection MergeBlacklist => _db.GetCollection( + public IMongoCollection MergeBlacklist => _mongoDbContext.Db.GetCollection( "MergeBlacklistCollection"); } } diff --git a/Backend/Contexts/MergeGraylistContext.cs b/Backend/Contexts/MergeGraylistContext.cs index d9ea381829..35b0f8caf9 100644 --- a/Backend/Contexts/MergeGraylistContext.cs +++ b/Backend/Contexts/MergeGraylistContext.cs @@ -1,7 +1,6 @@ using System.Diagnostics.CodeAnalysis; using BackendFramework.Interfaces; using BackendFramework.Models; -using Microsoft.Extensions.Options; using MongoDB.Driver; namespace BackendFramework.Contexts @@ -9,15 +8,13 @@ namespace BackendFramework.Contexts [ExcludeFromCodeCoverage] public class MergeGraylistContext : IMergeGraylistContext { - private readonly IMongoDatabase _db; - - public MergeGraylistContext(IOptions options) + private readonly IMongoDbContext _mongoDbContext; + public MergeGraylistContext(IMongoDbContext mongoDbContext) { - var client = new MongoClient(options.Value.ConnectionString); - _db = client.GetDatabase(options.Value.CombineDatabase); + _mongoDbContext = mongoDbContext; } - public IMongoCollection MergeGraylist => _db.GetCollection( + public IMongoCollection MergeGraylist => _mongoDbContext.Db.GetCollection( "MergeGraylistCollection"); } } diff --git a/Backend/Contexts/MongoDbContext.cs b/Backend/Contexts/MongoDbContext.cs new file mode 100644 index 0000000000..475c7cb3c7 --- /dev/null +++ b/Backend/Contexts/MongoDbContext.cs @@ -0,0 +1,49 @@ +using System.Threading.Tasks; +using BackendFramework.Interfaces; +using Microsoft.Extensions.Options; +using MongoDB.Driver; + +namespace BackendFramework.Contexts; + +public class MongoDbContext : IMongoDbContext +{ + public IMongoDatabase Db { get; } + + public MongoDbContext(IOptions options) + { + var client = new MongoClient(options.Value.ConnectionString); + Db = client.GetDatabase(options.Value.CombineDatabase); + } + + public async Task BeginTransaction() + { + var session = await Db.Client.StartSessionAsync(); + session.StartTransaction(); + return new MongoTransactionWrapper(session); + } + + private class MongoTransactionWrapper : IMongoTransaction + { + private readonly IClientSessionHandle _session; + + public MongoTransactionWrapper(IClientSessionHandle session) + { + _session = session; + } + + public Task CommitTransactionAsync() + { + return _session.CommitTransactionAsync(); + } + + public Task AbortTransactionAsync() + { + return _session.AbortTransactionAsync(); + } + + public void Dispose() + { + _session.Dispose(); + } + } +} diff --git a/Backend/Contexts/PasswordResetContext.cs b/Backend/Contexts/PasswordResetContext.cs index dbcc4041c0..84aebc7eed 100644 --- a/Backend/Contexts/PasswordResetContext.cs +++ b/Backend/Contexts/PasswordResetContext.cs @@ -10,17 +10,16 @@ namespace BackendFramework.Contexts [ExcludeFromCodeCoverage] public class PasswordResetContext : IPasswordResetContext { - private readonly IMongoDatabase _db; + private readonly IMongoDbContext _mongoDbContext; public int ExpireTime { get; } - public PasswordResetContext(IOptions options) + public PasswordResetContext(IOptions options, IMongoDbContext mongoDbContext) { - var client = new MongoClient(options.Value.ConnectionString); - _db = client.GetDatabase(options.Value.CombineDatabase); + _mongoDbContext = mongoDbContext; ExpireTime = options.Value.PassResetExpireTime; } - private IMongoCollection PasswordResets => _db.GetCollection( + private IMongoCollection PasswordResets => _mongoDbContext.Db.GetCollection( "PasswordResetCollection"); public Task ClearAll(string email) diff --git a/Backend/Contexts/ProjectContext.cs b/Backend/Contexts/ProjectContext.cs index 696a8a35da..0901b6b0e4 100644 --- a/Backend/Contexts/ProjectContext.cs +++ b/Backend/Contexts/ProjectContext.cs @@ -1,7 +1,6 @@ using System.Diagnostics.CodeAnalysis; using BackendFramework.Interfaces; using BackendFramework.Models; -using Microsoft.Extensions.Options; using MongoDB.Driver; namespace BackendFramework.Contexts @@ -9,14 +8,12 @@ namespace BackendFramework.Contexts [ExcludeFromCodeCoverage] public class ProjectContext : IProjectContext { - private readonly IMongoDatabase _db; - - public ProjectContext(IOptions options) + private readonly IMongoDbContext _mongoDbContext; + public ProjectContext(IMongoDbContext mongoDbContext) { - var client = new MongoClient(options.Value.ConnectionString); - _db = client.GetDatabase(options.Value.CombineDatabase); + _mongoDbContext = mongoDbContext; } - public IMongoCollection Projects => _db.GetCollection("ProjectsCollection"); + public IMongoCollection Projects => _mongoDbContext.Db.GetCollection("ProjectsCollection"); } } diff --git a/Backend/Contexts/SemanticDomainContext.cs b/Backend/Contexts/SemanticDomainContext.cs index 5ad11cac28..66fa39acd0 100644 --- a/Backend/Contexts/SemanticDomainContext.cs +++ b/Backend/Contexts/SemanticDomainContext.cs @@ -1,7 +1,6 @@ using System.Diagnostics.CodeAnalysis; using BackendFramework.Interfaces; using BackendFramework.Models; -using Microsoft.Extensions.Options; using MongoDB.Driver; namespace BackendFramework.Contexts @@ -9,15 +8,13 @@ namespace BackendFramework.Contexts [ExcludeFromCodeCoverage] public class SemanticDomainContext : ISemanticDomainContext { - private readonly IMongoDatabase _db; - - public SemanticDomainContext(IOptions options) + private readonly IMongoDbContext _mongoDbContext; + public SemanticDomainContext(IMongoDbContext mongoDbContext) { - var client = new MongoClient(options.Value.ConnectionString); - _db = client.GetDatabase(options.Value.CombineDatabase); + _mongoDbContext = mongoDbContext; } - public IMongoCollection SemanticDomains => _db.GetCollection("SemanticDomainTree"); - public IMongoCollection FullSemanticDomains => _db.GetCollection("SemanticDomains"); + public IMongoCollection SemanticDomains => _mongoDbContext.Db.GetCollection("SemanticDomainTree"); + public IMongoCollection FullSemanticDomains => _mongoDbContext.Db.GetCollection("SemanticDomains"); } } diff --git a/Backend/Contexts/SpeakerContext.cs b/Backend/Contexts/SpeakerContext.cs index f65c3e08c0..e577401afa 100644 --- a/Backend/Contexts/SpeakerContext.cs +++ b/Backend/Contexts/SpeakerContext.cs @@ -1,7 +1,6 @@ using System.Diagnostics.CodeAnalysis; using BackendFramework.Interfaces; using BackendFramework.Models; -using Microsoft.Extensions.Options; using MongoDB.Driver; namespace BackendFramework.Contexts @@ -9,14 +8,13 @@ namespace BackendFramework.Contexts [ExcludeFromCodeCoverage] public class SpeakerContext : ISpeakerContext { - private readonly IMongoDatabase _db; + private readonly IMongoDbContext _mongoDbContext; - public SpeakerContext(IOptions options) + public SpeakerContext(IMongoDbContext mongoDbContext) { - var client = new MongoClient(options.Value.ConnectionString); - _db = client.GetDatabase(options.Value.CombineDatabase); + _mongoDbContext = mongoDbContext; } - public IMongoCollection Speakers => _db.GetCollection("SpeakersCollection"); + public IMongoCollection Speakers => _mongoDbContext.Db.GetCollection("SpeakersCollection"); } } diff --git a/Backend/Contexts/UserContext.cs b/Backend/Contexts/UserContext.cs index 46122c146c..92778f02d7 100644 --- a/Backend/Contexts/UserContext.cs +++ b/Backend/Contexts/UserContext.cs @@ -1,7 +1,6 @@ using System.Diagnostics.CodeAnalysis; using BackendFramework.Interfaces; using BackendFramework.Models; -using Microsoft.Extensions.Options; using MongoDB.Driver; namespace BackendFramework.Contexts @@ -9,14 +8,12 @@ namespace BackendFramework.Contexts [ExcludeFromCodeCoverage] public class UserContext : IUserContext { - private readonly IMongoDatabase _db; - - public UserContext(IOptions options) + private readonly IMongoDbContext _mongoDbContext; + public UserContext(IMongoDbContext mongoDbContext) { - var client = new MongoClient(options.Value.ConnectionString); - _db = client.GetDatabase(options.Value.CombineDatabase); + _mongoDbContext = mongoDbContext; } - public IMongoCollection Users => _db.GetCollection("UsersCollection"); + public IMongoCollection Users => _mongoDbContext.Db.GetCollection("UsersCollection"); } } diff --git a/Backend/Contexts/UserEditContext.cs b/Backend/Contexts/UserEditContext.cs index 7487d78ec0..d0de83a1a6 100644 --- a/Backend/Contexts/UserEditContext.cs +++ b/Backend/Contexts/UserEditContext.cs @@ -1,7 +1,6 @@ using System.Diagnostics.CodeAnalysis; using BackendFramework.Interfaces; using BackendFramework.Models; -using Microsoft.Extensions.Options; using MongoDB.Driver; namespace BackendFramework.Contexts @@ -9,14 +8,12 @@ namespace BackendFramework.Contexts [ExcludeFromCodeCoverage] public class UserEditContext : IUserEditContext { - private readonly IMongoDatabase _db; - - public UserEditContext(IOptions options) + private readonly IMongoDbContext _mongoDbContext; + public UserEditContext(IMongoDbContext mongoDbContext) { - var client = new MongoClient(options.Value.ConnectionString); - _db = client.GetDatabase(options.Value.CombineDatabase); + _mongoDbContext = mongoDbContext; } - public IMongoCollection UserEdits => _db.GetCollection("UserEditsCollection"); + public IMongoCollection UserEdits => _mongoDbContext.Db.GetCollection("UserEditsCollection"); } } diff --git a/Backend/Contexts/UserRoleContext.cs b/Backend/Contexts/UserRoleContext.cs index 086c2ffe57..92b7c6506f 100644 --- a/Backend/Contexts/UserRoleContext.cs +++ b/Backend/Contexts/UserRoleContext.cs @@ -1,7 +1,6 @@ using System.Diagnostics.CodeAnalysis; using BackendFramework.Interfaces; using BackendFramework.Models; -using Microsoft.Extensions.Options; using MongoDB.Driver; namespace BackendFramework.Contexts @@ -9,14 +8,12 @@ namespace BackendFramework.Contexts [ExcludeFromCodeCoverage] public class UserRoleContext : IUserRoleContext { - private readonly IMongoDatabase _db; - - public UserRoleContext(IOptions options) + private readonly IMongoDbContext _mongoDbContext; + public UserRoleContext(IMongoDbContext mongoDbContext) { - var client = new MongoClient(options.Value.ConnectionString); - _db = client.GetDatabase(options.Value.CombineDatabase); + _mongoDbContext = mongoDbContext; } - public IMongoCollection UserRoles => _db.GetCollection("UserRolesCollection"); + public IMongoCollection UserRoles => _mongoDbContext.Db.GetCollection("UserRolesCollection"); } } diff --git a/Backend/Contexts/WordContext.cs b/Backend/Contexts/WordContext.cs index 32876c6af2..d8989de00e 100644 --- a/Backend/Contexts/WordContext.cs +++ b/Backend/Contexts/WordContext.cs @@ -1,7 +1,6 @@ using System.Diagnostics.CodeAnalysis; using BackendFramework.Interfaces; using BackendFramework.Models; -using Microsoft.Extensions.Options; using MongoDB.Driver; namespace BackendFramework.Contexts @@ -9,15 +8,13 @@ namespace BackendFramework.Contexts [ExcludeFromCodeCoverage] public class WordContext : IWordContext { - private readonly IMongoDatabase _db; - - public WordContext(IOptions options) + private readonly IMongoDbContext _mongoDbContext; + public WordContext(IMongoDbContext mongoDbContext) { - var client = new MongoClient(options.Value.ConnectionString); - _db = client.GetDatabase(options.Value.CombineDatabase); + _mongoDbContext = mongoDbContext; } - public IMongoCollection Words => _db.GetCollection("WordsCollection"); - public IMongoCollection Frontier => _db.GetCollection("FrontierCollection"); + public IMongoCollection Words => _mongoDbContext.Db.GetCollection("WordsCollection"); + public IMongoCollection Frontier => _mongoDbContext.Db.GetCollection("FrontierCollection"); } } diff --git a/Backend/Interfaces/IMongoDbContext.cs b/Backend/Interfaces/IMongoDbContext.cs new file mode 100644 index 0000000000..01827db0db --- /dev/null +++ b/Backend/Interfaces/IMongoDbContext.cs @@ -0,0 +1,17 @@ +using System; +using System.Threading.Tasks; +using MongoDB.Driver; + +namespace BackendFramework.Interfaces; + +public interface IMongoDbContext +{ + IMongoDatabase Db { get; } + Task BeginTransaction(); +} + +public interface IMongoTransaction : IDisposable +{ + Task CommitTransactionAsync(); + Task AbortTransactionAsync(); +} diff --git a/Backend/Services/MergeService.cs b/Backend/Services/MergeService.cs index 4e0774a697..bac1396702 100644 --- a/Backend/Services/MergeService.cs +++ b/Backend/Services/MergeService.cs @@ -11,14 +11,16 @@ namespace BackendFramework.Services /// More complex functions and application logic for s public class MergeService : IMergeService { + private readonly IMongoDbContext _mongoDbContext; private readonly IMergeBlacklistRepository _mergeBlacklistRepo; private readonly IMergeGraylistRepository _mergeGraylistRepo; private readonly IWordRepository _wordRepo; private readonly IWordService _wordService; - public MergeService(IMergeBlacklistRepository mergeBlacklistRepo, IMergeGraylistRepository mergeGraylistRepo, - IWordRepository wordRepo, IWordService wordService) + public MergeService(IMongoDbContext mongoDbContext, IMergeBlacklistRepository mergeBlacklistRepo, + IMergeGraylistRepository mergeGraylistRepo, IWordRepository wordRepo, IWordService wordService) { + _mongoDbContext = mongoDbContext; _mergeBlacklistRepo = mergeBlacklistRepo; _mergeGraylistRepo = mergeGraylistRepo; _wordRepo = wordRepo; @@ -75,8 +77,11 @@ public async Task> Merge(string projectId, string userId, List !m.DeleteOnly); var newWords = keptWords.Select(m => MergePrepParent(projectId, m).Result).ToList(); + using var transaction = await _mongoDbContext.BeginTransaction(); await Task.WhenAll(mergeWordsList.Select(m => MergeDeleteChildren(projectId, m))); - return await _wordService.Create(userId, newWords); + var words = await _wordService.Create(userId, newWords); + await transaction.CommitTransactionAsync(); + return words; } /// Undo merge @@ -92,15 +97,18 @@ public async Task UndoMerge(string projectId, string userId, MergeUndoIds } } + using var transaction = await _mongoDbContext.BeginTransaction(); // If children are not restorable, return without any undo. if (!await _wordService.RestoreFrontierWords(projectId, ids.ChildIds)) { + // Nothing happens, transaction does not need to commit. return false; - }; + } foreach (var parentId in ids.ParentIds) { await _wordService.DeleteFrontierWord(projectId, userId, parentId); } + await transaction.CommitTransactionAsync(); return true; } diff --git a/Backend/Startup.cs b/Backend/Startup.cs index 708bf293b0..4904a0ef43 100644 --- a/Backend/Startup.cs +++ b/Backend/Startup.cs @@ -225,6 +225,9 @@ public void ConfigureServices(IServiceCollection services) // Register concrete types for dependency injection + // Mongo context, scoped so it is shared across a single request + services.AddScoped(); + // Banner types services.AddTransient(); services.AddTransient(); @@ -344,7 +347,8 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IHostApp // If an admin user has been created via the command line, treat that as a single action and shut the // server down so the calling script knows it's been completed successfully or unsuccessfully. - var userRepo = app.ApplicationServices.GetService(); + using var startupScope = app.ApplicationServices.CreateAsyncScope(); + var userRepo = startupScope.ServiceProvider.GetService(); if (userRepo is not null && CreateAdminUser(userRepo)) { _logger.LogInformation("Stopping application");