Skip to content

Commit 9f77851

Browse files
committed
Move transaction scaffolding into MongoDbContext
1 parent d711004 commit 9f77851

File tree

4 files changed

+126
-78
lines changed

4 files changed

+126
-78
lines changed

Backend.Tests/Mocks/MongoDbContextMock.cs

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,27 +8,28 @@ namespace Backend.Tests.Mocks;
88
public class MongoDbContextMock : IMongoDbContext
99
{
1010
public IMongoDatabase Db => throw new NotSupportedException();
11+
1112
public Task<IMongoTransaction> BeginTransaction()
13+
=> Task.FromResult<IMongoTransaction>(new MongoTransactionMock());
14+
15+
public Task<T> ExecuteWithTransaction<T>(Func<IClientSessionHandle, Task<T>> operation)
16+
{
17+
throw new NotImplementedException();
18+
}
19+
20+
public Task<T?> ExecuteWithTransactionAllowNull<T>(Func<IClientSessionHandle, Task<T?>> operation) where T : class
1221
{
13-
return Task.FromResult<IMongoTransaction>(new MongoTransactionMock());
22+
throw new NotImplementedException();
1423
}
1524

1625
private sealed class MongoTransactionMock : IMongoTransaction
1726
{
1827
public IClientSessionHandle Session => null!;
1928

20-
public Task CommitTransactionAsync()
21-
{
22-
return Task.CompletedTask;
23-
}
29+
public Task CommitTransactionAsync() => Task.CompletedTask;
2430

25-
public Task AbortTransactionAsync()
26-
{
27-
return Task.CompletedTask;
28-
}
31+
public Task AbortTransactionAsync() => Task.CompletedTask;
2932

30-
public void Dispose()
31-
{
32-
}
33+
public void Dispose() { }
3334
}
3435
}

Backend/Contexts/MongoDbContext.cs

Lines changed: 72 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,105 @@
1-
using System.Threading.Tasks;
1+
using System;
2+
using System.Threading.Tasks;
23
using BackendFramework.Interfaces;
34
using Microsoft.Extensions.Options;
45
using MongoDB.Driver;
56

67
namespace BackendFramework.Contexts;
78

9+
/// <summary>
10+
/// MongoDB context for accessing the configured database and executing transactional operations.
11+
/// </summary>
812
public class MongoDbContext : IMongoDbContext
913
{
14+
/// <summary>
15+
/// Gets the configured MongoDB database instance.
16+
/// </summary>
1017
public IMongoDatabase Db { get; }
1118

19+
/// <summary>
20+
/// Creates a new <see cref="MongoDbContext"/> from application settings.
21+
/// </summary>
22+
/// <param name="options">Options containing the Mongo connection string and database name.</param>
1223
public MongoDbContext(IOptions<Startup.Settings> options)
1324
{
1425
var client = new MongoClient(options.Value.ConnectionString);
1526
Db = client.GetDatabase(options.Value.CombineDatabase);
1627
}
1728

29+
/// <summary>
30+
/// Begins a MongoDB transaction and returns a disposable transaction wrapper.
31+
/// </summary>
32+
/// <returns>A transaction wrapper containing the active client session.</returns>
1833
public async Task<IMongoTransaction> BeginTransaction()
1934
{
2035
var session = await Db.Client.StartSessionAsync();
2136
session.StartTransaction();
2237
return new MongoTransactionWrapper(session);
2338
}
2439

25-
private class MongoTransactionWrapper(IClientSessionHandle session) : IMongoTransaction
40+
/// <summary>
41+
/// Executes an operation in a transaction, committing on success and aborting on exception.
42+
/// </summary>
43+
/// <typeparam name="T">The operation result type.</typeparam>
44+
/// <param name="operation">Operation to execute with the transaction session.</param>
45+
/// <returns>The operation result.</returns>
46+
public async Task<T> ExecuteWithTransaction<T>(Func<IClientSessionHandle, Task<T>> operation)
2647
{
27-
private readonly IClientSessionHandle _session = session;
28-
29-
public IClientSessionHandle Session => _session;
30-
31-
public Task CommitTransactionAsync()
48+
using var transaction = await BeginTransaction();
49+
try
3250
{
33-
return _session.CommitTransactionAsync();
51+
var result = await operation(transaction.Session);
52+
await transaction.CommitTransactionAsync();
53+
return result;
3454
}
35-
36-
public Task AbortTransactionAsync()
55+
catch
3756
{
38-
return _session.AbortTransactionAsync();
57+
await transaction.AbortTransactionAsync();
58+
throw;
3959
}
60+
}
4061

41-
public void Dispose()
62+
/// <summary>
63+
/// Executes an operation in a transaction, committing when a non-null result is returned.
64+
/// </summary>
65+
/// <typeparam name="T">The operation result reference type.</typeparam>
66+
/// <param name="operation">Operation to execute with the transaction session.</param>
67+
/// <returns>
68+
/// The operation result when non-null; otherwise <see langword="null"/> after aborting the transaction.
69+
/// </returns>
70+
public async Task<T?> ExecuteWithTransactionAllowNull<T>(Func<IClientSessionHandle, Task<T?>> operation)
71+
where T : class
72+
{
73+
using var transaction = await BeginTransaction();
74+
try
75+
{
76+
var result = await operation(transaction.Session);
77+
if (result is null)
78+
{
79+
await transaction.AbortTransactionAsync();
80+
return null;
81+
}
82+
83+
await transaction.CommitTransactionAsync();
84+
return result;
85+
}
86+
catch
4287
{
43-
_session.Dispose();
88+
await transaction.AbortTransactionAsync();
89+
throw;
4490
}
4591
}
92+
93+
private class MongoTransactionWrapper(IClientSessionHandle session) : IMongoTransaction
94+
{
95+
private readonly IClientSessionHandle _session = session;
96+
97+
public IClientSessionHandle Session => _session;
98+
99+
public Task CommitTransactionAsync() => _session.CommitTransactionAsync();
100+
101+
public Task AbortTransactionAsync() => _session.AbortTransactionAsync();
102+
103+
public void Dispose() => _session.Dispose();
104+
}
46105
}

