From 81cbbd861455e25e2a52237c59a2f64d52edb204 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Fri, 17 Sep 2021 12:50:25 +0200 Subject: [PATCH] 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
-
+