/* 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;
}
}