using System; using System.Collections.Generic; using System.Configuration; using System.Data.Common; using System.Linq; using System.Threading; using NPoco; using NPoco.FluentMappings; using Umbraco.Core.Exceptions; using Umbraco.Core.Logging; using Umbraco.Core.Migrations.Install; 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. /// internal class UmbracoDatabaseFactory : DisposableObject, IUmbracoDatabaseFactory { private readonly ISqlSyntaxProvider[] _sqlSyntaxProviders; private readonly IMapperCollection _mappers; private readonly ILogger _logger; private readonly SqlContext _sqlContext = new SqlContext(); private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(); private DatabaseFactory _npocoDatabaseFactory; private IPocoDataFactory _pocoDataFactory; private string _connectionString; private string _providerName; private DbProviderFactory _dbProviderFactory; private DatabaseType _databaseType; private ISqlSyntaxProvider _sqlSyntax; private RetryPolicy _connectionRetryPolicy; private RetryPolicy _commandRetryPolicy; private NPoco.MapperCollection _pocoMappers; private bool _upgrading; #region Constructors /// /// Initializes a new instance of the . /// /// Used by LightInject. public UmbracoDatabaseFactory(IEnumerable sqlSyntaxProviders, ILogger logger, IMapperCollection mappers) : this(Constants.System.UmbracoConnectionName, sqlSyntaxProviders, logger, mappers) { if (Configured == false) DatabaseBuilder.GiveLegacyAChance(this, logger); } /// /// Initializes a new instance of the . /// /// Used by the other ctor and in tests. public UmbracoDatabaseFactory(string connectionStringName, IEnumerable sqlSyntaxProviders, ILogger logger, IMapperCollection mappers) { if (string.IsNullOrWhiteSpace(connectionStringName)) throw new ArgumentNullOrEmptyException(nameof(connectionStringName)); _mappers = mappers ?? throw new ArgumentNullException(nameof(mappers)); _sqlSyntaxProviders = sqlSyntaxProviders?.ToArray() ?? throw new ArgumentNullException(nameof(sqlSyntaxProviders)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); var settings = ConfigurationManager.ConnectionStrings[connectionStringName]; if (settings == null) 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("Missing 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(string connectionString, string providerName, IEnumerable sqlSyntaxProviders, ILogger logger, IMapperCollection mappers) { _mappers = mappers ?? throw new ArgumentNullException(nameof(mappers)); _sqlSyntaxProviders = sqlSyntaxProviders?.ToArray() ?? throw new ArgumentNullException(nameof(sqlSyntaxProviders)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); 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; private set; } /// public bool CanConnect => Configured && DbConnectionExtensions.IsConnectionAvailable(_connectionString, _providerName); /// public ISqlContext SqlContext => _sqlContext; /// public void ConfigureForUpgrade() { _upgrading = true; } /// public void Configure(string connectionString, string providerName) { using (new WriteLock(_lock)) { _logger.Debug("Configuring."); if (Configured) throw new InvalidOperationException("Already configured."); if (connectionString.IsNullOrWhiteSpace()) throw new ArgumentNullException(nameof(connectionString)); if (providerName.IsNullOrWhiteSpace()) throw new ArgumentNullException(nameof(providerName)); _connectionString = connectionString; _providerName = providerName; _connectionRetryPolicy = RetryPolicyFactory.GetDefaultSqlConnectionRetryPolicyByConnectionString(_connectionString); _commandRetryPolicy = RetryPolicyFactory.GetDefaultSqlCommandRetryPolicyByConnectionString(_connectionString); _dbProviderFactory = DbProviderFactories.GetFactory(_providerName); if (_dbProviderFactory == null) throw new Exception($"Can't find a provider factory for provider name \"{_providerName}\"."); _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 = GetSqlSyntaxProvider(_providerName); if (_sqlSyntax == null) throw new Exception($"Can't find a sql syntax provider for provider name \"{_providerName}\"."); // 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."); // can initialize now because it is the UmbracoDatabaseFactory that determines // the sql syntax, poco data factory, and database type _sqlContext.Initialize(_sqlSyntax, _databaseType, _pocoDataFactory, _mappers); _logger.Debug("Configured."); Configured = true; } } /// public IUmbracoDatabase CreateDatabase() { return (IUmbracoDatabase) _npocoDatabaseFactory.GetDatabase(); } // gets initialized poco data builders private InitializedPocoDataBuilder GetPocoDataFactoryResolver(Type type, IPocoDataFactory factory) => new UmbracoPocoDataBuilder(type, _pocoMappers, _upgrading).Init(); // gets the sql syntax provider that corresponds, from attribute private ISqlSyntaxProvider GetSqlSyntaxProvider(string providerName) { var name = providerName.ToLowerInvariant(); var provider = _sqlSyntaxProviders.FirstOrDefault(x => x.GetType() .FirstAttribute() .ProviderName.ToLowerInvariant() .Equals(name)); if (provider != null) return provider; throw new InvalidOperationException($"Unknown provider name \"{providerName}\""); // previously we'd try to return SqlServerSyntaxProvider by default but this is bad //provider = _syntaxProviders.FirstOrDefault(x => x.GetType() == typeof(SqlServerSyntaxProvider)); } // ensures that the database is configured, else throws private void EnsureConfigured() { _lock.EnterReadLock(); try { if (Configured == false) throw new InvalidOperationException("Not configured."); } finally { if (_lock.IsReadLockHeld) _lock.ExitReadLock(); } } // method used by NPoco's UmbracoDatabaseFactory to actually create the database instance private UmbracoDatabase CreateDatabaseInstance() { return new UmbracoDatabase(_connectionString, _sqlContext, _dbProviderFactory, _logger, _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... // fixme - does not make any sense! //var db = _umbracoDatabaseAccessor.UmbracoDatabase; //_umbracoDatabaseAccessor.UmbracoDatabase = null; //db?.Dispose(); Configured = false; } // during tests, the thread static var can leak between tests // this method provides a way to force-reset the variable internal void ResetForTests() { // fixme - does not make any sense! //var db = _umbracoDatabaseAccessor.UmbracoDatabase; //_umbracoDatabaseAccessor.UmbracoDatabase = null; //db?.Dispose(); //_databaseScopeAccessor.Scope = null; } } }