Backend/Interfaces/IMongoDbContext.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,45 @@
44

55
namespace BackendFramework.Interfaces;
66

7+
/// <summary>
8+
/// Abstraction over MongoDB database access and transaction execution.
9+
/// </summary>
710
public interface IMongoDbContext
811
{
12+
/// <summary>
13+
/// Gets the configured MongoDB database instance.
14+
/// </summary>
915
IMongoDatabase Db { get; }
16+
17+
/// <summary>
18+
/// Begins a new transaction and returns the transaction wrapper.
19+
/// </summary>
20+
/// <returns>A transaction wrapper containing the active client session.</returns>
1021
Task<IMongoTransaction> BeginTransaction();
22+
23+
/// <summary>
24+
/// Executes an operation in a transaction, committing on success and aborting on exception.
25+
/// </summary>
26+
/// <typeparam name="T">The operation result type.</typeparam>
27+
/// <param name="operation">Operation to execute with the transaction session.</param>
28+
/// <returns>The operation result.</returns>
29+
Task<T> ExecuteWithTransaction<T>(Func<IClientSessionHandle, Task<T>> operation);
30+
31+
/// <summary>
32+
/// Executes an operation in a transaction, committing only when a non-null result is returned.
33+
/// </summary>
34+
/// <typeparam name="T">The operation result reference type.</typeparam>
35+
/// <param name="operation">Operation to execute with the transaction session.</param>
36+
/// <returns>
37+
/// The operation result when non-null; otherwise <see langword="null"/> after aborting the transaction.
38+
/// </returns>
39+
Task<T?> ExecuteWithTransactionAllowNull<T>(Func<IClientSessionHandle, Task<T?>> operation)
40+
where T : class;
1141
}
1242

43+
/// <summary>
44+
/// Represents a MongoDB transaction wrapper that exposes the active session and transaction controls.
45+
/// </summary>
1346
public interface IMongoTransaction : IDisposable
1447
{
1548
IClientSessionHandle Session { get; }

Backend/Repositories/WordRepository.cs

Lines changed: 8 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ public async Task<List<Word>> RepoCreate(List<Word> words)
142142

143143
return words.Count == 0
144144
? []
145-
: await ExecuteWithTransaction(async s => await CreateWithSession(s, words));
145+
: await _dbContext.ExecuteWithTransaction(async s => await CreateWithSession(s, words));
146146
}
147147

148148
/// <summary>
@@ -175,7 +175,7 @@ public async Task<List<Word>> RepoCreate(List<Word> words)
175175
using var activity = OtelService.StartActivityWithTag(
176176
otelTagName, "updating a word in WordsCollection and Frontier, deleting old word from Frontier");
177177

