From 27043bec591e481c712e4ed7bac1906122f7d33b Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Wed, 15 Sep 2021 15:30:21 +0200 Subject: [PATCH 01/15] Remove custom SQL CE checks from IsConnectionStringConfigured --- .../ConfigConnectionStringExtensions.cs | 37 ++----------------- 1 file changed, 3 insertions(+), 34 deletions(-) diff --git a/src/Umbraco.Core/Extensions/ConfigConnectionStringExtensions.cs b/src/Umbraco.Core/Extensions/ConfigConnectionStringExtensions.cs index c1b788c0b9..329c9e8202 100644 --- a/src/Umbraco.Core/Extensions/ConfigConnectionStringExtensions.cs +++ b/src/Umbraco.Core/Extensions/ConfigConnectionStringExtensions.cs @@ -1,10 +1,6 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.IO; -using System.Linq; -using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration; namespace Umbraco.Extensions @@ -12,35 +8,8 @@ namespace Umbraco.Extensions public static class ConfigConnectionStringExtensions { public static bool IsConnectionStringConfigured(this ConfigConnectionString databaseSettings) - { - var dbIsSqlCe = false; - if (databaseSettings?.ProviderName != null) - { - dbIsSqlCe = databaseSettings.ProviderName == Constants.DbProviderNames.SqlCe; - } - - var sqlCeDatabaseExists = false; - if (dbIsSqlCe) - { - var parts = databaseSettings.ConnectionString.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries); - var dataSourcePart = parts.FirstOrDefault(x => x.InvariantStartsWith("Data Source=")); - if (dataSourcePart != null) - { - var datasource = dataSourcePart.Replace("|DataDirectory|", AppDomain.CurrentDomain.GetData("DataDirectory").ToString()); - var filePath = datasource.Replace("Data Source=", string.Empty); - sqlCeDatabaseExists = File.Exists(filePath); - } - } - - // Either the connection details are not fully specified or it's a SQL CE database that doesn't exist yet - if (databaseSettings == null - || string.IsNullOrWhiteSpace(databaseSettings.ConnectionString) || string.IsNullOrWhiteSpace(databaseSettings.ProviderName) - || (dbIsSqlCe && sqlCeDatabaseExists == false)) - { - return false; - } - - return true; - } + => databaseSettings != null && + !string.IsNullOrWhiteSpace(databaseSettings.ConnectionString) && + !string.IsNullOrWhiteSpace(databaseSettings.ProviderName); } } From aa15b0d244612fc44bf7e81cb1c146a8b4a5cebd Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Wed, 15 Sep 2021 15:41:20 +0200 Subject: [PATCH 02/15] Simplify parsing provider name from connection string --- .../Configuration/ConfigConnectionString.cs | 72 +++++++------------ .../Migrations/Install/DatabaseBuilder.cs | 2 +- .../Persistence/DbConnectionExtensions.cs | 39 ++++------ 3 files changed, 39 insertions(+), 74 deletions(-) diff --git a/src/Umbraco.Core/Configuration/ConfigConnectionString.cs b/src/Umbraco.Core/Configuration/ConfigConnectionString.cs index e88d1f4d01..d0dec2548c 100644 --- a/src/Umbraco.Core/Configuration/ConfigConnectionString.cs +++ b/src/Umbraco.Core/Configuration/ConfigConnectionString.cs @@ -5,69 +5,45 @@ namespace Umbraco.Cms.Core.Configuration { public class ConfigConnectionString { + public string Name { get; } + + public string ConnectionString { get; } + + public string ProviderName { get; } + public ConfigConnectionString(string name, string connectionString, string providerName = null) { Name = name ?? throw new ArgumentNullException(nameof(name)); ConnectionString = connectionString; - - ProviderName = string.IsNullOrEmpty(providerName) ? ParseProvider(connectionString) : providerName; + ProviderName = string.IsNullOrEmpty(providerName) ? ParseProviderName(connectionString) : providerName; } - public string ConnectionString { get; } - public string ProviderName { get; } - public string Name { get; } - - private static bool IsSqlCe(DbConnectionStringBuilder builder) => (builder.TryGetValue("Data Source", out var ds) - || builder.TryGetValue("DataSource", out ds)) && - ds is string dataSource && - dataSource.EndsWith(".sdf"); - - private static bool IsSqlServer(DbConnectionStringBuilder builder) => - !string.IsNullOrEmpty(GetServer(builder)) && - ((builder.TryGetValue("Database", out var db) && db is string database && - !string.IsNullOrEmpty(database)) || - (builder.TryGetValue("AttachDbFileName", out var a) && a is string attachDbFileName && - !string.IsNullOrEmpty(attachDbFileName)) || - (builder.TryGetValue("Initial Catalog", out var i) && i is string initialCatalog && - !string.IsNullOrEmpty(initialCatalog))); - - private static string GetServer(DbConnectionStringBuilder builder) - { - if(builder.TryGetValue("Server", out var s) && s is string server) - { - return server; - } - - if ((builder.TryGetValue("Data Source", out var ds) - || builder.TryGetValue("DataSource", out ds)) && ds is string dataSource) - { - return dataSource; - } - - return ""; - } - - private static string ParseProvider(string connectionString) + /// + /// Parses the connection string to get the provider name. + /// + /// The connection string. + /// + /// The provider name or null is the connection string is empty. + /// + public static string ParseProviderName(string connectionString) { if (string.IsNullOrEmpty(connectionString)) { return null; } - var builder = new DbConnectionStringBuilder {ConnectionString = connectionString}; - if (IsSqlCe(builder)) + var builder = new DbConnectionStringBuilder { - return Constants.DbProviderNames.SqlCe; + ConnectionString = connectionString + }; + + if ((builder.TryGetValue("Data Source", out var dataSource) || builder.TryGetValue("DataSource", out dataSource)) && + dataSource?.ToString().EndsWith(".sdf", StringComparison.OrdinalIgnoreCase) == true) + { + return Cms.Core.Constants.DbProviderNames.SqlCe; } - - if (IsSqlServer(builder)) - { - return Constants.DbProviderNames.SqlServer; - } - - throw new ArgumentException("Cannot determine provider name from connection string", - nameof(connectionString)); + return Cms.Core.Constants.DbProviderNames.SqlServer; } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs index b7437f4c2d..e4b8353e02 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs @@ -92,7 +92,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install if (string.IsNullOrWhiteSpace(connectionString) == false) { - providerName = DbConnectionExtensions.DetectProviderNameFromConnectionString(connectionString); + providerName = ConfigConnectionString.ParseProviderName(connectionString); } else if (integratedAuth) { diff --git a/src/Umbraco.Infrastructure/Persistence/DbConnectionExtensions.cs b/src/Umbraco.Infrastructure/Persistence/DbConnectionExtensions.cs index c4876aef7c..fb5d00abfd 100644 --- a/src/Umbraco.Infrastructure/Persistence/DbConnectionExtensions.cs +++ b/src/Umbraco.Infrastructure/Persistence/DbConnectionExtensions.cs @@ -1,35 +1,17 @@ -using System; +using System; using System.Data; using System.Data.Common; -using System.Linq; using Microsoft.Extensions.Logging; using StackExchange.Profiling.Data; using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.FaultHandling; -using Umbraco.Extensions; namespace Umbraco.Extensions { public static class DbConnectionExtensions { - public static string DetectProviderNameFromConnectionString(string connectionString) + public static bool IsConnectionAvailable(string connectionString, DbProviderFactory factory) { - var builder = new DbConnectionStringBuilder { ConnectionString = connectionString }; - var allKeys = builder.Keys.Cast(); - - if (allKeys.InvariantContains("Data Source") - //this dictionary is case insensitive - && builder["Data source"].ToString().InvariantContains(".sdf")) - { - return Cms.Core.Constants.DbProviderNames.SqlCe; - } - - return Cms.Core.Constants.DbProviderNames.SqlServer; - } - - public static bool IsConnectionAvailable(string connectionString, DbProviderFactory factory) - { - var connection = factory?.CreateConnection(); if (connection == null) @@ -42,7 +24,6 @@ namespace Umbraco.Extensions } } - public static bool IsAvailable(this IDbConnection connection) { try @@ -68,17 +49,25 @@ namespace Umbraco.Extensions internal static IDbConnection UnwrapUmbraco(this IDbConnection connection) { var unwrapped = connection; + IDbConnection c; do { c = unwrapped; - if (unwrapped is ProfiledDbConnection profiled) unwrapped = profiled.WrappedConnection; - if (unwrapped is RetryDbConnection retrying) unwrapped = retrying.Inner; - } while (c != unwrapped); + if (unwrapped is ProfiledDbConnection profiled) + { + unwrapped = profiled.WrappedConnection; + } + + if (unwrapped is RetryDbConnection retrying) + { + unwrapped = retrying.Inner; + } + } + while (c != unwrapped); return unwrapped; } - } } From 57610c96b937046405baea99950874442547e6b2 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Fri, 17 Sep 2021 11:57:20 +0200 Subject: [PATCH 03/15] Detect as brand new install if we can't connect and InstallMissingDatabase is enabled --- .../Install/InstallHelper.cs | 68 ++++++------------- 1 file changed, 19 insertions(+), 49 deletions(-) diff --git a/src/Umbraco.Infrastructure/Install/InstallHelper.cs b/src/Umbraco.Infrastructure/Install/InstallHelper.cs index 944ebbb586..6b5e882134 100644 --- a/src/Umbraco.Infrastructure/Install/InstallHelper.cs +++ b/src/Umbraco.Infrastructure/Install/InstallHelper.cs @@ -1,7 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -10,7 +7,6 @@ using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Install.Models; using Umbraco.Cms.Core.Net; -using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Web; using Umbraco.Cms.Infrastructure.Migrations.Install; @@ -30,6 +26,7 @@ namespace Umbraco.Cms.Infrastructure.Install private readonly ICookieManager _cookieManager; private readonly IUserAgentProvider _userAgentProvider; private readonly IUmbracoDatabaseFactory _umbracoDatabaseFactory; + private readonly IOptionsMonitor _globalSettings; private InstallationType? _installationType; public InstallHelper(DatabaseBuilder databaseBuilder, @@ -39,7 +36,8 @@ namespace Umbraco.Cms.Infrastructure.Install IInstallationService installationService, ICookieManager cookieManager, IUserAgentProvider userAgentProvider, - IUmbracoDatabaseFactory umbracoDatabaseFactory) + IUmbracoDatabaseFactory umbracoDatabaseFactory, + IOptionsMonitor globalSettings) { _logger = logger; _umbracoVersion = umbracoVersion; @@ -49,15 +47,13 @@ namespace Umbraco.Cms.Infrastructure.Install _cookieManager = cookieManager; _userAgentProvider = userAgentProvider; _umbracoDatabaseFactory = umbracoDatabaseFactory; + _globalSettings = globalSettings; - //We need to initialize the type already, as we can't detect later, if the connection string is added on the fly. + // We need to initialize the type already, as we can't detect later, if the connection string is added on the fly. GetInstallationType(); } - public InstallationType GetInstallationType() - { - return _installationType ?? (_installationType = IsBrandNewInstall ? InstallationType.NewInstall : InstallationType.Upgrade).Value; - } + public InstallationType GetInstallationType() => _installationType ??= IsBrandNewInstall ? InstallationType.NewInstall : InstallationType.Upgrade; public async Task SetInstallStatusAsync(bool isCompleted, string errorMsg) { @@ -65,25 +61,14 @@ namespace Umbraco.Cms.Infrastructure.Install { var userAgent = _userAgentProvider.GetUserAgent(); - // Check for current install Id - var installId = Guid.NewGuid(); - + // Check for current install ID var installCookie = _cookieManager.GetCookieValue(Constants.Web.InstallerCookieName); - if (string.IsNullOrEmpty(installCookie) == false) + if (!Guid.TryParse(installCookie, out var installId)) { - if (Guid.TryParse(installCookie, out installId)) - { - // check that it's a valid Guid - if (installId == Guid.Empty) - installId = Guid.NewGuid(); - } - else - { - installId = Guid.NewGuid(); // Guid.TryParse will have reset installId to Guid.Empty - } - } + installId = Guid.NewGuid(); - _cookieManager.SetCookieValue(Constants.Web.InstallerCookieName, installId.ToString()); + _cookieManager.SetCookieValue(Constants.Web.InstallerCookieName, installId.ToString()); + } var dbProvider = string.Empty; if (IsBrandNewInstall == false) @@ -108,29 +93,14 @@ namespace Umbraco.Cms.Infrastructure.Install } /// - /// Checks if this is a brand new install meaning that there is no configured version and there is no configured database connection + /// Checks if this is a brand new install, meaning that there is no configured database connection or the database is empty. /// - private bool IsBrandNewInstall - { - get - { - var databaseSettings = _connectionStrings.CurrentValue.UmbracoConnectionString; - if (databaseSettings.IsConnectionStringConfigured() == false) - { - //no version or conn string configured, must be a brand new install - return true; - } - - //now we have to check if this is really a new install, the db might be configured and might contain data - - if (databaseSettings.IsConnectionStringConfigured() == false - || _databaseBuilder.IsDatabaseConfigured == false) - { - return true; - } - - return _databaseBuilder.IsUmbracoInstalled() == false; - } - } + /// + /// true if this is a brand new install; otherwise, false. + /// + private bool IsBrandNewInstall => _connectionStrings.CurrentValue.UmbracoConnectionString?.IsConnectionStringConfigured() != true || + _databaseBuilder.IsDatabaseConfigured == false || + (_globalSettings.CurrentValue.InstallMissingDatabase && _databaseBuilder.CanConnectToDatabase == false) || + _databaseBuilder.IsUmbracoInstalled() == false; } } From b82fc3350fcf4b0bd2d730d310d2638927e38db9 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Fri, 17 Sep 2021 11:57:49 +0200 Subject: [PATCH 04/15] Set default UmbracoConnectionString value --- src/Umbraco.Core/Configuration/Models/ConnectionStrings.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Configuration/Models/ConnectionStrings.cs b/src/Umbraco.Core/Configuration/Models/ConnectionStrings.cs index bfcb41d992..c0421be7e3 100644 --- a/src/Umbraco.Core/Configuration/Models/ConnectionStrings.cs +++ b/src/Umbraco.Core/Configuration/Models/ConnectionStrings.cs @@ -26,6 +26,6 @@ namespace Umbraco.Cms.Core.Configuration.Models /// /// Gets or sets a value for the Umbraco database connection string.. /// - public ConfigConnectionString UmbracoConnectionString { get; set; } + public ConfigConnectionString UmbracoConnectionString { get; set; } = new ConfigConnectionString(Constants.System.UmbracoConnectionName, null); } } From 785630922e6c58e5b7409a098548487b40109a19 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Fri, 17 Sep 2021 12:02:41 +0200 Subject: [PATCH 05/15] Rename IEmbeddedDatabaseCreator to IDatabaseCreator and refactor implementations --- .../Persistence/DbProviderFactoryCreator.cs | 13 ++-- .../Persistence/IDatabaseCreator.cs | 9 +++ .../Persistence/IDbProviderFactoryCreator.cs | 2 +- .../Persistence/IEmbeddedDatabaseCreator.cs | 9 --- .../NoopEmbeddedDatabaseCreator.cs | 14 ----- .../Persistence/SqlServerDatabaseCreator.cs | 63 +++++++++++++++++++ .../SqlServerDbProviderFactoryCreator.cs | 26 +++----- .../SqlCeDatabaseCreator.cs | 16 +++++ .../SqlCeEmbeddedDatabaseCreator.cs | 18 ------ .../Persistence/DatabaseBuilderTests.cs | 8 +-- .../UmbracoBuilderExtensions.cs | 10 +-- 11 files changed, 114 insertions(+), 74 deletions(-) create mode 100644 src/Umbraco.Infrastructure/Persistence/IDatabaseCreator.cs delete mode 100644 src/Umbraco.Infrastructure/Persistence/IEmbeddedDatabaseCreator.cs delete mode 100644 src/Umbraco.Infrastructure/Persistence/NoopEmbeddedDatabaseCreator.cs create mode 100644 src/Umbraco.Infrastructure/Persistence/SqlServerDatabaseCreator.cs create mode 100644 src/Umbraco.Persistence.SqlCe/SqlCeDatabaseCreator.cs delete mode 100644 src/Umbraco.Persistence.SqlCe/SqlCeEmbeddedDatabaseCreator.cs diff --git a/src/Umbraco.Infrastructure/Persistence/DbProviderFactoryCreator.cs b/src/Umbraco.Infrastructure/Persistence/DbProviderFactoryCreator.cs index 942368f5cb..e54c1f5fbc 100644 --- a/src/Umbraco.Infrastructure/Persistence/DbProviderFactoryCreator.cs +++ b/src/Umbraco.Infrastructure/Persistence/DbProviderFactoryCreator.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Data.Common; using System.Linq; using NPoco; -using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; namespace Umbraco.Cms.Infrastructure.Persistence @@ -11,7 +10,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence public class DbProviderFactoryCreator : IDbProviderFactoryCreator { private readonly Func _getFactory; - private readonly IDictionary _embeddedDatabaseCreators; + private readonly IDictionary _databaseCreators; private readonly IDictionary _syntaxProviders; private readonly IDictionary _bulkSqlInsertProviders; private readonly IDictionary _providerSpecificMapperFactories; @@ -20,11 +19,11 @@ namespace Umbraco.Cms.Infrastructure.Persistence Func getFactory, IEnumerable syntaxProviders, IEnumerable bulkSqlInsertProviders, - IEnumerable embeddedDatabaseCreators, + IEnumerable databaseCreators, IEnumerable providerSpecificMapperFactories) { _getFactory = getFactory; - _embeddedDatabaseCreators = embeddedDatabaseCreators.ToDictionary(x => x.ProviderName); + _databaseCreators = databaseCreators.ToDictionary(x => x.ProviderName); _syntaxProviders = syntaxProviders.ToDictionary(x => x.ProviderName); _bulkSqlInsertProviders = bulkSqlInsertProviders.ToDictionary(x => x.ProviderName); _providerSpecificMapperFactories = providerSpecificMapperFactories.ToDictionary(x => x.ProviderName); @@ -60,11 +59,11 @@ namespace Umbraco.Cms.Infrastructure.Persistence return result; } - public void CreateDatabase(string providerName) + public void CreateDatabase(string providerName, string connectionString) { - if (_embeddedDatabaseCreators.TryGetValue(providerName, out var creator)) + if (_databaseCreators.TryGetValue(providerName, out var creator)) { - creator.Create(); + creator.Create(connectionString); } } diff --git a/src/Umbraco.Infrastructure/Persistence/IDatabaseCreator.cs b/src/Umbraco.Infrastructure/Persistence/IDatabaseCreator.cs new file mode 100644 index 0000000000..2d97cfbcd3 --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/IDatabaseCreator.cs @@ -0,0 +1,9 @@ +namespace Umbraco.Cms.Infrastructure.Persistence +{ + public interface IDatabaseCreator + { + string ProviderName { get; } + + void Create(string connectionString); + } +} diff --git a/src/Umbraco.Infrastructure/Persistence/IDbProviderFactoryCreator.cs b/src/Umbraco.Infrastructure/Persistence/IDbProviderFactoryCreator.cs index 99b7469085..6a38dc3c06 100644 --- a/src/Umbraco.Infrastructure/Persistence/IDbProviderFactoryCreator.cs +++ b/src/Umbraco.Infrastructure/Persistence/IDbProviderFactoryCreator.cs @@ -9,7 +9,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence DbProviderFactory CreateFactory(string providerName); ISqlSyntaxProvider GetSqlSyntaxProvider(string providerName); IBulkSqlInsertProvider CreateBulkSqlInsertProvider(string providerName); - void CreateDatabase(string providerName); + void CreateDatabase(string providerName, string connectionString); NPocoMapperCollection ProviderSpecificMappers(string providerName); } } diff --git a/src/Umbraco.Infrastructure/Persistence/IEmbeddedDatabaseCreator.cs b/src/Umbraco.Infrastructure/Persistence/IEmbeddedDatabaseCreator.cs deleted file mode 100644 index 2461644d0a..0000000000 --- a/src/Umbraco.Infrastructure/Persistence/IEmbeddedDatabaseCreator.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Umbraco.Cms.Infrastructure.Persistence -{ - public interface IEmbeddedDatabaseCreator - { - string ProviderName { get; } - string ConnectionString { get; set; } - void Create(); - } -} diff --git a/src/Umbraco.Infrastructure/Persistence/NoopEmbeddedDatabaseCreator.cs b/src/Umbraco.Infrastructure/Persistence/NoopEmbeddedDatabaseCreator.cs deleted file mode 100644 index fd378d44bc..0000000000 --- a/src/Umbraco.Infrastructure/Persistence/NoopEmbeddedDatabaseCreator.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Umbraco.Cms.Infrastructure.Persistence -{ - public class NoopEmbeddedDatabaseCreator : IEmbeddedDatabaseCreator - { - public string ProviderName => Cms.Core.Constants.DatabaseProviders.SqlServer; - - public string ConnectionString { get; set; } - - public void Create() - { - - } - } -} diff --git a/src/Umbraco.Infrastructure/Persistence/SqlServerDatabaseCreator.cs b/src/Umbraco.Infrastructure/Persistence/SqlServerDatabaseCreator.cs new file mode 100644 index 0000000000..e7f5934e78 --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/SqlServerDatabaseCreator.cs @@ -0,0 +1,63 @@ +using System; +using System.Data.SqlClient; +using System.IO; +using Umbraco.Cms.Core; + +namespace Umbraco.Cms.Infrastructure.Persistence +{ + public class SqlServerDatabaseCreator : IDatabaseCreator + { + public string ProviderName => Constants.DatabaseProviders.SqlServer; + + public void Create(string connectionString) + { + var builder = new SqlConnectionStringBuilder(connectionString); + + // Get connection string without database specific information + var masterBuilder = new SqlConnectionStringBuilder(builder.ConnectionString) + { + AttachDBFilename = string.Empty, + InitialCatalog = string.Empty + }; + var masterConnectionString = masterBuilder.ConnectionString; + + string fileName = builder.AttachDBFilename, + database = builder.InitialCatalog; + + // Create database + if (!string.IsNullOrEmpty(fileName) && !File.Exists(fileName)) + { + if (string.IsNullOrWhiteSpace(database)) + { + // Use a temporary database name + database = "Umbraco-" + Guid.NewGuid(); + } + + using var connection = new SqlConnection(masterConnectionString); + connection.Open(); + + using var command = new SqlCommand( + $"CREATE DATABASE [{database}] ON (NAME='{database}', FILENAME='{fileName}');" + + $"ALTER DATABASE [{database}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE;" + + $"EXEC sp_detach_db @dbname='{database}';", + connection); + command.ExecuteNonQuery(); + + connection.Close(); + } + else if (!string.IsNullOrEmpty(database)) + { + using var connection = new SqlConnection(masterConnectionString); + connection.Open(); + + using var command = new SqlCommand( + $"IF NOT EXISTS (SELECT * FROM sys.databases WHERE name = '{database}') " + + $"CREATE DATABASE [{database}];", + connection); + command.ExecuteNonQuery(); + + connection.Close(); + } + } + } +} diff --git a/src/Umbraco.Infrastructure/Persistence/SqlServerDbProviderFactoryCreator.cs b/src/Umbraco.Infrastructure/Persistence/SqlServerDbProviderFactoryCreator.cs index 756490c531..c16e5a75ab 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlServerDbProviderFactoryCreator.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlServerDbProviderFactoryCreator.cs @@ -3,12 +3,12 @@ using System.Data.Common; using System.Linq; using Microsoft.Extensions.Options; using NPoco; -using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; namespace Umbraco.Cms.Infrastructure.Persistence { + [Obsolete("This is only used for integration tests and should be moved into a test project.")] public class SqlServerDbProviderFactoryCreator : IDbProviderFactoryCreator { private readonly Func _getFactory; @@ -23,35 +23,29 @@ namespace Umbraco.Cms.Infrastructure.Persistence public DbProviderFactory CreateFactory(string providerName) { if (string.IsNullOrEmpty(providerName)) return null; + return _getFactory(providerName); } // gets the sql syntax provider that corresponds, from attribute public ISqlSyntaxProvider GetSqlSyntaxProvider(string providerName) - { - return providerName switch + => providerName switch { Cms.Core.Constants.DbProviderNames.SqlCe => throw new NotSupportedException("SqlCe is not supported"), Cms.Core.Constants.DbProviderNames.SqlServer => new SqlServerSyntaxProvider(_globalSettings), _ => throw new InvalidOperationException($"Unknown provider name \"{providerName}\""), }; - } public IBulkSqlInsertProvider CreateBulkSqlInsertProvider(string providerName) - { - switch (providerName) + => providerName switch { - case Cms.Core.Constants.DbProviderNames.SqlCe: - throw new NotSupportedException("SqlCe is not supported"); - case Cms.Core.Constants.DbProviderNames.SqlServer: - return new SqlServerBulkSqlInsertProvider(); - default: - return new BasicBulkSqlInsertProvider(); - } - } + Cms.Core.Constants.DbProviderNames.SqlCe => throw new NotSupportedException("SqlCe is not supported"), + Cms.Core.Constants.DbProviderNames.SqlServer => new SqlServerBulkSqlInsertProvider(), + _ => new BasicBulkSqlInsertProvider(), + }; - public void CreateDatabase(string providerName) - => throw new NotSupportedException("Embedded databases are not supported"); + public void CreateDatabase(string providerName, string connectionString) + => throw new NotSupportedException("Embedded databases are not supported"); // TODO But LocalDB is? public NPocoMapperCollection ProviderSpecificMappers(string providerName) => new NPocoMapperCollection(() => Enumerable.Empty()); diff --git a/src/Umbraco.Persistence.SqlCe/SqlCeDatabaseCreator.cs b/src/Umbraco.Persistence.SqlCe/SqlCeDatabaseCreator.cs new file mode 100644 index 0000000000..fd360be13a --- /dev/null +++ b/src/Umbraco.Persistence.SqlCe/SqlCeDatabaseCreator.cs @@ -0,0 +1,16 @@ +using Umbraco.Cms.Infrastructure.Persistence; +using Constants = Umbraco.Cms.Core.Constants; + +namespace Umbraco.Cms.Persistence.SqlCe +{ + public class SqlCeDatabaseCreator : IDatabaseCreator + { + public string ProviderName => Constants.DatabaseProviders.SqlCe; + + public void Create(string connectionString) + { + using var engine = new System.Data.SqlServerCe.SqlCeEngine(connectionString); + engine.CreateDatabase(); + } + } +} diff --git a/src/Umbraco.Persistence.SqlCe/SqlCeEmbeddedDatabaseCreator.cs b/src/Umbraco.Persistence.SqlCe/SqlCeEmbeddedDatabaseCreator.cs deleted file mode 100644 index ee2d888109..0000000000 --- a/src/Umbraco.Persistence.SqlCe/SqlCeEmbeddedDatabaseCreator.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Umbraco.Cms.Infrastructure.Migrations.Install; -using Umbraco.Cms.Infrastructure.Persistence; -using Constants = Umbraco.Cms.Core.Constants; - -namespace Umbraco.Cms.Persistence.SqlCe -{ - public class SqlCeEmbeddedDatabaseCreator : IEmbeddedDatabaseCreator - { - public string ProviderName => Constants.DatabaseProviders.SqlCe; - - public string ConnectionString { get; set; } = DatabaseBuilder.EmbeddedDatabaseConnectionString; - public void Create() - { - var engine = new System.Data.SqlServerCe.SqlCeEngine(ConnectionString); - engine.CreateDatabase(); - } - } -} diff --git a/src/Umbraco.Tests.Integration.SqlCe/Umbraco.Infrastructure/Persistence/DatabaseBuilderTests.cs b/src/Umbraco.Tests.Integration.SqlCe/Umbraco.Infrastructure/Persistence/DatabaseBuilderTests.cs index e8269fafa9..500d71dddc 100644 --- a/src/Umbraco.Tests.Integration.SqlCe/Umbraco.Infrastructure/Persistence/DatabaseBuilderTests.cs +++ b/src/Umbraco.Tests.Integration.SqlCe/Umbraco.Infrastructure/Persistence/DatabaseBuilderTests.cs @@ -23,7 +23,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence { private IDbProviderFactoryCreator DbProviderFactoryCreator => GetRequiredService(); private IUmbracoDatabaseFactory UmbracoDatabaseFactory => GetRequiredService(); - private IEmbeddedDatabaseCreator EmbeddedDatabaseCreator => GetRequiredService(); + private IDatabaseCreator EmbeddedDatabaseCreator => GetRequiredService(); public DatabaseBuilderTests() { @@ -42,10 +42,10 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence if (File.Exists(filePath)) File.Delete(filePath); - EmbeddedDatabaseCreator.ConnectionString = $"Datasource=|DataDirectory|{dbFile};Flush Interval=1"; + var connectionString = $"Datasource=|DataDirectory|{dbFile};Flush Interval=1"; - UmbracoDatabaseFactory.Configure(EmbeddedDatabaseCreator.ConnectionString, Constants.DbProviderNames.SqlCe); - DbProviderFactoryCreator.CreateDatabase(Constants.DbProviderNames.SqlCe); + UmbracoDatabaseFactory.Configure(connectionString, Constants.DbProviderNames.SqlCe); + DbProviderFactoryCreator.CreateDatabase(Constants.DbProviderNames.SqlCe, connectionString); UmbracoDatabaseFactory.CreateDatabase(); // test get database type (requires an actual database) diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs index 5073eefe2f..4319bc3544 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs @@ -150,7 +150,7 @@ namespace Umbraco.Extensions DbProviderFactories.GetFactory, factory.GetServices(), factory.GetServices(), - factory.GetServices(), + factory.GetServices(), factory.GetServices() )); @@ -357,17 +357,17 @@ namespace Umbraco.Extensions Type sqlCeSyntaxProviderType = umbSqlCeAssembly.GetType("Umbraco.Cms.Persistence.SqlCe.SqlCeSyntaxProvider"); Type sqlCeBulkSqlInsertProviderType = umbSqlCeAssembly.GetType("Umbraco.Cms.Persistence.SqlCe.SqlCeBulkSqlInsertProvider"); - Type sqlCeEmbeddedDatabaseCreatorType = umbSqlCeAssembly.GetType("Umbraco.Cms.Persistence.SqlCe.SqlCeEmbeddedDatabaseCreator"); + Type sqlCeDatabaseCreatorType = umbSqlCeAssembly.GetType("Umbraco.Cms.Persistence.SqlCe.SqlCeDatabaseCreator"); Type sqlCeSpecificMapperFactory = umbSqlCeAssembly.GetType("Umbraco.Cms.Persistence.SqlCe.SqlCeSpecificMapperFactory"); if (!(sqlCeSyntaxProviderType is null || sqlCeBulkSqlInsertProviderType is null - || sqlCeEmbeddedDatabaseCreatorType is null + || sqlCeDatabaseCreatorType is null || sqlCeSpecificMapperFactory is null)) { builder.Services.AddSingleton(typeof(ISqlSyntaxProvider), sqlCeSyntaxProviderType); builder.Services.AddSingleton(typeof(IBulkSqlInsertProvider), sqlCeBulkSqlInsertProviderType); - builder.Services.AddSingleton(typeof(IEmbeddedDatabaseCreator), sqlCeEmbeddedDatabaseCreatorType); + builder.Services.AddSingleton(typeof(IDatabaseCreator), sqlCeDatabaseCreatorType); builder.Services.AddSingleton(typeof(IProviderSpecificMapperFactory), sqlCeSpecificMapperFactory); } @@ -397,7 +397,7 @@ namespace Umbraco.Extensions builder.Services.AddSingleton(); builder.Services.AddSingleton(); - builder.Services.AddSingleton(); + builder.Services.AddSingleton(); return builder; } From 81cbbd861455e25e2a52237c59a2f64d52edb204 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Fri, 17 Sep 2021 12:50:25 +0200 Subject: [PATCH 06/15] Add LocalDB database option to installer --- .../Install/Models/DatabaseModel.cs | 4 +- .../Install/Models/DatabaseType.cs | 3 +- .../InstallSteps/DatabaseConfigureStep.cs | 33 +++++++---- .../InstallSteps/DatabaseInstallStep.cs | 14 ++++- .../Migrations/Install/DatabaseBuilder.cs | 56 ++++++++++-------- .../Persistence/UmbracoDatabaseFactory.cs | 20 ++----- .../src/installer/steps/database.html | 58 +++++++++++-------- 7 files changed, 107 insertions(+), 81 deletions(-) diff --git a/src/Umbraco.Core/Install/Models/DatabaseModel.cs b/src/Umbraco.Core/Install/Models/DatabaseModel.cs index c7f4ce0aab..514500f445 100644 --- a/src/Umbraco.Core/Install/Models/DatabaseModel.cs +++ b/src/Umbraco.Core/Install/Models/DatabaseModel.cs @@ -1,4 +1,4 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; namespace Umbraco.Cms.Core.Install.Models { @@ -8,7 +8,7 @@ namespace Umbraco.Cms.Core.Install.Models public DatabaseModel() { //defaults - DatabaseType = DatabaseType.SqlCe; + DatabaseType = DatabaseType.SqlLocalDb; } [DataMember(Name = "dbType")] diff --git a/src/Umbraco.Core/Install/Models/DatabaseType.cs b/src/Umbraco.Core/Install/Models/DatabaseType.cs index 5eef471562..bc0616620f 100644 --- a/src/Umbraco.Core/Install/Models/DatabaseType.cs +++ b/src/Umbraco.Core/Install/Models/DatabaseType.cs @@ -1,7 +1,8 @@ -namespace Umbraco.Cms.Core.Install.Models +namespace Umbraco.Cms.Core.Install.Models { public enum DatabaseType { + SqlLocalDb, SqlCe, SqlServer, SqlAzure, diff --git a/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseConfigureStep.cs b/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseConfigureStep.cs index 7b02ea786e..a14b0f3a1c 100644 --- a/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseConfigureStep.cs +++ b/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseConfigureStep.cs @@ -7,6 +7,7 @@ using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Install; using Umbraco.Cms.Core.Install.Models; using Umbraco.Cms.Infrastructure.Migrations.Install; +using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Install.InstallSteps @@ -39,7 +40,9 @@ namespace Umbraco.Cms.Infrastructure.Install.InstallSteps { throw new InstallException("Could not connect to the database"); } + ConfigureConnection(database); + return Task.FromResult(null); } @@ -49,6 +52,10 @@ namespace Umbraco.Cms.Infrastructure.Install.InstallSteps { _databaseBuilder.ConfigureDatabaseConnection(database.ConnectionString); } + else if (database.DatabaseType == DatabaseType.SqlLocalDb) + { + _databaseBuilder.ConfigureSqlLocalDbDatabaseConnection(); + } else if (database.DatabaseType == DatabaseType.SqlCe) { _databaseBuilder.ConfigureEmbeddedDatabaseConnection(); @@ -62,9 +69,7 @@ namespace Umbraco.Cms.Infrastructure.Install.InstallSteps var password = database.Password.Replace("'", "''"); password = string.Format("'{0}'", password); - _databaseBuilder.ConfigureDatabaseConnection( - database.Server, database.DatabaseName, database.Login, password, - database.DatabaseType.ToString()); + _databaseBuilder.ConfigureDatabaseConnection(database.Server, database.DatabaseName, database.Login, password, database.DatabaseType.ToString()); } } @@ -84,28 +89,31 @@ namespace Umbraco.Cms.Infrastructure.Install.InstallSteps databases.Insert(0, new { name = "Microsoft SQL Server Compact (SQL CE)", id = DatabaseType.SqlCe.ToString() }); } + if (IsLocalDbAvailable()) + { + // Ensure this is always inserted as first when available + databases.Insert(0, new { name = "Microsoft SQL Server Express (LocalDB)", id = DatabaseType.SqlLocalDb.ToString() }); + } + return new { - databases = databases + databases }; } } - public static bool IsSqlCeAvailable() - { + public static bool IsLocalDbAvailable() => new LocalDb().IsAvailable; + + public static bool IsSqlCeAvailable() => // NOTE: Type.GetType will only return types that are currently loaded into the appdomain. In this case // that is ok because we know if this is availalbe we will have manually loaded it into the appdomain. // Else we'd have to use Assembly.LoadFrom and need to know the DLL location here which we don't need to do. - return !(Type.GetType("Umbraco.Cms.Persistence.SqlCe.SqlCeSyntaxProvider, Umbraco.Persistence.SqlCe") is null); - } + !(Type.GetType("Umbraco.Cms.Persistence.SqlCe.SqlCeSyntaxProvider, Umbraco.Persistence.SqlCe") is null); public override string View => ShouldDisplayView() ? base.View : ""; - public override bool RequiresExecution(DatabaseModel model) - { - return ShouldDisplayView(); - } + public override bool RequiresExecution(DatabaseModel model) => ShouldDisplayView(); private bool ShouldDisplayView() { @@ -118,6 +126,7 @@ namespace Umbraco.Cms.Infrastructure.Install.InstallSteps { //Since a connection string was present we verify the db can connect and query _ = _databaseBuilder.ValidateSchema(); + return false; } catch (Exception ex) diff --git a/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseInstallStep.cs b/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseInstallStep.cs index 61d78173fa..1c58d04810 100644 --- a/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseInstallStep.cs +++ b/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseInstallStep.cs @@ -1,11 +1,14 @@ -using System; +using System; using System.Collections.Generic; using System.Threading.Tasks; +using Microsoft.Extensions.Options; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Install; using Umbraco.Cms.Core.Install.Models; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Migrations.Install; +using Umbraco.Cms.Infrastructure.Persistence; namespace Umbraco.Cms.Infrastructure.Install.InstallSteps { @@ -15,11 +18,13 @@ namespace Umbraco.Cms.Infrastructure.Install.InstallSteps { private readonly DatabaseBuilder _databaseBuilder; private readonly IRuntimeState _runtime; + private readonly IOptionsMonitor _globalSettings; - public DatabaseInstallStep(DatabaseBuilder databaseBuilder, IRuntimeState runtime) + public DatabaseInstallStep(DatabaseBuilder databaseBuilder, IRuntimeState runtime, IOptionsMonitor globalSettings) { _databaseBuilder = databaseBuilder; _runtime = runtime; + _globalSettings = globalSettings; } public override Task ExecuteAsync(object model) @@ -27,6 +32,11 @@ namespace Umbraco.Cms.Infrastructure.Install.InstallSteps if (_runtime.Level == RuntimeLevel.Run) throw new Exception("Umbraco is already configured!"); + if (_globalSettings.CurrentValue.InstallMissingDatabase) + { + _databaseBuilder.CreateDatabase(); + } + var result = _databaseBuilder.CreateSchemaAndData(); if (result.Success == false) diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs index e4b8353e02..8b9b2b07cd 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs @@ -1,6 +1,4 @@ using System; -using System.IO; -using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration; @@ -85,7 +83,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install public bool CanConnect(string databaseType, string connectionString, string server, string database, string login, string password, bool integratedAuth) { // we do not test SqlCE connection - if (databaseType.InvariantContains("sqlce")) + if (databaseType.InvariantContains("sqlce") || databaseType.InvariantContains("localdb")) return true; string providerName; @@ -153,23 +151,32 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install /// public void ConfigureEmbeddedDatabaseConnection() { - ConfigureEmbeddedDatabaseConnection(_databaseFactory); + const string connectionString = EmbeddedDatabaseConnectionString; + const string providerName = Constants.DbProviderNames.SqlCe; + + _configManipulator.SaveConnectionString(connectionString, providerName); + _databaseFactory.Configure(connectionString, providerName); + + // Always create embedded database + CreateDatabase(); } - private void ConfigureEmbeddedDatabaseConnection(IUmbracoDatabaseFactory factory) + public const string LocalDbConnectionString = @"Server=(localdb)\MSSQLLocalDB;Integrated Security=true;AttachDbFileName=|DataDirectory|\Umbraco.mdf"; + + public void ConfigureSqlLocalDbDatabaseConnection() { - _configManipulator.SaveConnectionString(EmbeddedDatabaseConnectionString, Constants.DbProviderNames.SqlCe); + string connectionString = LocalDbConnectionString; + const string providerName = Constants.DbProviderNames.SqlServer; - var path = _hostingEnvironment.MapPathContentRoot(Path.Combine(Constants.SystemDirectories.Data, "Umbraco.sdf")); - if (File.Exists(path) == false) - { - // this should probably be in a "using (new SqlCeEngine)" clause but not sure - // of the side effects and it's been like this for quite some time now + // Replace data directory placeholder (this is not supported by LocalDB) + var dataDirectory = AppDomain.CurrentDomain.GetData("DataDirectory")?.ToString(); + connectionString = connectionString.Replace("|DataDirectory|", dataDirectory); - _dbProviderFactoryCreator.CreateDatabase(Constants.DbProviderNames.SqlCe); - } + _configManipulator.SaveConnectionString(connectionString, providerName); + _databaseFactory.Configure(connectionString, providerName); - factory.Configure(EmbeddedDatabaseConnectionString, Constants.DbProviderNames.SqlCe); + // Always create LocalDB database + CreateDatabase(); } /// @@ -214,9 +221,10 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install public static string GetDatabaseConnectionString(string server, string databaseName, string user, string password, string databaseProvider, out string providerName) { providerName = Constants.DbProviderNames.SqlServer; - var provider = databaseProvider.ToLower(); - if (provider.InvariantContains("azure")) + + if (databaseProvider.InvariantContains("Azure")) return GetAzureConnectionString(server, databaseName, user, password); + return $"server={server};database={databaseName};user id={user};password={password}"; } @@ -228,8 +236,10 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install public void ConfigureIntegratedSecurityDatabaseConnection(string server, string databaseName) { var connectionString = GetIntegratedSecurityDatabaseConnectionString(server, databaseName); - _configManipulator.SaveConnectionString(connectionString, Constants.DbProviderNames.SqlServer); - _databaseFactory.Configure(connectionString, Constants.DbProviderNames.SqlServer); + const string providerName = Constants.DbProviderNames.SqlServer; + + _configManipulator.SaveConnectionString(connectionString, providerName); + _databaseFactory.Configure(connectionString, providerName); } /// @@ -292,18 +302,14 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install return $"Server={server};Database={databaseName};User ID={user};Password={password}"; } - private static bool ServerStartsWithTcp(string server) - { - return server.ToLower().StartsWith("tcp:".ToLower()); - } - - - + private static bool ServerStartsWithTcp(string server) => server.InvariantStartsWith("tcp:"); #endregion #region Database Schema + public void CreateDatabase() => _dbProviderFactoryCreator.CreateDatabase(_databaseFactory.ProviderName, _databaseFactory.ConnectionString); + /// /// Validates the database schema. /// diff --git a/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseFactory.cs b/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseFactory.cs index 6dfe2ada6b..299aff2caa 100644 --- a/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseFactory.cs @@ -96,7 +96,6 @@ namespace Umbraco.Cms.Infrastructure.Persistence _loggerFactory = loggerFactory; var settings = connectionStrings.CurrentValue.UmbracoConnectionString; - if (settings == null) { logger.LogDebug("Missing connection string, defer configuration."); @@ -105,9 +104,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence // 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)) + if (settings.IsConnectionStringConfigured() == false) { logger.LogDebug("Empty connection string or provider name, defer configuration."); return; // not configured @@ -148,7 +145,6 @@ namespace Umbraco.Cms.Infrastructure.Persistence private void UpdateSqlServerDatabaseType() { // replace NPoco database type by a more efficient one - var setting = _globalSettings.Value.DatabaseFactoryServerVersion; var fromSettings = false; @@ -188,6 +184,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence { // must be initialized to have a context EnsureInitialized(); + return _sqlContext; } } @@ -199,15 +196,13 @@ namespace Umbraco.Cms.Infrastructure.Persistence { // must be initialized to have a bulk insert provider EnsureInitialized(); + return _bulkSqlInsertProvider; } } /// - public void ConfigureForUpgrade() - { - _upgrading = true; - } + public void ConfigureForUpgrade() => _upgrading = true; /// public void Configure(string connectionString, string providerName) @@ -248,13 +243,6 @@ namespace Umbraco.Cms.Infrastructure.Persistence 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); diff --git a/src/Umbraco.Web.UI.Client/src/installer/steps/database.html b/src/Umbraco.Web.UI.Client/src/installer/steps/database.html index eee933b561..cf367b2ff2 100644 --- a/src/Umbraco.Web.UI.Client/src/installer/steps/database.html +++ b/src/Umbraco.Web.UI.Client/src/installer/steps/database.html @@ -10,38 +10,48 @@ What type of database do you use?
-
-
+

Great! No need to configure anything, you can simply click the continue button below to continue to the next step

-
+
+

Great! No need to configure anything, you can simply click the continue button below to continue to the next step

+
+ +
What is the exact connection string we should use?
- +
- + Enter a valid database connection string.
-
+
Where do we find your database?
- +
- + Enter server domain or IP
@@ -49,11 +59,12 @@
- +
- + required + ng-model="installer.current.model.databaseName" /> Enter the name of the database
@@ -64,11 +75,12 @@ What credentials are used to access the database?
- +
- + required + ng-model="installer.current.model.login" /> Enter the database user name
@@ -76,21 +88,21 @@
- +
- + required + ng-model="installer.current.model.password" /> Enter the database password
-
+
From 074bbb045bd30bb06c972f9e727f457e89358428 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Fri, 17 Sep 2021 12:52:05 +0200 Subject: [PATCH 07/15] Install missing database during unattended install --- .../Install/UnattendedInstaller.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/Umbraco.Infrastructure/Install/UnattendedInstaller.cs b/src/Umbraco.Infrastructure/Install/UnattendedInstaller.cs index babf882e1b..24e55c90f5 100644 --- a/src/Umbraco.Infrastructure/Install/UnattendedInstaller.cs +++ b/src/Umbraco.Infrastructure/Install/UnattendedInstaller.cs @@ -20,6 +20,7 @@ namespace Umbraco.Cms.Infrastructure.Install private readonly DatabaseSchemaCreatorFactory _databaseSchemaCreatorFactory; private readonly IEventAggregator _eventAggregator; private readonly IUmbracoDatabaseFactory _databaseFactory; + private readonly IDbProviderFactoryCreator _dbProviderFactoryCreator; private readonly IOptions _globalSettings; private readonly ILogger _logger; private readonly IRuntimeState _runtimeState; @@ -29,6 +30,7 @@ namespace Umbraco.Cms.Infrastructure.Install IEventAggregator eventAggregator, IOptions unattendedSettings, IUmbracoDatabaseFactory databaseFactory, + IDbProviderFactoryCreator dbProviderFactoryCreator, IOptions globalSettings, ILogger logger, IRuntimeState runtimeState) @@ -37,6 +39,7 @@ namespace Umbraco.Cms.Infrastructure.Install _eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator)); _unattendedSettings = unattendedSettings; _databaseFactory = databaseFactory; + _dbProviderFactoryCreator = dbProviderFactoryCreator; _globalSettings = globalSettings; _logger = logger; _runtimeState = runtimeState; @@ -70,6 +73,14 @@ namespace Umbraco.Cms.Infrastructure.Install } _logger.LogDebug("Could not immediately connect to database, trying again."); + + if (_globalSettings.Value.InstallMissingDatabase) + { + _logger.LogDebug("Install missing database is enabled, creating database before trying again."); + + _dbProviderFactoryCreator.CreateDatabase(_databaseFactory.ProviderName, _databaseFactory.ConnectionString); + } + Thread.Sleep(1000); } } From 51002ba7a4d4fd615931136f99b63886cef8cf86 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Fri, 17 Sep 2021 12:52:23 +0200 Subject: [PATCH 08/15] Minor code cleanup --- .../Persistence/UmbracoDatabase.cs | 49 ++++++++++--------- .../Runtime/CoreRuntime.cs | 2 - 2 files changed, 27 insertions(+), 24 deletions(-) diff --git a/src/Umbraco.Infrastructure/Persistence/UmbracoDatabase.cs b/src/Umbraco.Infrastructure/Persistence/UmbracoDatabase.cs index 529144953d..80970ec637 100644 --- a/src/Umbraco.Infrastructure/Persistence/UmbracoDatabase.cs +++ b/src/Umbraco.Infrastructure/Persistence/UmbracoDatabase.cs @@ -7,7 +7,6 @@ using System.Text; using Microsoft.Extensions.Logging; using NPoco; using StackExchange.Profiling; -using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Infrastructure.Migrations.Install; using Umbraco.Cms.Infrastructure.Persistence.FaultHandling; using Umbraco.Extensions; @@ -62,6 +61,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence _connectionRetryPolicy = connectionRetryPolicy; _commandRetryPolicy = commandRetryPolicy; _mapperCollection = mapperCollection; + Init(); } @@ -79,6 +79,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence SqlContext = sqlContext; _logger = logger; _bulkSqlInsertProvider = bulkSqlInsertProvider; + Init(); } @@ -86,6 +87,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence { EnableSqlTrace = EnableSqlTraceDefault; NPocoDatabaseExtensions.ConfigureNPocoBulkExtensions(); + if (_mapperCollection != null) { Mappers.AddRange(_mapperCollection); @@ -104,11 +106,14 @@ namespace Umbraco.Cms.Infrastructure.Persistence { var command = base.CreateCommand(connection, commandType, sql, args); - if (!DatabaseType.IsSqlCe()) return command; + if (!DatabaseType.IsSqlCe()) + return command; foreach (DbParameter parameter in command.Parameters) + { if (parameter.Value == DBNull.Value) parameter.DbType = DbType.String; + } return command; } @@ -128,17 +133,12 @@ namespace Umbraco.Cms.Infrastructure.Persistence #endif /// - public string InstanceId - { - get - { + public string InstanceId => #if DEBUG_DATABASES - return _instanceGuid.ToString("N").Substring(0, 8) + ':' + _spid; + _instanceGuid.ToString("N").Substring(0, 8) + ':' + _spid; #else - return _instanceId ?? (_instanceId = _instanceGuid.ToString("N").Substring(0, 8)); + _instanceId ??= _instanceGuid.ToString("N").Substring(0, 8); #endif - } - } /// public bool InTransaction { get; private set; } @@ -175,8 +175,11 @@ namespace Umbraco.Cms.Infrastructure.Persistence set { _enableCount = value; + if (_enableCount == false) + { SqlCount = 0; + } } } @@ -193,11 +196,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence internal IEnumerable Commands => _commands; - public int BulkInsertRecords(IEnumerable records) - { - return _bulkSqlInsertProvider.BulkInsertRecords(this, records); - - } + public int BulkInsertRecords(IEnumerable records) => _bulkSqlInsertProvider.BulkInsertRecords(this, records); /// /// Returns the for the database @@ -206,6 +205,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence { var dbSchema = _databaseSchemaCreatorFactory.Create(this); var databaseSchemaValidationResult = dbSchema.ValidateSchema(); + return databaseSchemaValidationResult; } @@ -263,8 +263,10 @@ namespace Umbraco.Cms.Infrastructure.Persistence { _logger.LogError(ex, "Exception ({InstanceId}).", InstanceId); _logger.LogDebug("At:\r\n{StackTrace}", Environment.StackTrace); + if (EnableSqlTrace == false) _logger.LogDebug("Sql:\r\n{Sql}", CommandToString(LastSQL, LastArgs)); + base.OnException(ex); } @@ -287,22 +289,22 @@ namespace Umbraco.Cms.Infrastructure.Persistence #endif _cmd = cmd; + base.OnExecutingCommand(cmd); } - private string CommandToString(DbCommand cmd) - { - return CommandToString(cmd.CommandText, cmd.Parameters.Cast().Select(x => x.Value).ToArray()); - } + private string CommandToString(DbCommand cmd) => CommandToString(cmd.CommandText, cmd.Parameters.Cast().Select(x => x.Value).ToArray()); private string CommandToString(string sql, object[] args) { var text = new StringBuilder(); #if DEBUG_DATABASES - text.Append(InstanceId); - text.Append(": "); + text.Append(InstanceId); + text.Append(": "); #endif + NPocoSqlExtensions.ToText(sql, args, text); + return text.ToString(); } @@ -325,11 +327,14 @@ namespace Umbraco.Cms.Infrastructure.Persistence { Text = cmd.CommandText; var parameters = new List(); - foreach (IDbDataParameter parameter in cmd.Parameters) parameters.Add(new ParameterInfo(parameter)); + foreach (IDbDataParameter parameter in cmd.Parameters) + parameters.Add(new ParameterInfo(parameter)); + Parameters = parameters.ToArray(); } public string Text { get; } + public ParameterInfo[] Parameters { get; } } diff --git a/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs b/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs index 597a8901da..f9a8578dc3 100644 --- a/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs +++ b/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs @@ -12,8 +12,6 @@ using Umbraco.Cms.Core.Logging; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Runtime; using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Infrastructure.Migrations.Install; -using Umbraco.Cms.Infrastructure.Migrations.Upgrade; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Extensions; From d702b2f616dddbd064673bae293825fa77434c2f Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Fri, 17 Sep 2021 13:10:21 +0200 Subject: [PATCH 09/15] Fix non-Windows database configuration install step --- src/Umbraco.Core/Install/Models/DatabaseModel.cs | 8 +------- .../Install/InstallSteps/DatabaseConfigureStep.cs | 11 +++++++++++ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Core/Install/Models/DatabaseModel.cs b/src/Umbraco.Core/Install/Models/DatabaseModel.cs index 514500f445..b87941e590 100644 --- a/src/Umbraco.Core/Install/Models/DatabaseModel.cs +++ b/src/Umbraco.Core/Install/Models/DatabaseModel.cs @@ -5,14 +5,8 @@ namespace Umbraco.Cms.Core.Install.Models [DataContract(Name = "database", Namespace = "")] public class DatabaseModel { - public DatabaseModel() - { - //defaults - DatabaseType = DatabaseType.SqlLocalDb; - } - [DataMember(Name = "dbType")] - public DatabaseType DatabaseType { get; set; } + public DatabaseType DatabaseType { get; set; } = DatabaseType.SqlServer; [DataMember(Name = "server")] public string Server { get; set; } diff --git a/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseConfigureStep.cs b/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseConfigureStep.cs index a14b0f3a1c..a25151bda2 100644 --- a/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseConfigureStep.cs +++ b/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseConfigureStep.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Runtime.InteropServices; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -34,6 +35,15 @@ namespace Umbraco.Cms.Infrastructure.Install.InstallSteps if (database == null) { database = new DatabaseModel(); + + if (IsLocalDbAvailable()) + { + database.DatabaseType = DatabaseType.SqlLocalDb; + } + else if (IsSqlCeAvailable()) + { + database.DatabaseType = DatabaseType.SqlCe; + } } if (_databaseBuilder.CanConnect(database.DatabaseType.ToString(), database.ConnectionString, database.Server, database.DatabaseName, database.Login, database.Password, database.IntegratedAuth) == false) @@ -108,6 +118,7 @@ namespace Umbraco.Cms.Infrastructure.Install.InstallSteps // NOTE: Type.GetType will only return types that are currently loaded into the appdomain. In this case // that is ok because we know if this is availalbe we will have manually loaded it into the appdomain. // Else we'd have to use Assembly.LoadFrom and need to know the DLL location here which we don't need to do. + RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && !(Type.GetType("Umbraco.Cms.Persistence.SqlCe.SqlCeSyntaxProvider, Umbraco.Persistence.SqlCe") is null); public override string View => ShouldDisplayView() ? base.View : ""; From ab28251558cf7307579dad64a919b95d7bd39a87 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Fri, 17 Sep 2021 16:30:46 +0200 Subject: [PATCH 10/15] Use RuntimeState to determine whether to install missing database --- .../Install/InstallHelper.cs | 8 ++--- .../InstallSteps/DatabaseInstallStep.cs | 11 ++----- .../Install/UnattendedInstaller.cs | 19 +++++------- .../Migrations/Install/DatabaseBuilder.cs | 16 +++------- .../Runtime/RuntimeState.cs | 30 ++++++++++--------- 5 files changed, 34 insertions(+), 50 deletions(-) diff --git a/src/Umbraco.Infrastructure/Install/InstallHelper.cs b/src/Umbraco.Infrastructure/Install/InstallHelper.cs index 6b5e882134..056d1c1b8c 100644 --- a/src/Umbraco.Infrastructure/Install/InstallHelper.cs +++ b/src/Umbraco.Infrastructure/Install/InstallHelper.cs @@ -26,7 +26,7 @@ namespace Umbraco.Cms.Infrastructure.Install private readonly ICookieManager _cookieManager; private readonly IUserAgentProvider _userAgentProvider; private readonly IUmbracoDatabaseFactory _umbracoDatabaseFactory; - private readonly IOptionsMonitor _globalSettings; + private readonly IRuntimeState _runtimeState; private InstallationType? _installationType; public InstallHelper(DatabaseBuilder databaseBuilder, @@ -37,7 +37,7 @@ namespace Umbraco.Cms.Infrastructure.Install ICookieManager cookieManager, IUserAgentProvider userAgentProvider, IUmbracoDatabaseFactory umbracoDatabaseFactory, - IOptionsMonitor globalSettings) + IRuntimeState runtimeState) { _logger = logger; _umbracoVersion = umbracoVersion; @@ -47,7 +47,7 @@ namespace Umbraco.Cms.Infrastructure.Install _cookieManager = cookieManager; _userAgentProvider = userAgentProvider; _umbracoDatabaseFactory = umbracoDatabaseFactory; - _globalSettings = globalSettings; + _runtimeState = runtimeState; // We need to initialize the type already, as we can't detect later, if the connection string is added on the fly. GetInstallationType(); @@ -100,7 +100,7 @@ namespace Umbraco.Cms.Infrastructure.Install /// private bool IsBrandNewInstall => _connectionStrings.CurrentValue.UmbracoConnectionString?.IsConnectionStringConfigured() != true || _databaseBuilder.IsDatabaseConfigured == false || - (_globalSettings.CurrentValue.InstallMissingDatabase && _databaseBuilder.CanConnectToDatabase == false) || + (_runtimeState.Level == RuntimeLevel.Install && _runtimeState.Reason == RuntimeLevelReason.InstallMissingDatabase) || _databaseBuilder.IsUmbracoInstalled() == false; } } diff --git a/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseInstallStep.cs b/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseInstallStep.cs index 1c58d04810..0976e14112 100644 --- a/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseInstallStep.cs +++ b/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseInstallStep.cs @@ -18,13 +18,11 @@ namespace Umbraco.Cms.Infrastructure.Install.InstallSteps { private readonly DatabaseBuilder _databaseBuilder; private readonly IRuntimeState _runtime; - private readonly IOptionsMonitor _globalSettings; - public DatabaseInstallStep(DatabaseBuilder databaseBuilder, IRuntimeState runtime, IOptionsMonitor globalSettings) + public DatabaseInstallStep(DatabaseBuilder databaseBuilder, IRuntimeState runtime) { _databaseBuilder = databaseBuilder; _runtime = runtime; - _globalSettings = globalSettings; } public override Task ExecuteAsync(object model) @@ -32,7 +30,7 @@ namespace Umbraco.Cms.Infrastructure.Install.InstallSteps if (_runtime.Level == RuntimeLevel.Run) throw new Exception("Umbraco is already configured!"); - if (_globalSettings.CurrentValue.InstallMissingDatabase) + if (_runtime.Reason == RuntimeLevelReason.InstallMissingDatabase) { _databaseBuilder.CreateDatabase(); } @@ -56,9 +54,6 @@ namespace Umbraco.Cms.Infrastructure.Install.InstallSteps })); } - public override bool RequiresExecution(object model) - { - return true; - } + public override bool RequiresExecution(object model) => true; } } diff --git a/src/Umbraco.Infrastructure/Install/UnattendedInstaller.cs b/src/Umbraco.Infrastructure/Install/UnattendedInstaller.cs index 24e55c90f5..94a6646086 100644 --- a/src/Umbraco.Infrastructure/Install/UnattendedInstaller.cs +++ b/src/Umbraco.Infrastructure/Install/UnattendedInstaller.cs @@ -3,6 +3,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Exceptions; @@ -21,7 +22,6 @@ namespace Umbraco.Cms.Infrastructure.Install private readonly IEventAggregator _eventAggregator; private readonly IUmbracoDatabaseFactory _databaseFactory; private readonly IDbProviderFactoryCreator _dbProviderFactoryCreator; - private readonly IOptions _globalSettings; private readonly ILogger _logger; private readonly IRuntimeState _runtimeState; @@ -31,7 +31,6 @@ namespace Umbraco.Cms.Infrastructure.Install IOptions unattendedSettings, IUmbracoDatabaseFactory databaseFactory, IDbProviderFactoryCreator dbProviderFactoryCreator, - IOptions globalSettings, ILogger logger, IRuntimeState runtimeState) { @@ -40,7 +39,6 @@ namespace Umbraco.Cms.Infrastructure.Install _unattendedSettings = unattendedSettings; _databaseFactory = databaseFactory; _dbProviderFactoryCreator = dbProviderFactoryCreator; - _globalSettings = globalSettings; _logger = logger; _runtimeState = runtimeState; } @@ -59,7 +57,11 @@ namespace Umbraco.Cms.Infrastructure.Install return Task.CompletedTask; } - var tries = _globalSettings.Value.InstallMissingDatabase ? 2 : 5; + _runtimeState.DetermineRuntimeLevel(); + if (_runtimeState.Reason == RuntimeLevelReason.InstallMissingDatabase) + { + _dbProviderFactoryCreator.CreateDatabase(_databaseFactory.ProviderName, _databaseFactory.ConnectionString); + } bool connect; try @@ -67,20 +69,13 @@ namespace Umbraco.Cms.Infrastructure.Install for (var i = 0; ;) { connect = _databaseFactory.CanConnect; - if (connect || ++i == tries) + if (connect || ++i == 5) { break; } _logger.LogDebug("Could not immediately connect to database, trying again."); - if (_globalSettings.Value.InstallMissingDatabase) - { - _logger.LogDebug("Install missing database is enabled, creating database before trying again."); - - _dbProviderFactoryCreator.CreateDatabase(_databaseFactory.ProviderName, _databaseFactory.ConnectionString); - } - Thread.Sleep(1000); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs index 8b9b2b07cd..a4efa29e00 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs @@ -20,12 +20,9 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install { private readonly IUmbracoDatabaseFactory _databaseFactory; private readonly IScopeProvider _scopeProvider; - private readonly IRuntimeState _runtime; - private readonly IMigrationBuilder _migrationBuilder; + private readonly IRuntimeState _runtimeState; private readonly IKeyValueService _keyValueService; - private readonly IHostingEnvironment _hostingEnvironment; private readonly ILogger _logger; - private readonly ILoggerFactory _loggerFactory; private readonly IDbProviderFactoryCreator _dbProviderFactoryCreator; private readonly IConfigManipulator _configManipulator; private readonly IMigrationPlanExecutor _migrationPlanExecutor; @@ -39,11 +36,9 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install public DatabaseBuilder( IScopeProvider scopeProvider, IUmbracoDatabaseFactory databaseFactory, - IRuntimeState runtime, + IRuntimeState runtimeState, ILoggerFactory loggerFactory, - IMigrationBuilder migrationBuilder, IKeyValueService keyValueService, - IHostingEnvironment hostingEnvironment, IDbProviderFactoryCreator dbProviderFactoryCreator, IConfigManipulator configManipulator, IMigrationPlanExecutor migrationPlanExecutor, @@ -51,12 +46,9 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install { _scopeProvider = scopeProvider; _databaseFactory = databaseFactory; - _runtime = runtime; + _runtimeState = runtimeState; _logger = loggerFactory.CreateLogger(); - _loggerFactory = loggerFactory; - _migrationBuilder = migrationBuilder; _keyValueService = keyValueService; - _hostingEnvironment = hostingEnvironment; _dbProviderFactoryCreator = dbProviderFactoryCreator; _configManipulator = configManipulator; _migrationPlanExecutor = migrationPlanExecutor; @@ -381,7 +373,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install //If the determined version is "empty" its a new install - otherwise upgrade the existing if (!hasInstalledVersion) { - if (_runtime.Level == RuntimeLevel.Run) + if (_runtimeState.Level == RuntimeLevel.Run) throw new Exception("Umbraco is already configured!"); var creator = _databaseSchemaCreatorFactory.Create(database); diff --git a/src/Umbraco.Infrastructure/Runtime/RuntimeState.cs b/src/Umbraco.Infrastructure/Runtime/RuntimeState.cs index d91a3de4bf..8eab08bfee 100644 --- a/src/Umbraco.Infrastructure/Runtime/RuntimeState.cs +++ b/src/Umbraco.Infrastructure/Runtime/RuntimeState.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Threading; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -13,6 +12,7 @@ using Umbraco.Cms.Core.Semver; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Migrations.Upgrade; using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Runtime { @@ -110,7 +110,7 @@ namespace Umbraco.Cms.Infrastructure.Runtime // cannot connect to configured database, this is bad, fail _logger.LogDebug("Could not connect to database."); - if (_globalSettings.Value.InstallMissingDatabase) + if (_globalSettings.Value.InstallMissingDatabase || CanAutoInstallMissingDatabase(_databaseFactory)) { // ok to install on a configured but missing database Level = RuntimeLevel.Install; @@ -173,6 +173,17 @@ namespace Umbraco.Cms.Infrastructure.Runtime } } + public void Configure(RuntimeLevel level, RuntimeLevelReason reason, Exception bootFailedException = null) + { + Level = level; + Reason = reason; + + if (bootFailedException != null) + { + BootFailedException = new BootFailedException(bootFailedException.Message, bootFailedException); + } + } + private enum UmbracoDatabaseState { Ok, @@ -233,17 +244,6 @@ namespace Umbraco.Cms.Infrastructure.Runtime } } - public void Configure(RuntimeLevel level, RuntimeLevelReason reason, Exception bootFailedException = null) - { - Level = level; - Reason = reason; - - if (bootFailedException != null) - { - BootFailedException = new BootFailedException(bootFailedException.Message, bootFailedException); - } - } - private bool DoesUmbracoRequireUpgrade(IReadOnlyDictionary keyValues) { var upgrader = new Upgrader(new UmbracoPlan(_umbracoVersion)); @@ -277,6 +277,8 @@ namespace Umbraco.Cms.Infrastructure.Runtime return canConnect; } - + private bool CanAutoInstallMissingDatabase(IUmbracoDatabaseFactory databaseFactory) + => databaseFactory.ProviderName == Constants.DatabaseProviders.SqlCe || + databaseFactory.ConnectionString?.InvariantContains("(localdb)") == true; } } From 4ad34270dda2d590e7c15755658a24e1c18013a6 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Fri, 17 Sep 2021 19:59:54 +0200 Subject: [PATCH 11/15] Remove runtime state dependency in InstallHelper --- src/Umbraco.Infrastructure/Install/InstallHelper.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Infrastructure/Install/InstallHelper.cs b/src/Umbraco.Infrastructure/Install/InstallHelper.cs index 056d1c1b8c..cd7dfcbf0c 100644 --- a/src/Umbraco.Infrastructure/Install/InstallHelper.cs +++ b/src/Umbraco.Infrastructure/Install/InstallHelper.cs @@ -26,7 +26,6 @@ namespace Umbraco.Cms.Infrastructure.Install private readonly ICookieManager _cookieManager; private readonly IUserAgentProvider _userAgentProvider; private readonly IUmbracoDatabaseFactory _umbracoDatabaseFactory; - private readonly IRuntimeState _runtimeState; private InstallationType? _installationType; public InstallHelper(DatabaseBuilder databaseBuilder, @@ -36,8 +35,7 @@ namespace Umbraco.Cms.Infrastructure.Install IInstallationService installationService, ICookieManager cookieManager, IUserAgentProvider userAgentProvider, - IUmbracoDatabaseFactory umbracoDatabaseFactory, - IRuntimeState runtimeState) + IUmbracoDatabaseFactory umbracoDatabaseFactory) { _logger = logger; _umbracoVersion = umbracoVersion; @@ -47,7 +45,6 @@ namespace Umbraco.Cms.Infrastructure.Install _cookieManager = cookieManager; _userAgentProvider = userAgentProvider; _umbracoDatabaseFactory = umbracoDatabaseFactory; - _runtimeState = runtimeState; // We need to initialize the type already, as we can't detect later, if the connection string is added on the fly. GetInstallationType(); @@ -100,7 +97,7 @@ namespace Umbraco.Cms.Infrastructure.Install /// private bool IsBrandNewInstall => _connectionStrings.CurrentValue.UmbracoConnectionString?.IsConnectionStringConfigured() != true || _databaseBuilder.IsDatabaseConfigured == false || - (_runtimeState.Level == RuntimeLevel.Install && _runtimeState.Reason == RuntimeLevelReason.InstallMissingDatabase) || + _databaseBuilder.CanConnectToDatabase == false || _databaseBuilder.IsUmbracoInstalled() == false; } } From fd5cef6b429a8676e5e2523cf7c942331939b5e0 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Fri, 17 Sep 2021 20:20:36 +0200 Subject: [PATCH 12/15] Support DataDirectory placeholder in LocalDB connection string --- .../Configuration/ConfigConnectionString.cs | 48 +++++++++++++++++-- .../Migrations/Install/DatabaseBuilder.cs | 4 -- .../Runtime/CoreRuntime.cs | 2 - .../UmbracoBuilderExtensions.cs | 3 ++ 4 files changed, 47 insertions(+), 10 deletions(-) diff --git a/src/Umbraco.Core/Configuration/ConfigConnectionString.cs b/src/Umbraco.Core/Configuration/ConfigConnectionString.cs index d0dec2548c..18bdace632 100644 --- a/src/Umbraco.Core/Configuration/ConfigConnectionString.cs +++ b/src/Umbraco.Core/Configuration/ConfigConnectionString.cs @@ -14,8 +14,43 @@ namespace Umbraco.Cms.Core.Configuration public ConfigConnectionString(string name, string connectionString, string providerName = null) { Name = name ?? throw new ArgumentNullException(nameof(name)); - ConnectionString = connectionString; - ProviderName = string.IsNullOrEmpty(providerName) ? ParseProviderName(connectionString) : providerName; + ConnectionString = ParseConnectionString(connectionString, ref providerName); + ProviderName = providerName; + } + + private static string ParseConnectionString(string connectionString, ref string providerName) + { + if (string.IsNullOrEmpty(connectionString)) + { + return null; + } + + var builder = new DbConnectionStringBuilder + { + ConnectionString = connectionString + }; + + // Replace data directory placeholder + const string attachDbFileNameKey = "AttachDbFileName"; + const string dataDirectoryPlaceholder = "|DataDirectory|"; + if (builder.TryGetValue(attachDbFileNameKey, out var attachDbFileNameValue) && + attachDbFileNameValue is string attachDbFileName && + attachDbFileName.Contains(dataDirectoryPlaceholder)) + { + var dataDirectory = AppDomain.CurrentDomain.GetData("DataDirectory")?.ToString(); + if (!string.IsNullOrEmpty(dataDirectory)) + { + builder[attachDbFileNameKey] = attachDbFileName.Replace(dataDirectoryPlaceholder, dataDirectory); + } + } + + // Also parse provider name now we already have a builder + if (string.IsNullOrEmpty(providerName)) + { + providerName = ParseProviderName(builder); + } + + return builder.ToString(); } /// @@ -37,13 +72,18 @@ namespace Umbraco.Cms.Core.Configuration ConnectionString = connectionString }; + return ParseProviderName(builder); + } + + private static string ParseProviderName(DbConnectionStringBuilder builder) + { if ((builder.TryGetValue("Data Source", out var dataSource) || builder.TryGetValue("DataSource", out dataSource)) && dataSource?.ToString().EndsWith(".sdf", StringComparison.OrdinalIgnoreCase) == true) { - return Cms.Core.Constants.DbProviderNames.SqlCe; + return Constants.DbProviderNames.SqlCe; } - return Cms.Core.Constants.DbProviderNames.SqlServer; + return Constants.DbProviderNames.SqlServer; } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs index a4efa29e00..0a1def06ef 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs @@ -160,10 +160,6 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install string connectionString = LocalDbConnectionString; const string providerName = Constants.DbProviderNames.SqlServer; - // Replace data directory placeholder (this is not supported by LocalDB) - var dataDirectory = AppDomain.CurrentDomain.GetData("DataDirectory")?.ToString(); - connectionString = connectionString.Replace("|DataDirectory|", dataDirectory); - _configManipulator.SaveConnectionString(connectionString, providerName); _databaseFactory.Configure(connectionString, providerName); diff --git a/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs b/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs index f9a8578dc3..86f4e070c2 100644 --- a/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs +++ b/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs @@ -94,8 +94,6 @@ namespace Umbraco.Cms.Infrastructure.Runtime _logger.LogError(exception, msg); }; - AppDomain.CurrentDomain.SetData("DataDirectory", _hostingEnvironment?.MapPathContentRoot(Constants.SystemDirectories.Data)); - // acquire the main domain - if this fails then anything that should be registered with MainDom will not operate AcquireMainDom(); diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs index 4319bc3544..90307cf746 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs @@ -94,6 +94,9 @@ namespace Umbraco.Extensions services.AddLogger(tempHostingEnvironment, loggingConfig, config); + // The DataDirectory is used to resolve database file paths (directly supported by SQL CE and manually replaced for LocalDB) + AppDomain.CurrentDomain.SetData("DataDirectory", tempHostingEnvironment?.MapPathContentRoot(Constants.SystemDirectories.Data)); + // Manually create and register the HttpContextAccessor. In theory this should not be registered // again by the user but if that is the case it's not the end of the world since HttpContextAccessor // is just based on AsyncLocal, see https://github.com/dotnet/aspnetcore/blob/main/src/Http/Http/src/HttpContextAccessor.cs From bc291550326a5fe174a158bc9104e420fddd224a Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Sat, 18 Sep 2021 00:23:47 +0200 Subject: [PATCH 13/15] Fix parsing of connection string during setup --- .../InstallSteps/DatabaseInstallStep.cs | 16 +++---- .../Migrations/Install/DatabaseBuilder.cs | 44 ++++++++++++------- 2 files changed, 35 insertions(+), 25 deletions(-) diff --git a/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseInstallStep.cs b/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseInstallStep.cs index 0976e14112..96d2a7c996 100644 --- a/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseInstallStep.cs +++ b/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseInstallStep.cs @@ -1,28 +1,24 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -using Microsoft.Extensions.Options; using Umbraco.Cms.Core; -using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Install; using Umbraco.Cms.Core.Install.Models; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Migrations.Install; -using Umbraco.Cms.Infrastructure.Persistence; namespace Umbraco.Cms.Infrastructure.Install.InstallSteps { - [InstallSetupStep(InstallationType.NewInstall | InstallationType.Upgrade, - "DatabaseInstall", 11, "")] + [InstallSetupStep(InstallationType.NewInstall | InstallationType.Upgrade, "DatabaseInstall", 11, "")] public class DatabaseInstallStep : InstallSetupStep { - private readonly DatabaseBuilder _databaseBuilder; private readonly IRuntimeState _runtime; + private readonly DatabaseBuilder _databaseBuilder; - public DatabaseInstallStep(DatabaseBuilder databaseBuilder, IRuntimeState runtime) + public DatabaseInstallStep(IRuntimeState runtime, DatabaseBuilder databaseBuilder) { - _databaseBuilder = databaseBuilder; _runtime = runtime; + _databaseBuilder = databaseBuilder; } public override Task ExecuteAsync(object model) @@ -47,10 +43,10 @@ namespace Umbraco.Cms.Infrastructure.Install.InstallSteps return Task.FromResult(null); } - //upgrade is required so set the flag for the next step + // Upgrade is required, so set the flag for the next step return Task.FromResult(new InstallSetupResult(new Dictionary { - {"upgrade", true} + { "upgrade", true} })); } diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs index 0a1def06ef..f9e36d8d12 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs @@ -1,7 +1,9 @@ using System; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Migrations; using Umbraco.Cms.Core.Scoping; @@ -25,6 +27,8 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install private readonly ILogger _logger; private readonly IDbProviderFactoryCreator _dbProviderFactoryCreator; private readonly IConfigManipulator _configManipulator; + private readonly IOptionsMonitor _globalSettings; + private readonly IOptionsMonitor _connectionStrings; private readonly IMigrationPlanExecutor _migrationPlanExecutor; private readonly DatabaseSchemaCreatorFactory _databaseSchemaCreatorFactory; @@ -41,6 +45,8 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install IKeyValueService keyValueService, IDbProviderFactoryCreator dbProviderFactoryCreator, IConfigManipulator configManipulator, + IOptionsMonitor globalSettings, + IOptionsMonitor connectionStrings, IMigrationPlanExecutor migrationPlanExecutor, DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory) { @@ -51,6 +57,8 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install _keyValueService = keyValueService; _dbProviderFactoryCreator = dbProviderFactoryCreator; _configManipulator = configManipulator; + _globalSettings = globalSettings; + _connectionStrings = connectionStrings; _migrationPlanExecutor = migrationPlanExecutor; _databaseSchemaCreatorFactory = databaseSchemaCreatorFactory; } @@ -74,8 +82,8 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install /// public bool CanConnect(string databaseType, string connectionString, string server, string database, string login, string password, bool integratedAuth) { - // we do not test SqlCE connection - if (databaseType.InvariantContains("sqlce") || databaseType.InvariantContains("localdb")) + // we do not test SqlCE or LocalDB connections + if (databaseType.InvariantContains("SqlCe") || databaseType.InvariantContains("SqlLocalDb")) return true; string providerName; @@ -147,10 +155,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install const string providerName = Constants.DbProviderNames.SqlCe; _configManipulator.SaveConnectionString(connectionString, providerName); - _databaseFactory.Configure(connectionString, providerName); - - // Always create embedded database - CreateDatabase(); + Configure(connectionString, providerName, true); } public const string LocalDbConnectionString = @"Server=(localdb)\MSSQLLocalDB;Integrated Security=true;AttachDbFileName=|DataDirectory|\Umbraco.mdf"; @@ -161,10 +166,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install const string providerName = Constants.DbProviderNames.SqlServer; _configManipulator.SaveConnectionString(connectionString, providerName); - _databaseFactory.Configure(connectionString, providerName); - - // Always create LocalDB database - CreateDatabase(); + Configure(connectionString, providerName, true); } /// @@ -174,10 +176,8 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install /// Has to be SQL Server public void ConfigureDatabaseConnection(string connectionString) { - const string providerName = Constants.DbProviderNames.SqlServer; - - _configManipulator.SaveConnectionString(connectionString, providerName); - _databaseFactory.Configure(connectionString, providerName); + _configManipulator.SaveConnectionString(connectionString, null); + Configure(connectionString, null, _globalSettings.CurrentValue.InstallMissingDatabase); } /// @@ -193,7 +193,21 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install var connectionString = GetDatabaseConnectionString(server, databaseName, user, password, databaseProvider, out var providerName); _configManipulator.SaveConnectionString(connectionString, providerName); - _databaseFactory.Configure(connectionString, providerName); + Configure(connectionString, providerName, _globalSettings.CurrentValue.InstallMissingDatabase); + } + + private void Configure(string connectionString, string providerName, bool installMissingDatabase) + { + // Update existing connection string + var umbracoConnectionString = new ConfigConnectionString(Constants.System.UmbracoConnectionName, connectionString, providerName); + _connectionStrings.CurrentValue.UmbracoConnectionString = umbracoConnectionString; + + _databaseFactory.Configure(umbracoConnectionString.ConnectionString, umbracoConnectionString.ProviderName); + + if (installMissingDatabase) + { + CreateDatabase(); + } } /// From fed720042cddd404a49e040abb511ac112cbb060 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Mon, 20 Sep 2021 11:38:54 +0200 Subject: [PATCH 14/15] Fix ParseConnectionString to pass tests --- src/Umbraco.Core/Configuration/ConfigConnectionString.cs | 7 +++++-- .../Migrations/Install/DatabaseBuilder.cs | 3 +-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Core/Configuration/ConfigConnectionString.cs b/src/Umbraco.Core/Configuration/ConfigConnectionString.cs index 18bdace632..116b96df9c 100644 --- a/src/Umbraco.Core/Configuration/ConfigConnectionString.cs +++ b/src/Umbraco.Core/Configuration/ConfigConnectionString.cs @@ -22,7 +22,7 @@ namespace Umbraco.Cms.Core.Configuration { if (string.IsNullOrEmpty(connectionString)) { - return null; + return connectionString; } var builder = new DbConnectionStringBuilder @@ -41,6 +41,9 @@ namespace Umbraco.Cms.Core.Configuration if (!string.IsNullOrEmpty(dataDirectory)) { builder[attachDbFileNameKey] = attachDbFileName.Replace(dataDirectoryPlaceholder, dataDirectory); + + // Mutate the existing connection string (note: the builder also lowercases the properties) + connectionString = builder.ToString(); } } @@ -50,7 +53,7 @@ namespace Umbraco.Cms.Core.Configuration providerName = ParseProviderName(builder); } - return builder.ToString(); + return connectionString; } /// diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs index f9e36d8d12..55fdd24f77 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs @@ -4,7 +4,6 @@ using Microsoft.Extensions.Options; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Configuration.Models; -using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Migrations; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; @@ -144,7 +143,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install #region Configure Connection String - public const string EmbeddedDatabaseConnectionString = @"Data Source=|DataDirectory|\Umbraco.sdf;Flush Interval=1;"; + public const string EmbeddedDatabaseConnectionString = @"Data Source=|DataDirectory|\Umbraco.sdf;Flush Interval=1"; /// /// Configures a connection string for the embedded database. From f638636c10ecf3355a691b4c3800f0144a6a7b5c Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Mon, 20 Sep 2021 11:45:08 +0200 Subject: [PATCH 15/15] Update default LocalDB connection string to equal parsed value --- .../Migrations/Install/DatabaseBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs index 55fdd24f77..78dbdddc84 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs @@ -157,7 +157,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install Configure(connectionString, providerName, true); } - public const string LocalDbConnectionString = @"Server=(localdb)\MSSQLLocalDB;Integrated Security=true;AttachDbFileName=|DataDirectory|\Umbraco.mdf"; + public const string LocalDbConnectionString = @"Data Source=(localdb)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\Umbraco.mdf;Integrated Security=True"; public void ConfigureSqlLocalDbDatabaseConnection() {