using System; using System.Data.Common; using System.Data.SqlClient; using System.Threading; using NPoco; using NPoco.FluentMappings; using Umbraco.Core.Composing; using Umbraco.Core.Configuration; using Umbraco.Core.Logging; using Umbraco.Core.Persistence.FaultHandling; using Umbraco.Core.Persistence.Mappers; using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence { /// /// Default implementation of . /// /// /// This factory implementation creates and manages an "ambient" database connection. When running /// within an Http context, "ambient" means "associated with that context". Otherwise, it means "static to /// the current thread". In this latter case, note that the database connection object is not thread safe. /// It wraps an NPoco UmbracoDatabaseFactory which is initializes with a proper IPocoDataFactory to ensure /// that NPoco's plumbing is cached appropriately for the whole application. /// // TODO: these comments are not true anymore // TODO: this class needs not be disposable! internal class UmbracoDatabaseFactory : DisposableObjectSlim, IUmbracoDatabaseFactory { private readonly IDbProviderFactoryCreator _dbProviderFactoryCreator; private readonly IGlobalSettings _globalSettings; private readonly Lazy _mappers; private readonly ILogger _logger; private object _lock = new object(); private DatabaseFactory _npocoDatabaseFactory; private IPocoDataFactory _pocoDataFactory; private string _providerName; private DatabaseType _databaseType; private ISqlSyntaxProvider _sqlSyntax; private IBulkSqlInsertProvider _bulkSqlInsertProvider; private RetryPolicy _connectionRetryPolicy; private RetryPolicy _commandRetryPolicy; private NPoco.MapperCollection _pocoMappers; private SqlContext _sqlContext; private bool _upgrading; private bool _initialized; private DbProviderFactory _dbProviderFactory = null; private DbProviderFactory DbProviderFactory { get { if (_dbProviderFactory == null) { _dbProviderFactory = string.IsNullOrWhiteSpace(_providerName) ? null : _dbProviderFactoryCreator.CreateFactory(_providerName); } return _dbProviderFactory; } } #region Constructors /// /// Initializes a new instance of the . /// /// Used by core runtime. public UmbracoDatabaseFactory(ILogger logger, IGlobalSettings globalSettings, IConnectionStrings connectionStrings, Lazy mappers,IDbProviderFactoryCreator dbProviderFactoryCreator) : this(logger, globalSettings, connectionStrings, Constants.System.UmbracoConnectionName, mappers, dbProviderFactoryCreator) { } /// /// Initializes a new instance of the . /// /// Used by the other ctor and in tests. public UmbracoDatabaseFactory(ILogger logger, IGlobalSettings globalSettings, IConnectionStrings connectionStrings, string connectionStringName, Lazy mappers, IDbProviderFactoryCreator dbProviderFactoryCreator) { if (connectionStringName == null) throw new ArgumentNullException(nameof(connectionStringName)); if (string.IsNullOrWhiteSpace(connectionStringName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(connectionStringName)); _globalSettings = globalSettings; _mappers = mappers ?? throw new ArgumentNullException(nameof(mappers)); _dbProviderFactoryCreator = dbProviderFactoryCreator ?? throw new ArgumentNullException(nameof(dbProviderFactoryCreator)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); var settings = connectionStrings[connectionStringName]; if (settings == null) { logger.Debug("Missing connection string, defer configuration."); return; // not configured } // could as well be // so need to test the values too var connectionString = settings.ConnectionString; var providerName = settings.ProviderName; if (string.IsNullOrWhiteSpace(connectionString) || string.IsNullOrWhiteSpace(providerName)) { logger.Debug("Empty connection string or provider name, defer configuration."); return; // not configured } Configure(settings.ConnectionString, settings.ProviderName); } /// /// Initializes a new instance of the . /// /// Used in tests. public UmbracoDatabaseFactory(ILogger logger, string connectionString, string providerName, Lazy mappers, IDbProviderFactoryCreator dbProviderFactoryCreator) { _mappers = mappers ?? throw new ArgumentNullException(nameof(mappers)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _dbProviderFactoryCreator = dbProviderFactoryCreator ?? throw new ArgumentNullException(nameof(dbProviderFactoryCreator)); if (string.IsNullOrWhiteSpace(connectionString) || string.IsNullOrWhiteSpace(providerName)) { logger.Debug("Missing connection string or provider name, defer configuration."); return; // not configured } Configure(connectionString, providerName); } #endregion /// public bool Configured { get { lock (_lock) { return !ConnectionString.IsNullOrWhiteSpace() && !_providerName.IsNullOrWhiteSpace(); } } } /// public bool Initialized => Volatile.Read(ref _initialized); /// public string ConnectionString { get; private set; } /// public bool CanConnect => // actually tries to connect to the database (regardless of configured/initialized) !ConnectionString.IsNullOrWhiteSpace() && !_providerName.IsNullOrWhiteSpace() && DbConnectionExtensions.IsConnectionAvailable(ConnectionString, DbProviderFactory); private void UpdateSqlServerDatabaseType() { // replace NPoco database type by a more efficient one var setting = _globalSettings.DatabaseFactoryServerVersion; var fromSettings = false; if (setting.IsNullOrWhiteSpace() || !setting.StartsWith("SqlServer.") || !Enum.TryParse(setting.Substring("SqlServer.".Length), out var versionName, true)) { versionName = ((SqlServerSyntaxProvider) _sqlSyntax).GetSetVersion(ConnectionString, _providerName, _logger).ProductVersionName; } else { fromSettings = true; } switch (versionName) { case SqlServerSyntaxProvider.VersionName.V2008: _databaseType = DatabaseType.SqlServer2008; break; case SqlServerSyntaxProvider.VersionName.V2012: case SqlServerSyntaxProvider.VersionName.V2014: case SqlServerSyntaxProvider.VersionName.V2016: case SqlServerSyntaxProvider.VersionName.V2017: case SqlServerSyntaxProvider.VersionName.V2019: _databaseType = DatabaseType.SqlServer2012; break; // else leave unchanged } _logger.Debug("SqlServer {SqlServerVersion}, DatabaseType is {DatabaseType} ({Source}).", versionName, _databaseType, fromSettings ? "settings" : "detected"); } /// public ISqlContext SqlContext { get { // must be initialized to have a context EnsureInitialized(); return _sqlContext; } } /// public IBulkSqlInsertProvider BulkSqlInsertProvider { get { // must be initialized to have a bulk insert provider EnsureInitialized(); return _bulkSqlInsertProvider; } } /// public void ConfigureForUpgrade() { _upgrading = true; } /// public void Configure(string connectionString, string providerName) { if (connectionString.IsNullOrWhiteSpace()) throw new ArgumentNullException(nameof(connectionString)); if (providerName.IsNullOrWhiteSpace()) throw new ArgumentNullException(nameof(providerName)); lock (_lock) { if (Volatile.Read(ref _initialized)) throw new InvalidOperationException("Already initialized."); ConnectionString = connectionString; _providerName = providerName; } // rest to be lazy-initialized } private void EnsureInitialized() { LazyInitializer.EnsureInitialized(ref _sqlContext, ref _initialized, ref _lock, Initialize); } private SqlContext Initialize() { _logger.Debug("Initializing."); if (ConnectionString.IsNullOrWhiteSpace()) throw new InvalidOperationException("The factory has not been configured with a proper connection string."); if (_providerName.IsNullOrWhiteSpace()) throw new InvalidOperationException("The factory has not been configured with a proper provider name."); if (DbProviderFactory == null) throw new Exception($"Can't find a provider factory for provider name \"{_providerName}\"."); // cannot initialize without being able to talk to the database // TODO: Why not? if (!DbConnectionExtensions.IsConnectionAvailable(ConnectionString, DbProviderFactory)) throw new Exception("Cannot connect to the database."); _connectionRetryPolicy = RetryPolicyFactory.GetDefaultSqlConnectionRetryPolicyByConnectionString(ConnectionString); _commandRetryPolicy = RetryPolicyFactory.GetDefaultSqlCommandRetryPolicyByConnectionString(ConnectionString); _databaseType = DatabaseType.Resolve(DbProviderFactory.GetType().Name, _providerName); if (_databaseType == null) throw new Exception($"Can't find an NPoco database type for provider name \"{_providerName}\"."); _sqlSyntax = _dbProviderFactoryCreator.GetSqlSyntaxProvider(_providerName); if (_sqlSyntax == null) throw new Exception($"Can't find a sql syntax provider for provider name \"{_providerName}\"."); _bulkSqlInsertProvider = _dbProviderFactoryCreator.CreateBulkSqlInsertProvider(_providerName); if (_databaseType.IsSqlServer()) UpdateSqlServerDatabaseType(); // ensure we have only 1 set of mappers, and 1 PocoDataFactory, for all database // so that everything NPoco is properly cached for the lifetime of the application _pocoMappers = new NPoco.MapperCollection { new PocoMapper() }; var factory = new FluentPocoDataFactory(GetPocoDataFactoryResolver); _pocoDataFactory = factory; var config = new FluentConfig(xmappers => factory); // create the database factory _npocoDatabaseFactory = DatabaseFactory.Config(x => x .UsingDatabase(CreateDatabaseInstance) // creating UmbracoDatabase instances .WithFluentConfig(config)); // with proper configuration if (_npocoDatabaseFactory == null) throw new NullReferenceException("The call to UmbracoDatabaseFactory.Config yielded a null UmbracoDatabaseFactory instance."); _logger.Debug("Initialized."); return new SqlContext(_sqlSyntax, _databaseType, _pocoDataFactory, _mappers); } /// public IUmbracoDatabase CreateDatabase() { // must be initialized to create a database EnsureInitialized(); return (IUmbracoDatabase) _npocoDatabaseFactory.GetDatabase(); } // gets initialized poco data builders private InitializedPocoDataBuilder GetPocoDataFactoryResolver(Type type, IPocoDataFactory factory) => new UmbracoPocoDataBuilder(type, _pocoMappers, _upgrading).Init(); // method used by NPoco's UmbracoDatabaseFactory to actually create the database instance private UmbracoDatabase CreateDatabaseInstance() { return new UmbracoDatabase(ConnectionString, SqlContext, DbProviderFactory, _logger, _bulkSqlInsertProvider, _connectionRetryPolicy, _commandRetryPolicy); } protected override void DisposeResources() { // this is weird, because hybrid accessors store different databases per // thread, so we don't really know what we are disposing here... // besides, we don't really want to dispose the factory, which is a singleton... // TODO: the class does not need be disposable //var db = _umbracoDatabaseAccessor.UmbracoDatabase; //_umbracoDatabaseAccessor.UmbracoDatabase = null; //db?.Dispose(); Volatile.Write(ref _initialized, false); } // during tests, the thread static var can leak between tests // this method provides a way to force-reset the variable internal void ResetForTests() { // TODO: remove all this eventually //var db = _umbracoDatabaseAccessor.UmbracoDatabase; //_umbracoDatabaseAccessor.UmbracoDatabase = null; //db?.Dispose(); //_databaseScopeAccessor.Scope = null; } } }