178-
return await ExecuteWithTransactionAllowNull(async session =>
178+
return await _dbContext.ExecuteWithTransactionAllowNull(async session =>
179179
{
180180
// Make sure old word exists in the Frontier.
181181
var deletedWord =
@@ -208,7 +208,7 @@ public async Task<List<Word>> RepoCreate(List<Word> words)
208208
using var activity = OtelService.StartActivityWithTag(
209209
otelTagName, "creating word in WordsCollection and Frontier, deleting old word from Frontier");
210210

211-
return await ExecuteWithTransactionAllowNull(
211+
return await _dbContext.ExecuteWithTransactionAllowNull(
212212
async s => await RepoUpdateFrontierWithSession(s, word, false, modifyNewWordFromOldWord));
213213
}
214214

@@ -227,7 +227,7 @@ public async Task<List<Word>> RepoCreate(List<Word> words)
227227

228228
var oldIdSet = idsToDelete.ToHashSet();
229229

230-
return await ExecuteWithTransactionAllowNull(async session =>
230+
return await _dbContext.ExecuteWithTransactionAllowNull(async session =>
231231
{
232232
// Update the new words
233233
foreach (var word in newWords)
@@ -260,7 +260,7 @@ public async Task<List<Word>> RepoRevertReplaceFrontier(
260260
throw new ArgumentException("Ids to delete and restore must be disjoint");
261261
}
262262

263-
return await ExecuteWithTransaction(async session =>
263+
return await _dbContext.ExecuteWithTransaction(async session =>
264264
{
265265
var restoredWords = await RepoRestoreFrontierWithSession(session, projectId, restoreSet.ToList());
266266
if (restoredWords.Count != restoreSet.Count)
@@ -334,7 +334,7 @@ private async Task RepoDeleteFrontierWithSession(IClientSessionHandle session,
334334
using var activity = OtelService.StartActivityWithTag(
335335
otelTagName, "adding word to WordsCollection, deleting word from Frontier");
336336

337-
return await ExecuteWithTransactionAllowNull(
337+
return await _dbContext.ExecuteWithTransactionAllowNull(
338338
async s => await RepoDeleteFrontierWithSession(s, projectId, wordId, modifyWord)
339339
);
340340
}
@@ -431,7 +431,8 @@ public async Task<List<Word>> RepoRestoreFrontier(string projectId, List<string>
431431

432432
return wordIds.Count == 0
433433
? []
434-
: await ExecuteWithTransaction(async s => await RepoRestoreFrontierWithSession(s, projectId, wordIds));
434+
: await _dbContext.ExecuteWithTransaction(
435+
async s => await RepoRestoreFrontierWithSession(s, projectId, wordIds));
435436
}
436437

437438
/// <summary> Adds a list of <see cref="Word"/>s only to the Frontier </summary>
@@ -472,51 +473,5 @@ public async Task<int> CountFrontierWordsWithDomain(string projectId, string dom
472473

473474
// TODO: Move them all here.
474475

475-
// TRANSACTION SCAFFOLDING
476-
477-
/// <summary>
478-
/// Executes the given operation in a transaction and commits if it succeeds.
479-
/// </summary>
480-
private async Task<T> ExecuteWithTransaction<T>(Func<IClientSessionHandle, Task<T>> operation)
481-
{
482-
using var transaction = await _dbContext.BeginTransaction();
483-
try
484-
{
485-
var result = await operation(transaction.Session);
486-
await transaction.CommitTransactionAsync();
487-
return result;
488-
}
489-
catch
490-
{
491-
await transaction.AbortTransactionAsync();
492-
throw;
493-
}
494-
}
495-
496-
/// <summary>
497-
/// Executes the given operation in a transaction and aborts if the operation returns null.
498-
/// </summary>
499-
private async Task<T?> ExecuteWithTransactionAllowNull<T>(Func<IClientSessionHandle, Task<T?>> operation)
500-
where T : class
501-
{
502-
using var transaction = await _dbContext.BeginTransaction();
503-
try
504-
{
505-
var result = await operation(transaction.Session);
506-
if (result is null)
507-
{
508-
await transaction.AbortTransactionAsync();
509-
return null;
510-
}
511-
512-
await transaction.CommitTransactionAsync();
513-
return result;
514-
}
515-
catch
516-
{
517-
await transaction.AbortTransactionAsync();
518-
throw;
519-
}
520-
}
521476
}
522477
}

0 commit comments

Comments
 (0)