diff --git a/framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/Clients/AbpMongoClientFactory.cs b/framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/Clients/AbpMongoClientFactory.cs new file mode 100644 index 0000000000..983f7e067e --- /dev/null +++ b/framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/Clients/AbpMongoClientFactory.cs @@ -0,0 +1,44 @@ +using System.Collections.Concurrent; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using MongoDB.Driver; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.MongoDB.Clients; + +public class AbpMongoClientFactory : IAbpMongoClientFactory, ISingletonDependency +{ + protected ConcurrentDictionary ClientCache { get; } + protected AbpMongoDbContextOptions Options { get; } + + public AbpMongoClientFactory(IOptions options) + { + Options = options.Value; + ClientCache = new ConcurrentDictionary(); + } + + public virtual Task GetAsync(MongoUrl mongoUrl) + { + Check.NotNull(mongoUrl, nameof(mongoUrl)); + + return Task.FromResult( + ClientCache.GetOrAdd(mongoUrl.ToString(), _ => + { + var mongoClientSettings = MongoClientSettings.FromUrl(mongoUrl); + Options.MongoClientSettingsConfigurer?.Invoke(mongoClientSettings); + return new MongoClient(mongoClientSettings); + })); + } + + public virtual MongoClient Get(MongoUrl mongoUrl) + { + Check.NotNull(mongoUrl, nameof(mongoUrl)); + + return ClientCache.GetOrAdd(mongoUrl.ToString(), _ => + { + var mongoClientSettings = MongoClientSettings.FromUrl(mongoUrl); + Options.MongoClientSettingsConfigurer?.Invoke(mongoClientSettings); + return new MongoClient(mongoClientSettings); + }); + } +} diff --git a/framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/Clients/IAbpMongoClientFactory.cs b/framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/Clients/IAbpMongoClientFactory.cs new file mode 100644 index 0000000000..bf0d20d3ae --- /dev/null +++ b/framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/Clients/IAbpMongoClientFactory.cs @@ -0,0 +1,13 @@ +using System; +using System.Threading.Tasks; +using MongoDB.Driver; + +namespace Volo.Abp.MongoDB.Clients; + +public interface IAbpMongoClientFactory +{ + Task GetAsync(MongoUrl mongoUrl); + + [Obsolete("Use GetAsync method")] + MongoClient Get(MongoUrl mongoUrl); +} 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..acea595dce 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 @@ -4,11 +4,11 @@ using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Options; 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; @@ -24,23 +24,23 @@ public class UnitOfWorkMongoDbContextProvider : IMongoDbContext protected readonly IConnectionStringResolver ConnectionStringResolver; protected readonly ICancellationTokenProvider CancellationTokenProvider; protected readonly ICurrentTenant CurrentTenant; - protected readonly AbpMongoDbContextOptions Options; protected readonly IMongoDbContextTypeProvider DbContextTypeProvider; + protected readonly IAbpMongoClientFactory MongoClientFactory; public UnitOfWorkMongoDbContextProvider( IUnitOfWorkManager unitOfWorkManager, IConnectionStringResolver connectionStringResolver, ICancellationTokenProvider cancellationTokenProvider, ICurrentTenant currentTenant, - IOptions options, - IMongoDbContextTypeProvider dbContextTypeProvider) + IMongoDbContextTypeProvider dbContextTypeProvider, + IAbpMongoClientFactory mongoClientFactory) { UnitOfWorkManager = unitOfWorkManager; ConnectionStringResolver = connectionStringResolver; CancellationTokenProvider = cancellationTokenProvider; CurrentTenant = currentTenant; DbContextTypeProvider = dbContextTypeProvider; - Options = options.Value; + MongoClientFactory = mongoClientFactory; Logger = NullLogger>.Instance; } @@ -77,12 +77,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,14 +120,13 @@ 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); + var client = MongoClientFactory.Get(mongoUrl); var database = client.GetDatabase(databaseName); if (unitOfWork.Options.IsTransactional) @@ -148,7 +146,7 @@ public class UnitOfWorkMongoDbContextProvider : IMongoDbContext string databaseName, CancellationToken cancellationToken = default) { - var client = CreateMongoClient(mongoUrl); + var client = await MongoClientFactory.GetAsync(mongoUrl); var database = client.GetDatabase(databaseName); if (unitOfWork.Options.IsTransactional) @@ -297,14 +295,6 @@ public class UnitOfWorkMongoDbContextProvider : IMongoDbContext return ConnectionStringResolver.Resolve(dbContextType); } - protected virtual MongoClient CreateMongoClient(MongoUrl mongoUrl) - { - var mongoClientSettings = MongoClientSettings.FromUrl(mongoUrl); - Options.MongoClientSettingsConfigurer?.Invoke(mongoClientSettings); - - return new MongoClient(mongoClientSettings); - } - protected virtual CancellationToken GetCancellationToken(CancellationToken preferredValue = default) { return CancellationTokenProvider.FallbackToProvider(preferredValue); 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..df649132fa --- /dev/null +++ b/framework/test/Volo.Abp.MongoDB.Tests/Volo/Abp/MongoDB/Clients/MongoClient_Factory_Tests.cs @@ -0,0 +1,79 @@ +using System; +using System.Threading.Tasks; +using MongoDB.Driver; +using Xunit; + +namespace Volo.Abp.MongoDB.Clients; + +[Collection(MongoTestCollection.Name)] +public class MongoClient_Factory_Tests : MongoDbTestBase +{ + private readonly IAbpMongoClientFactory _factory; + + public MongoClient_Factory_Tests() + { + _factory = GetRequiredService(); + } + + [Fact] + public async Task Should_Return_Same_Instance_For_Same_ConnectionString() + { + // Arrange + var mongoUrl = new MongoUrl("mongodb://localhost:27017/my-db"); + + // Act + var client1 = await _factory.GetAsync(mongoUrl); + var client2 = await _factory.GetAsync(mongoUrl); + + // Assert + Assert.Same(client1, client2); + } + + [Fact] + public async Task Should_Return_Different_Instances_For_Different_ConnectionStrings() + { + // Arrange + var mongoUrl1 = new MongoUrl("mongodb://localhost:27017/db1"); + var mongoUrl2 = new MongoUrl("mongodb://localhost:27017/db2"); + + // Act + var client1 = await _factory.GetAsync(mongoUrl1); + var client2 = await _factory.GetAsync(mongoUrl2); + + // Assert + Assert.NotSame(client1, client2); + } + + [Fact] + public async Task Should_Not_Throw_For_Valid_But_Unreachable_Connection() + { + // Arrange + var mongoUrl = new MongoUrl("mongodb://unreachablehost:12345/any"); + + // Act + var client = await _factory.GetAsync(mongoUrl); + + // Assert + Assert.NotNull(client); // Even though it's not connectable now, the instance can be created + } + + [Fact] + public async Task Should_Be_ThreadSafe_When_Accessed_Concurrently() + { + var mongoUrl = new MongoUrl("mongodb://localhost:27017/threadsafe"); + var results = new MongoClient[100]; + + await Parallel.ForAsync(0, 100, async (i, _) => + { + results[i] = await _factory.GetAsync(mongoUrl); + }); + + Assert.All(results, client => Assert.Same(results[0], client)); + } + + [Fact] + public async Task Should_Throw_If_ConnectionString_Is_Null_Or_Empty() + { + await Assert.ThrowsAsync(() => _factory.GetAsync(null!)); + } +}