/* Copyright 2010-present MongoDB Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using MongoDB.Bson; using MongoDB.Bson.IO; using MongoDB.Bson.Serialization; using MongoDB.Bson.Serialization.Serializers; using MongoDB.Driver.Core.Clusters; using MongoDB.Driver.Core.Configuration; using MongoDB.Driver.Core.Logging; using MongoDB.Driver.Core.Misc; using MongoDB.Driver.Core.Operations; using MongoDB.Driver.Core.WireProtocol.Messages.Encoders; using MongoDB.Driver.Encryption; namespace MongoDB.Driver { /// public sealed class MongoClient : IMongoClient { // private fields private bool _disposed; #pragma warning disable CA2213 // Disposable fields should be disposed private readonly IClusterInternal _cluster; #pragma warning restore CA2213 // Disposable fields should be disposed private readonly IAutoEncryptionLibMongoCryptController _libMongoCryptController; private readonly Func _operationExecutorFactory; private readonly IOperationExecutor _operationExecutor; private readonly MongoClientSettings _settings; private readonly ILogger _logger; // constructors /// /// Initializes a new instance of the MongoClient class. /// public MongoClient() : this(new MongoClientSettings()) { } /// /// Initializes a new instance of the MongoClient class. /// /// The settings. public MongoClient(MongoClientSettings settings) : this(settings, client => new OperationExecutor(client)) { } /// /// Initializes a new instance of the MongoClient class. /// /// The URL. public MongoClient(MongoUrl url) : this(MongoClientSettings.FromUrl(url)) { } /// /// Initializes a new instance of the MongoClient class. /// /// The connection string. public MongoClient(string connectionString) : this(MongoClientSettings.FromConnectionString(connectionString)) { } internal MongoClient(MongoClientSettings settings, Func operationExecutorFactory) { _settings = Ensure.IsNotNull(settings, nameof(settings)).FrozenCopy(); _operationExecutorFactory = Ensure.IsNotNull(operationExecutorFactory, nameof(operationExecutorFactory)); _logger = _settings.LoggingSettings?.CreateLogger(); _cluster = _settings.ClusterSource.Get(_settings.ToClusterKey()); _operationExecutor = _operationExecutorFactory(this); if (settings.AutoEncryptionOptions != null) { _libMongoCryptController = MongoClientSettings.Extensions.AutoEncryptionProvider.CreateAutoCryptClientController(this, settings.AutoEncryptionOptions); _settings.LoggingSettings?.CreateLogger()?.LogTrace( StructuredLogTemplateProviders.TopologyId_Message_SharedLibraryVersion, _cluster.ClusterId, "CryptClient created. Configured shared library version: ", _libMongoCryptController.CryptSharedLibraryVersion() ?? "None"); } } // public properties /// public ICluster Cluster => ThrowIfDisposed(_cluster); /// public MongoClientSettings Settings => ThrowIfDisposed(_settings); // internal properties internal IAutoEncryptionLibMongoCryptController LibMongoCryptController => ThrowIfDisposed(_libMongoCryptController); // internal methods internal void ConfigureAutoEncryptionMessageEncoderSettings(MessageEncoderSettings messageEncoderSettings) { ThrowIfDisposed(); var autoEncryptionOptions = _settings.AutoEncryptionOptions; if (autoEncryptionOptions != null) { if (!autoEncryptionOptions.BypassAutoEncryption) { messageEncoderSettings.Add(MessageEncoderSettingsName.BinaryDocumentFieldEncryptor, _libMongoCryptController); } messageEncoderSettings.Add(MessageEncoderSettingsName.BinaryDocumentFieldDecryptor, _libMongoCryptController); } } // public methods /// /// Appends the specified library information to the metadata sent to the server in the connection handshake. /// Only connections opened after this call are affected. /// /// The library information to append. public void AppendMetadata(LibraryInfo libraryInfo) { ThrowIfDisposed(); Ensure.IsNotNull(libraryInfo, nameof(libraryInfo)); _cluster.AppendClientMetadata(libraryInfo); } /// public ClientBulkWriteResult BulkWrite(IReadOnlyList models, ClientBulkWriteOptions options = null, CancellationToken cancellationToken = default) { ThrowIfDisposed(); using var session = _operationExecutor.StartImplicitSession(); return BulkWrite(session, models, options, cancellationToken); } /// public ClientBulkWriteResult BulkWrite(IClientSessionHandle session, IReadOnlyList models, ClientBulkWriteOptions options = null, CancellationToken cancellationToken = default) { Ensure.IsNotNull(session, nameof(session)); ThrowIfDisposed(); var operation = CreateClientBulkWriteOperation(models, options); return ExecuteWriteOperation(session, operation, options?.Timeout, cancellationToken); } /// public async Task BulkWriteAsync(IReadOnlyList models, ClientBulkWriteOptions options = null, CancellationToken cancellationToken = default) { ThrowIfDisposed(); using var session = _operationExecutor.StartImplicitSession(); return await BulkWriteAsync(session, models, options, cancellationToken).ConfigureAwait(false); } /// public Task BulkWriteAsync(IClientSessionHandle session, IReadOnlyList models, ClientBulkWriteOptions options = null, CancellationToken cancellationToken = default) { Ensure.IsNotNull(session, nameof(session)); ThrowIfDisposed(); var operation = CreateClientBulkWriteOperation(models, options); return ExecuteWriteOperationAsync(session, operation, options?.Timeout, cancellationToken); } /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// /// Releases unmanaged and - optionally - managed resources. /// /// true to release both managed and unmanaged resources; false to release only unmanaged resources. public void Dispose(bool disposing) { if (!_disposed) { if (disposing) { _logger?.LogDebug(_cluster.ClusterId, "MongoClient disposing"); _operationExecutor.Dispose(); _settings.ClusterSource.Return(_cluster); _libMongoCryptController?.Dispose(); _logger?.LogDebug(_cluster.ClusterId, "MongoClient disposed"); } _disposed = true; } } /// public void DropDatabase(string name, CancellationToken cancellationToken = default) { ThrowIfDisposed(); using var session = _operationExecutor.StartImplicitSession(); DropDatabase(session, name, cancellationToken); } /// public void DropDatabase(IClientSessionHandle session, string name, CancellationToken cancellationToken = default) { Ensure.IsNotNull(session, nameof(session)); ThrowIfDisposed(); var operation = CreateDropDatabaseOperation(name); // TODO: CSOT: find a way to add timeout parameter to the interface method ExecuteWriteOperation(session, operation, null, cancellationToken); } /// public async Task DropDatabaseAsync(string name, CancellationToken cancellationToken = default) { ThrowIfDisposed(); using var session = _operationExecutor.StartImplicitSession(); await DropDatabaseAsync(session, name, cancellationToken).ConfigureAwait(false); } /// public Task DropDatabaseAsync(IClientSessionHandle session, string name, CancellationToken cancellationToken = default) { Ensure.IsNotNull(session, nameof(session)); ThrowIfDisposed(); var opertion = CreateDropDatabaseOperation(name); // TODO: CSOT: find a way to add timeout parameter to the interface method return ExecuteWriteOperationAsync(session, opertion, null, cancellationToken); } /// public IMongoDatabase GetDatabase(string name, MongoDatabaseSettings settings = null) { ThrowIfDisposed(); settings = settings == null ? new MongoDatabaseSettings() : settings.Clone(); settings.ApplyDefaultValues(_settings); return new MongoDatabase(this, new DatabaseNamespace(name), settings, _cluster, _operationExecutor); } /// public IAsyncCursor ListDatabaseNames(CancellationToken cancellationToken = default) => ListDatabaseNames(options: null, cancellationToken); /// public IAsyncCursor ListDatabaseNames( ListDatabaseNamesOptions options, CancellationToken cancellationToken = default) { ThrowIfDisposed(); using var session = _operationExecutor.StartImplicitSession(); return ListDatabaseNames(session, options, cancellationToken); } /// public IAsyncCursor ListDatabaseNames( IClientSessionHandle session, CancellationToken cancellationToken = default) => ListDatabaseNames(session, options: null, cancellationToken); /// public IAsyncCursor ListDatabaseNames( IClientSessionHandle session, ListDatabaseNamesOptions options, CancellationToken cancellationToken = default) { ThrowIfDisposed(); var listDatabasesOptions = CreateListDatabasesOptionsFromListDatabaseNamesOptions(options); var databases = ListDatabases(session, listDatabasesOptions, cancellationToken); return CreateDatabaseNamesCursor(databases); } /// public Task> ListDatabaseNamesAsync(CancellationToken cancellationToken = default) => ListDatabaseNamesAsync(options: null, cancellationToken); /// public async Task> ListDatabaseNamesAsync( ListDatabaseNamesOptions options, CancellationToken cancellationToken = default) { ThrowIfDisposed(); using var session = _operationExecutor.StartImplicitSession(); return await ListDatabaseNamesAsync(session, options, cancellationToken).ConfigureAwait(false); } /// public Task> ListDatabaseNamesAsync( IClientSessionHandle session, CancellationToken cancellationToken = default) => ListDatabaseNamesAsync(session, options: null, cancellationToken); /// public async Task> ListDatabaseNamesAsync( IClientSessionHandle session, ListDatabaseNamesOptions options, CancellationToken cancellationToken = default) { ThrowIfDisposed(); var listDatabasesOptions = CreateListDatabasesOptionsFromListDatabaseNamesOptions(options); var databases = await ListDatabasesAsync(session, listDatabasesOptions, cancellationToken).ConfigureAwait(false); return CreateDatabaseNamesCursor(databases); } /// public IAsyncCursor ListDatabases(CancellationToken cancellationToken) { ThrowIfDisposed(); using var session = _operationExecutor.StartImplicitSession(); return ListDatabases(session, cancellationToken); } /// public IAsyncCursor ListDatabases( ListDatabasesOptions options, CancellationToken cancellationToken = default) { ThrowIfDisposed(); using var session = _operationExecutor.StartImplicitSession(); return ListDatabases(session, options, cancellationToken); } /// public IAsyncCursor ListDatabases( IClientSessionHandle session, CancellationToken cancellationToken = default) => ListDatabases(session, null, cancellationToken); /// public IAsyncCursor ListDatabases( IClientSessionHandle session, ListDatabasesOptions options, CancellationToken cancellationToken = default) { ThrowIfDisposed(); Ensure.IsNotNull(session, nameof(session)); var operation = CreateListDatabasesOperation(options); return ExecuteReadOperation(session, operation, options?.Timeout, cancellationToken); } /// public async Task> ListDatabasesAsync(CancellationToken cancellationToken = default) { ThrowIfDisposed(); using var session = _operationExecutor.StartImplicitSession(); return await ListDatabasesAsync(session, cancellationToken).ConfigureAwait(false); } /// public async Task> ListDatabasesAsync( ListDatabasesOptions options, CancellationToken cancellationToken = default) { ThrowIfDisposed(); using var session = _operationExecutor.StartImplicitSession(); return await ListDatabasesAsync(session, options, cancellationToken).ConfigureAwait(false); } /// public Task> ListDatabasesAsync( IClientSessionHandle session, CancellationToken cancellationToken = default) => ListDatabasesAsync(session, null, cancellationToken); /// public Task> ListDatabasesAsync( IClientSessionHandle session, ListDatabasesOptions options, CancellationToken cancellationToken = default) { Ensure.IsNotNull(session, nameof(session)); ThrowIfDisposed(); var operation = CreateListDatabasesOperation(options); return ExecuteReadOperationAsync(session, operation, options?.Timeout, cancellationToken); } /// public IClientSessionHandle StartSession(ClientSessionOptions options = null, CancellationToken cancellationToken = default) { ThrowIfDisposed(); return StartSession(options); } /// public Task StartSessionAsync(ClientSessionOptions options = null, CancellationToken cancellationToken = default) { ThrowIfDisposed(); return Task.FromResult(StartSession(options)); } /// public IChangeStreamCursor Watch( PipelineDefinition, TResult> pipeline, ChangeStreamOptions options = null, CancellationToken cancellationToken = default) { ThrowIfDisposed(); using var session = _operationExecutor.StartImplicitSession(); return Watch(session, pipeline, options, cancellationToken); } /// public IChangeStreamCursor Watch( IClientSessionHandle session, PipelineDefinition, TResult> pipeline, ChangeStreamOptions options = null, CancellationToken cancellationToken = default) { Ensure.IsNotNull(session, nameof(session)); Ensure.IsNotNull(pipeline, nameof(pipeline)); ThrowIfDisposed(); var operation = CreateChangeStreamOperation(pipeline, options); return ExecuteReadOperation(session, operation, options?.Timeout, cancellationToken); } /// public async Task> WatchAsync( PipelineDefinition, TResult> pipeline, ChangeStreamOptions options = null, CancellationToken cancellationToken = default) { ThrowIfDisposed(); using var session = _operationExecutor.StartImplicitSession(); return await WatchAsync(session, pipeline, options, cancellationToken).ConfigureAwait(false); } /// public Task> WatchAsync( IClientSessionHandle session, PipelineDefinition, TResult> pipeline, ChangeStreamOptions options = null, CancellationToken cancellationToken = default) { Ensure.IsNotNull(session, nameof(session)); Ensure.IsNotNull(pipeline, nameof(pipeline)); ThrowIfDisposed(); var operation = CreateChangeStreamOperation(pipeline, options); return ExecuteReadOperationAsync(session, operation, options?.Timeout, cancellationToken); } /// public IMongoClient WithReadConcern(ReadConcern readConcern) { Ensure.IsNotNull(readConcern, nameof(readConcern)); ThrowIfDisposed(); var newSettings = Settings.Clone(); newSettings.ReadConcern = readConcern; return new MongoClient(newSettings, _operationExecutorFactory); } /// public IMongoClient WithReadPreference(ReadPreference readPreference) { Ensure.IsNotNull(readPreference, nameof(readPreference)); ThrowIfDisposed(); var newSettings = Settings.Clone(); newSettings.ReadPreference = readPreference; return new MongoClient(newSettings, _operationExecutorFactory); } /// public IMongoClient WithWriteConcern(WriteConcern writeConcern) { Ensure.IsNotNull(writeConcern, nameof(writeConcern)); ThrowIfDisposed(); var newSettings = Settings.Clone(); newSettings.WriteConcern = writeConcern; return new MongoClient(newSettings, _operationExecutorFactory); } // private methods private ClientBulkWriteOperation CreateClientBulkWriteOperation(IReadOnlyList models, ClientBulkWriteOptions options) { if (_settings.AutoEncryptionOptions != null) { throw new NotSupportedException("BulkWrite does not currently support automatic encryption."); } if (options?.WriteConcern?.IsAcknowledged == false && options?.IsOrdered == true) { throw new NotSupportedException("Cannot request unacknowledged write concern and ordered writes."); } if (options?.WriteConcern?.IsAcknowledged == false && options?.VerboseResult == true) { throw new NotSupportedException("Cannot request unacknowledged write concern and verbose results"); } var messageEncoderSettings = GetMessageEncoderSettings(); var renderArgs = GetRenderArgs(); var operation = new ClientBulkWriteOperation(models, options, messageEncoderSettings, renderArgs) { EnableOverloadRetargeting = _settings.EnableOverloadRetargeting, MaxAdaptiveRetries = _settings.MaxAdaptiveRetries, RetryRequested = _settings.RetryWrites }; if (options?.WriteConcern == null) { operation.WriteConcern = _settings.WriteConcern; } return operation; } private IAsyncCursor CreateDatabaseNamesCursor(IAsyncCursor cursor) => new BatchTransformingAsyncCursor( cursor, databases => databases.Select(database => database["name"].AsString)); private DropDatabaseOperation CreateDropDatabaseOperation(string name) => new(new DatabaseNamespace(name), GetMessageEncoderSettings()) { EnableOverloadRetargeting = _settings.EnableOverloadRetargeting, MaxAdaptiveRetries = _settings.MaxAdaptiveRetries, RetryRequested = _settings.RetryWrites, WriteConcern = _settings.WriteConcern }; private ListDatabasesOperation CreateListDatabasesOperation(ListDatabasesOptions options) { options ??= new ListDatabasesOptions(); var messageEncoderSettings = GetMessageEncoderSettings(); var translationOptions = _settings.TranslationOptions; return new ListDatabasesOperation(messageEncoderSettings) { AuthorizedDatabases = options.AuthorizedDatabases, Comment = options.Comment, EnableOverloadRetargeting = _settings.EnableOverloadRetargeting, Filter = options.Filter?.Render(new(BsonDocumentSerializer.Instance, BsonSerializer.SerializerRegistry, translationOptions: translationOptions)), MaxAdaptiveRetries = _settings.MaxAdaptiveRetries, NameOnly = options.NameOnly, RetryRequested = _settings.RetryReads }; } private ListDatabasesOptions CreateListDatabasesOptionsFromListDatabaseNamesOptions(ListDatabaseNamesOptions options) { var listDatabasesOptions = new ListDatabasesOptions { NameOnly = true }; if (options != null) { listDatabasesOptions.AuthorizedDatabases = options.AuthorizedDatabases; listDatabasesOptions.Filter = options.Filter; listDatabasesOptions.Comment = options.Comment; listDatabasesOptions.Timeout = options.Timeout; } return listDatabasesOptions; } private ChangeStreamOperation CreateChangeStreamOperation( PipelineDefinition, TResult> pipeline, ChangeStreamOptions options) => ChangeStreamHelper.CreateChangeStreamOperation( pipeline, options, _settings.ReadConcern, GetMessageEncoderSettings(), _settings.RetryReads, _settings.MaxAdaptiveRetries, _settings.EnableOverloadRetargeting, _settings.TranslationOptions); private OperationContext CreateOperationContext(IClientSessionHandle session, TimeSpan? timeout, string operationName, CancellationToken cancellationToken) { var operationContext = session.WrappedCoreSession.CurrentTransaction?.OperationContext; if (operationContext != null && timeout != null) { throw new InvalidOperationException("Cannot specify per operation timeout inside transaction."); } var tracingOptions = _settings.TracingOptions; var isTracingEnabled = tracingOptions?.Disabled != true && MongoTelemetry.ActivitySource.HasListeners(); if (operationContext != null) { return isTracingEnabled ? operationContext.ForkWithOperationMetadata(operationName, "admin", null, isTracingEnabled) : operationContext.Fork(); } return isTracingEnabled ? new OperationContext(timeout ?? _settings.Timeout, operationName, "admin", null, isTracingEnabled, cancellationToken) : new OperationContext(timeout ?? _settings.Timeout, cancellationToken); } private TResult ExecuteReadOperation(IClientSessionHandle session, IReadOperation operation, TimeSpan? timeout, CancellationToken cancellationToken) { var readPreference = session.GetEffectiveReadPreference(_settings.ReadPreference); using var operationContext = CreateOperationContext(session, timeout, operation.OperationName, cancellationToken); return _operationExecutor.ExecuteReadOperation(operationContext, session, operation, readPreference, false); } private async Task ExecuteReadOperationAsync(IClientSessionHandle session, IReadOperation operation, TimeSpan? timeout, CancellationToken cancellationToken) { var readPreference = session.GetEffectiveReadPreference(_settings.ReadPreference); using var operationContext = CreateOperationContext(session, timeout, operation.OperationName, cancellationToken); return await _operationExecutor.ExecuteReadOperationAsync(operationContext, session, operation, readPreference, false).ConfigureAwait(false); } private TResult ExecuteWriteOperation(IClientSessionHandle session, IWriteOperation operation, TimeSpan? timeout, CancellationToken cancellationToken) { using var operationContext = CreateOperationContext(session, timeout, operation.OperationName, cancellationToken); return _operationExecutor.ExecuteWriteOperation(operationContext, session, operation, false); } private async Task ExecuteWriteOperationAsync(IClientSessionHandle session, IWriteOperation operation, TimeSpan? timeout, CancellationToken cancellationToken) { using var operationContext = CreateOperationContext(session, timeout, operation.OperationName, cancellationToken); return await _operationExecutor.ExecuteWriteOperationAsync(operationContext, session, operation, false).ConfigureAwait(false); } private MessageEncoderSettings GetMessageEncoderSettings() { var messageEncoderSettings = new MessageEncoderSettings { { MessageEncoderSettingsName.ReadEncoding, _settings.ReadEncoding ?? Utf8Encodings.Strict }, { MessageEncoderSettingsName.WriteEncoding, _settings.WriteEncoding ?? Utf8Encodings.Strict } }; ConfigureAutoEncryptionMessageEncoderSettings(messageEncoderSettings); return messageEncoderSettings; } private RenderArgs GetRenderArgs() { var translationOptions = Settings.TranslationOptions; var serializerRegistry = BsonSerializer.SerializerRegistry; return new RenderArgs(BsonDocumentSerializer.Instance, serializerRegistry, translationOptions: translationOptions); } private IClientSessionHandle StartSession(ClientSessionOptions options) { if (options != null) { if (options.SnapshotTime != null && !options.Snapshot) { throw new InvalidOperationException("Specifying a snapshot time requires snapshot to be true."); } if (options.Snapshot && options.CausalConsistency == true) { throw new NotSupportedException("Combining both causal consistency and snapshot options is not supported."); } } options ??= new ClientSessionOptions(); if (_settings.Timeout.HasValue && options.DefaultTransactionOptions?.Timeout == null) { options.DefaultTransactionOptions = new TransactionOptions( _settings.Timeout, options.DefaultTransactionOptions?.ReadConcern, options.DefaultTransactionOptions?.ReadPreference, options.DefaultTransactionOptions?.WriteConcern, options.DefaultTransactionOptions?.MaxCommitTime); } var coreSession = _cluster.StartSession(options.ToCore( maxAdaptiveRetries: _settings.MaxAdaptiveRetries, enableOverloadRetargeting: _settings.EnableOverloadRetargeting)); return new ClientSessionHandle(this, options, coreSession); } private void ThrowIfDisposed() => ThrowIfDisposed(string.Empty); private T ThrowIfDisposed(T value) => _disposed ? throw new ObjectDisposedException(GetType().Name) : value; } }