From a4b32d41905b006bb05970be354e3852f4bd04d3 Mon Sep 17 00:00:00 2001 From: Mustafa Ozan Sindi Date: Wed, 4 Jun 2025 13:01:16 +0300 Subject: [PATCH] feat(mongodb): Add MongoClientFactory to reuse MongoClient per connection string" --- .../Volo/Abp/MongoDB/AbpMongoDbModule.cs | 5 +- .../MongoDB/Clients/IMongoClientFactory.cs | 9 ++ .../Abp/MongoDB/Clients/MongoClientFactory.cs | 32 ++++++++ .../UnitOfWorkMongoDbContextProvider.cs | 18 ++-- .../Clients/MongoClient_Factory_Tests.cs | 82 +++++++++++++++++++ 5 files changed, 135 insertions(+), 11 deletions(-) create mode 100644 framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/Clients/IMongoClientFactory.cs create mode 100644 framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/Clients/MongoClientFactory.cs create mode 100644 framework/test/Volo.Abp.MongoDB.Tests/Volo/Abp/MongoDB/Clients/MongoClient_Factory_Tests.cs diff --git a/framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/AbpMongoDbModule.cs b/framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/AbpMongoDbModule.cs index 195491d989..61e1e2120d 100644 --- a/framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/AbpMongoDbModule.cs +++ b/framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/AbpMongoDbModule.cs @@ -8,6 +8,7 @@ using Volo.Abp.Domain; using Volo.Abp.Domain.Entities.Events.Distributed; using Volo.Abp.Domain.Repositories.MongoDB; using Volo.Abp.Modularity; +using Volo.Abp.MongoDB.Clients; using Volo.Abp.MongoDB.DependencyInjection; using Volo.Abp.Uow.MongoDB; using Volo.Abp.MongoDB.DistributedEvents; @@ -30,6 +31,8 @@ public class AbpMongoDbModule : AbpModule public override void ConfigureServices(ServiceConfigurationContext context) { + context.Services.AddSingleton(); + context.Services.TryAddTransient( typeof(IMongoDbContextProvider<>), typeof(UnitOfWorkMongoDbContextProvider<>) @@ -61,4 +64,4 @@ public class AbpMongoDbModule : AbpModule options.IgnoredEventSelectors.Add(); }); } -} +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/Clients/IMongoClientFactory.cs b/framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/Clients/IMongoClientFactory.cs new file mode 100644 index 0000000000..aa63574e74 --- /dev/null +++ b/framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/Clients/IMongoClientFactory.cs @@ -0,0 +1,9 @@ +using MongoDB.Driver; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.MongoDB.Clients; + +public interface IMongoClientFactory : ISingletonDependency +{ + MongoClient GetClient(string connectionString); +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/Clients/MongoClientFactory.cs b/framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/Clients/MongoClientFactory.cs new file mode 100644 index 0000000000..80f866fda7 --- /dev/null +++ b/framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/Clients/MongoClientFactory.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Concurrent; +using Microsoft.Extensions.Options; +using MongoDB.Driver; + +namespace Volo.Abp.MongoDB.Clients; + +public class MongoClientFactory : IMongoClientFactory +{ + private readonly ConcurrentDictionary _clients = new(); + private readonly AbpMongoDbContextOptions Options; + + public MongoClientFactory(IOptions options) + { + Options = options.Value; + } + + public MongoClient GetClient(string connectionString) + { + if (string.IsNullOrWhiteSpace(connectionString)) + { + throw new ArgumentException("Connection string must not be null or empty.", nameof(connectionString)); + } + + return _clients.GetOrAdd(connectionString, cs => + { + var mongoClientSettings = MongoClientSettings.FromUrl(new MongoUrl(cs)); + Options.MongoClientSettingsConfigurer?.Invoke(mongoClientSettings); + return new MongoClient(mongoClientSettings); + }); + } +} \ No newline at end of file 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 1585bc1332..bf305abefa 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 @@ -9,6 +9,7 @@ using MongoDB.Bson; using MongoDB.Driver; using Volo.Abp.Data; using Volo.Abp.MongoDB; +using Volo.Abp.MongoDB.Clients; using Volo.Abp.MultiTenancy; using Volo.Abp.Threading; @@ -26,6 +27,7 @@ public class UnitOfWorkMongoDbContextProvider : IMongoDbContext protected readonly ICurrentTenant CurrentTenant; protected readonly AbpMongoDbContextOptions Options; protected readonly IMongoDbContextTypeProvider DbContextTypeProvider; + protected readonly IMongoClientFactory MongoClientFactory; public UnitOfWorkMongoDbContextProvider( IUnitOfWorkManager unitOfWorkManager, @@ -33,13 +35,14 @@ public class UnitOfWorkMongoDbContextProvider : IMongoDbContext ICancellationTokenProvider cancellationTokenProvider, ICurrentTenant currentTenant, IOptions options, - IMongoDbContextTypeProvider dbContextTypeProvider) + IMongoDbContextTypeProvider dbContextTypeProvider, IMongoClientFactory mongoClientFactory) { UnitOfWorkManager = unitOfWorkManager; ConnectionStringResolver = connectionStringResolver; CancellationTokenProvider = cancellationTokenProvider; CurrentTenant = currentTenant; DbContextTypeProvider = dbContextTypeProvider; + MongoClientFactory = mongoClientFactory; Options = options.Value; Logger = NullLogger>.Instance; @@ -77,12 +80,11 @@ public class UnitOfWorkMongoDbContextProvider : IMongoDbContext databaseName = ConnectionStringNameAttribute.GetConnStringName(targetDbContextType); } - //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 (TMongoDbContext)((MongoDbDatabaseApi) databaseApi).DbContext; + return (TMongoDbContext)((MongoDbDatabaseApi)databaseApi).DbContext; } public virtual async Task GetDbContextAsync(CancellationToken cancellationToken = default) @@ -121,11 +123,10 @@ public class UnitOfWorkMongoDbContextProvider : IMongoDbContext unitOfWork.AddDatabaseApi(dbContextKey, databaseApi); } - return (TMongoDbContext)((MongoDbDatabaseApi) databaseApi).DbContext; + return (TMongoDbContext)((MongoDbDatabaseApi)databaseApi).DbContext; } [Obsolete("Use CreateDbContextAsync")] - private TMongoDbContext CreateDbContext(IUnitOfWork unitOfWork, MongoUrl mongoUrl, string databaseName) { var client = CreateMongoClient(mongoUrl); @@ -299,14 +300,11 @@ public class UnitOfWorkMongoDbContextProvider : IMongoDbContext protected virtual MongoClient CreateMongoClient(MongoUrl mongoUrl) { - var mongoClientSettings = MongoClientSettings.FromUrl(mongoUrl); - Options.MongoClientSettingsConfigurer?.Invoke(mongoClientSettings); - - return new MongoClient(mongoClientSettings); + return MongoClientFactory.GetClient(mongoUrl.ToString()); } protected virtual CancellationToken GetCancellationToken(CancellationToken preferredValue = default) { return CancellationTokenProvider.FallbackToProvider(preferredValue); } -} +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.MongoDB.Tests/Volo/Abp/MongoDB/Clients/MongoClient_Factory_Tests.cs b/framework/test/Volo.Abp.MongoDB.Tests/Volo/Abp/MongoDB/Clients/MongoClient_Factory_Tests.cs new file mode 100644 index 0000000000..b2f8c9b7d5 --- /dev/null +++ b/framework/test/Volo.Abp.MongoDB.Tests/Volo/Abp/MongoDB/Clients/MongoClient_Factory_Tests.cs @@ -0,0 +1,82 @@ +using System; +using System.Threading.Tasks; +using MongoDB.Driver; +using Volo.Abp.TestApp.Testing; +using Xunit; + +namespace Volo.Abp.MongoDB.Clients; + +[Collection(MongoTestCollection.Name)] +public class MongoClient_Factory_Tests : MongoDbTestBase +{ + private readonly IMongoClientFactory _factory; + + public MongoClient_Factory_Tests() + { + _factory = GetRequiredService(); + } + + [Fact] + public void Should_Return_Same_Instance_For_Same_ConnectionString() + { + // Arrange + var connectionString = "mongodb://localhost:27017/my-db"; + + // Act + var client1 = _factory.GetClient(connectionString); + var client2 = _factory.GetClient(connectionString); + + // Assert + Assert.Same(client1, client2); + } + + [Fact] + public void Should_Return_Different_Instances_For_Different_ConnectionStrings() + { + // Arrange + var cs1 = "mongodb://localhost:27017/db1"; + var cs2 = "mongodb://localhost:27017/db2"; + + // Act + var client1 = _factory.GetClient(cs1); + var client2 = _factory.GetClient(cs2); + + // Assert + Assert.NotSame(client1, client2); + } + + [Fact] + public void Should_Not_Throw_For_Valid_But_Unreachable_Connection() + { + // Arrange + var cs = "mongodb://unreachablehost:12345/any"; + + // Act + var client = _factory.GetClient(cs); + + // Assert + Assert.NotNull(client); // Even though it's not connectable now, the instance can be created + } + + [Fact] + public void Should_Be_ThreadSafe_When_Accessed_Concurrently() + { + var connectionString = "mongodb://localhost:27017/threadsafe"; + MongoClient[] results = new MongoClient[100]; + + Parallel.For(0, 100, i => + { + results[i] = _factory.GetClient(connectionString); + }); + + Assert.All(results, client => Assert.Same(results[0], client)); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + public void Should_Throw_If_ConnectionString_Is_Null_Or_Empty(string connectionString) + { + Assert.Throws(() => _factory.GetClient(connectionString)); + } +} \ No newline at end of file