using System.Globalization; using Microsoft.Extensions.Options; using MongoDB.Bson; using MongoDB.Bson.Serialization; using MongoDB.Bson.Serialization.Conventions; using MongoDB.Bson.Serialization.Serializers; using MongoDB.Driver; namespace BuildingBlocks.Mongo; // https://www.thecodebuzz.com/mongodb-repository-implementation-unit-testing-net-core-example/ public class MongoDbContext : IMongoDbContext { public IClientSessionHandle? Session { get; set; } public IMongoDatabase Database { get; } public IMongoClient MongoClient { get; } protected readonly IList> _commands; private static readonly bool _isSerializerRegisterd; static MongoDbContext() { BsonSerializer.RegisterSerializer(new GuidSerializer(BsonType.String)); } public MongoDbContext(IOptions options) { RegisterConventions(); MongoClient = new MongoClient(options.Value.ConnectionString); var databaseName = options.Value.DatabaseName; Database = MongoClient.GetDatabase(databaseName); // Every command will be stored and it'll be processed at SaveChanges _commands = new List>(); } private static void RegisterConventions() { ConventionRegistry.Register( "conventions", new ConventionPack { new CamelCaseElementNameConvention(), new IgnoreExtraElementsConvention(true), new EnumRepresentationConvention(BsonType.String), new IgnoreIfDefaultConvention(false), new ImmutablePocoConvention() }, _ => true); } public IMongoCollection GetCollection(string? name = null) { return Database.GetCollection(name ?? typeof(T).Name.ToLower(CultureInfo.CurrentCulture)); } public void Dispose() { while (Session is { IsInTransaction: true }) Thread.Sleep(TimeSpan.FromMilliseconds(100)); GC.SuppressFinalize(this); } public async Task SaveChangesAsync(CancellationToken cancellationToken = default) { var result = _commands.Count; using (Session = await MongoClient.StartSessionAsync(cancellationToken: cancellationToken)) { Session.StartTransaction(); try { var commandTasks = _commands.Select(c => c()); await Task.WhenAll(commandTasks); await Session.CommitTransactionAsync(cancellationToken); } catch (System.Exception ex) { await Session.AbortTransactionAsync(cancellationToken); _commands.Clear(); throw; } } _commands.Clear(); return result; } public async Task BeginTransactionAsync(CancellationToken cancellationToken = default) { Session = await MongoClient.StartSessionAsync(cancellationToken: cancellationToken); Session.StartTransaction(); } public async Task CommitTransactionAsync(CancellationToken cancellationToken = default) { if (Session is { IsInTransaction: true }) await Session.CommitTransactionAsync(cancellationToken); Session?.Dispose(); } public async Task RollbackTransaction(CancellationToken cancellationToken = default) { await Session?.AbortTransactionAsync(cancellationToken)!; } public void AddCommand(Func func) { _commands.Add(func); } public async Task ExecuteTransactionalAsync(Func action, CancellationToken cancellationToken = default) { await BeginTransactionAsync(cancellationToken); try { await action(); await CommitTransactionAsync(cancellationToken); } catch { await RollbackTransaction(cancellationToken); throw; } } public async Task ExecuteTransactionalAsync( Func> action, CancellationToken cancellationToken = default) { await BeginTransactionAsync(cancellationToken); try { var result = await action(); await CommitTransactionAsync(cancellationToken); return result; } catch { await RollbackTransaction(cancellationToken); throw; } } }