Merge pull request #11127 from umbraco/v9/feature/localdb-install-option

Add LocalDB database install option and implement automatic database creation
This commit is contained in:
Bjarke Berg
2021-09-20 20:02:30 +02:00
committed by GitHub
27 changed files with 405 additions and 386 deletions

View File

@@ -5,69 +5,88 @@ 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;
ConnectionString = ParseConnectionString(connectionString, ref providerName);
ProviderName = 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)
private static string ParseConnectionString(string connectionString, ref string providerName)
{
if(builder.TryGetValue("Server", out var s) && s is string server)
if (string.IsNullOrEmpty(connectionString))
{
return server;
return connectionString;
}
if ((builder.TryGetValue("Data Source", out var ds)
|| builder.TryGetValue("DataSource", out ds)) && ds is string dataSource)
var builder = new DbConnectionStringBuilder
{
return dataSource;
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);
// Mutate the existing connection string (note: the builder also lowercases the properties)
connectionString = builder.ToString();
}
}
return "";
// Also parse provider name now we already have a builder
if (string.IsNullOrEmpty(providerName))
{
providerName = ParseProviderName(builder);
}
return connectionString;
}
private static string ParseProvider(string connectionString)
/// <summary>
/// Parses the connection string to get the provider name.
/// </summary>
/// <param name="connectionString">The connection string.</param>
/// <returns>
/// The provider name or <c>null</c> is the connection string is empty.
/// </returns>
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
{
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 Constants.DbProviderNames.SqlCe;
}
if (IsSqlServer(builder))
{
return Constants.DbProviderNames.SqlServer;
}
throw new ArgumentException("Cannot determine provider name from connection string",
nameof(connectionString));
return Constants.DbProviderNames.SqlServer;
}
}
}

View File

@@ -26,6 +26,6 @@ namespace Umbraco.Cms.Core.Configuration.Models
/// <summary>
/// Gets or sets a value for the Umbraco database connection string..
/// </summary>
public ConfigConnectionString UmbracoConnectionString { get; set; }
public ConfigConnectionString UmbracoConnectionString { get; set; } = new ConfigConnectionString(Constants.System.UmbracoConnectionName, null);
}
}

View File

@@ -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);
}
}

View File

@@ -1,18 +1,12 @@
using System.Runtime.Serialization;
using System.Runtime.Serialization;
namespace Umbraco.Cms.Core.Install.Models
{
[DataContract(Name = "database", Namespace = "")]
public class DatabaseModel
{
public DatabaseModel()
{
//defaults
DatabaseType = DatabaseType.SqlCe;
}
[DataMember(Name = "dbType")]
public DatabaseType DatabaseType { get; set; }
public DatabaseType DatabaseType { get; set; } = DatabaseType.SqlServer;
[DataMember(Name = "server")]
public string Server { get; set; }

View File

@@ -1,7 +1,8 @@
namespace Umbraco.Cms.Core.Install.Models
namespace Umbraco.Cms.Core.Install.Models
{
public enum DatabaseType
{
SqlLocalDb,
SqlCe,
SqlServer,
SqlAzure,

View File

@@ -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;
@@ -50,14 +46,11 @@ namespace Umbraco.Cms.Infrastructure.Install
_userAgentProvider = userAgentProvider;
_umbracoDatabaseFactory = umbracoDatabaseFactory;
//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 +58,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 +90,14 @@ namespace Umbraco.Cms.Infrastructure.Install
}
/// <summary>
/// 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.
/// </summary>
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;
}
}
/// <value>
/// <c>true</c> if this is a brand new install; otherwise, <c>false</c>.
/// </value>
private bool IsBrandNewInstall => _connectionStrings.CurrentValue.UmbracoConnectionString?.IsConnectionStringConfigured() != true ||
_databaseBuilder.IsDatabaseConfigured == false ||
_databaseBuilder.CanConnectToDatabase == false ||
_databaseBuilder.IsUmbracoInstalled() == false;
}
}

View File

@@ -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;
@@ -7,6 +8,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
@@ -33,13 +35,24 @@ 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)
{
throw new InstallException("Could not connect to the database");
}
ConfigureConnection(database);
return Task.FromResult<InstallSetupResult>(null);
}
@@ -49,6 +62,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 +79,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 +99,32 @@ 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);
}
RuntimeInformation.IsOSPlatform(OSPlatform.Windows) &&
!(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 +137,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)

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Umbraco.Cms.Core;
@@ -9,17 +9,16 @@ using Umbraco.Cms.Infrastructure.Migrations.Install;
namespace Umbraco.Cms.Infrastructure.Install.InstallSteps
{
[InstallSetupStep(InstallationType.NewInstall | InstallationType.Upgrade,
"DatabaseInstall", 11, "")]
[InstallSetupStep(InstallationType.NewInstall | InstallationType.Upgrade, "DatabaseInstall", 11, "")]
public class DatabaseInstallStep : InstallSetupStep<object>
{
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<InstallSetupResult> ExecuteAsync(object model)
@@ -27,6 +26,11 @@ namespace Umbraco.Cms.Infrastructure.Install.InstallSteps
if (_runtime.Level == RuntimeLevel.Run)
throw new Exception("Umbraco is already configured!");
if (_runtime.Reason == RuntimeLevelReason.InstallMissingDatabase)
{
_databaseBuilder.CreateDatabase();
}
var result = _databaseBuilder.CreateSchemaAndData();
if (result.Success == false)
@@ -39,16 +43,13 @@ namespace Umbraco.Cms.Infrastructure.Install.InstallSteps
return Task.FromResult<InstallSetupResult>(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<string, object>
{
{"upgrade", true}
{ "upgrade", true}
}));
}
public override bool RequiresExecution(object model)
{
return true;
}
public override bool RequiresExecution(object model) => true;
}
}

View File

@@ -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;
@@ -20,7 +21,7 @@ namespace Umbraco.Cms.Infrastructure.Install
private readonly DatabaseSchemaCreatorFactory _databaseSchemaCreatorFactory;
private readonly IEventAggregator _eventAggregator;
private readonly IUmbracoDatabaseFactory _databaseFactory;
private readonly IOptions<GlobalSettings> _globalSettings;
private readonly IDbProviderFactoryCreator _dbProviderFactoryCreator;
private readonly ILogger<UnattendedInstaller> _logger;
private readonly IRuntimeState _runtimeState;
@@ -29,7 +30,7 @@ namespace Umbraco.Cms.Infrastructure.Install
IEventAggregator eventAggregator,
IOptions<UnattendedSettings> unattendedSettings,
IUmbracoDatabaseFactory databaseFactory,
IOptions<GlobalSettings> globalSettings,
IDbProviderFactoryCreator dbProviderFactoryCreator,
ILogger<UnattendedInstaller> logger,
IRuntimeState runtimeState)
{
@@ -37,7 +38,7 @@ namespace Umbraco.Cms.Infrastructure.Install
_eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
_unattendedSettings = unattendedSettings;
_databaseFactory = databaseFactory;
_globalSettings = globalSettings;
_dbProviderFactoryCreator = dbProviderFactoryCreator;
_logger = logger;
_runtimeState = runtimeState;
}
@@ -56,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
@@ -64,12 +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.");
Thread.Sleep(1000);
}
}

View File

@@ -1,10 +1,9 @@
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration;
using Umbraco.Cms.Core.Hosting;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Migrations;
using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Core.Services;
@@ -22,14 +21,13 @@ 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<DatabaseBuilder> _logger;
private readonly ILoggerFactory _loggerFactory;
private readonly IDbProviderFactoryCreator _dbProviderFactoryCreator;
private readonly IConfigManipulator _configManipulator;
private readonly IOptionsMonitor<GlobalSettings> _globalSettings;
private readonly IOptionsMonitor<ConnectionStrings> _connectionStrings;
private readonly IMigrationPlanExecutor _migrationPlanExecutor;
private readonly DatabaseSchemaCreatorFactory _databaseSchemaCreatorFactory;
@@ -41,26 +39,25 @@ 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,
IOptionsMonitor<GlobalSettings> globalSettings,
IOptionsMonitor<ConnectionStrings> connectionStrings,
IMigrationPlanExecutor migrationPlanExecutor,
DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory)
{
_scopeProvider = scopeProvider;
_databaseFactory = databaseFactory;
_runtime = runtime;
_runtimeState = runtimeState;
_logger = loggerFactory.CreateLogger<DatabaseBuilder>();
_loggerFactory = loggerFactory;
_migrationBuilder = migrationBuilder;
_keyValueService = keyValueService;
_hostingEnvironment = hostingEnvironment;
_dbProviderFactoryCreator = dbProviderFactoryCreator;
_configManipulator = configManipulator;
_globalSettings = globalSettings;
_connectionStrings = connectionStrings;
_migrationPlanExecutor = migrationPlanExecutor;
_databaseSchemaCreatorFactory = databaseSchemaCreatorFactory;
}
@@ -84,15 +81,15 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install
/// </summary>
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"))
// we do not test SqlCE or LocalDB connections
if (databaseType.InvariantContains("SqlCe") || databaseType.InvariantContains("SqlLocalDb"))
return true;
string providerName;
if (string.IsNullOrWhiteSpace(connectionString) == false)
{
providerName = DbConnectionExtensions.DetectProviderNameFromConnectionString(connectionString);
providerName = ConfigConnectionString.ParseProviderName(connectionString);
}
else if (integratedAuth)
{
@@ -146,30 +143,29 @@ 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";
/// <summary>
/// Configures a connection string for the embedded database.
/// </summary>
public void ConfigureEmbeddedDatabaseConnection()
{
ConfigureEmbeddedDatabaseConnection(_databaseFactory);
const string connectionString = EmbeddedDatabaseConnectionString;
const string providerName = Constants.DbProviderNames.SqlCe;
_configManipulator.SaveConnectionString(connectionString, providerName);
Configure(connectionString, providerName, true);
}
private void ConfigureEmbeddedDatabaseConnection(IUmbracoDatabaseFactory factory)
public const string LocalDbConnectionString = @"Data Source=(localdb)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\Umbraco.mdf;Integrated Security=True";
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
_dbProviderFactoryCreator.CreateDatabase(Constants.DbProviderNames.SqlCe);
}
factory.Configure(EmbeddedDatabaseConnectionString, Constants.DbProviderNames.SqlCe);
_configManipulator.SaveConnectionString(connectionString, providerName);
Configure(connectionString, providerName, true);
}
/// <summary>
@@ -179,10 +175,8 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install
/// <remarks>Has to be SQL Server</remarks>
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);
}
/// <summary>
@@ -198,7 +192,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();
}
}
/// <summary>
@@ -214,9 +222,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 +237,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);
}
/// <summary>
@@ -292,18 +303,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);
/// <summary>
/// Validates the database schema.
/// </summary>
@@ -375,7 +382,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);

View File

@@ -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<string>();
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;
}
}
}

View File

@@ -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<string, DbProviderFactory> _getFactory;
private readonly IDictionary<string, IEmbeddedDatabaseCreator> _embeddedDatabaseCreators;
private readonly IDictionary<string, IDatabaseCreator> _databaseCreators;
private readonly IDictionary<string, ISqlSyntaxProvider> _syntaxProviders;
private readonly IDictionary<string, IBulkSqlInsertProvider> _bulkSqlInsertProviders;
private readonly IDictionary<string, IProviderSpecificMapperFactory> _providerSpecificMapperFactories;
@@ -20,11 +19,11 @@ namespace Umbraco.Cms.Infrastructure.Persistence
Func<string, DbProviderFactory> getFactory,
IEnumerable<ISqlSyntaxProvider> syntaxProviders,
IEnumerable<IBulkSqlInsertProvider> bulkSqlInsertProviders,
IEnumerable<IEmbeddedDatabaseCreator> embeddedDatabaseCreators,
IEnumerable<IDatabaseCreator> databaseCreators,
IEnumerable<IProviderSpecificMapperFactory> 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);
}
}

View File

@@ -0,0 +1,9 @@
namespace Umbraco.Cms.Infrastructure.Persistence
{
public interface IDatabaseCreator
{
string ProviderName { get; }
void Create(string connectionString);
}
}

View File

@@ -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);
}
}

View File

@@ -1,9 +0,0 @@
namespace Umbraco.Cms.Infrastructure.Persistence
{
public interface IEmbeddedDatabaseCreator
{
string ProviderName { get; }
string ConnectionString { get; set; }
void Create();
}
}

View File

@@ -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()
{
}
}
}

View File

@@ -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();
}
}
}
}

View File

@@ -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<string, DbProviderFactory> _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<IMapper>());

View File

@@ -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
/// <inheritdoc />
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
}
}
/// <inheritdoc />
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<CommandInfo> Commands => _commands;
public int BulkInsertRecords<T>(IEnumerable<T> records)
{
return _bulkSqlInsertProvider.BulkInsertRecords(this, records);
}
public int BulkInsertRecords<T>(IEnumerable<T> records) => _bulkSqlInsertProvider.BulkInsertRecords(this, records);
/// <summary>
/// Returns the <see cref="DatabaseSchemaResult"/> 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<DbParameter>().Select(x => x.Value).ToArray());
}
private string CommandToString(DbCommand cmd) => CommandToString(cmd.CommandText, cmd.Parameters.Cast<DbParameter>().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<ParameterInfo>();
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; }
}

View File

@@ -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 <add name="umbracoDbDSN" connectionString="" providerName="" />
// 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;
}
}
/// <inheritdoc />
public void ConfigureForUpgrade()
{
_upgrading = true;
}
public void ConfigureForUpgrade() => _upgrading = true;
/// <inheritdoc />
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);

View File

@@ -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;
@@ -96,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();

View File

@@ -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<string, string> 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;
}
}

View File

@@ -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();
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -23,7 +23,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence
{
private IDbProviderFactoryCreator DbProviderFactoryCreator => GetRequiredService<IDbProviderFactoryCreator>();
private IUmbracoDatabaseFactory UmbracoDatabaseFactory => GetRequiredService<IUmbracoDatabaseFactory>();
private IEmbeddedDatabaseCreator EmbeddedDatabaseCreator => GetRequiredService<IEmbeddedDatabaseCreator>();
private IDatabaseCreator EmbeddedDatabaseCreator => GetRequiredService<IDatabaseCreator>();
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)

View File

@@ -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
@@ -154,7 +157,7 @@ namespace Umbraco.Extensions
DbProviderFactories.GetFactory,
factory.GetServices<ISqlSyntaxProvider>(),
factory.GetServices<IBulkSqlInsertProvider>(),
factory.GetServices<IEmbeddedDatabaseCreator>(),
factory.GetServices<IDatabaseCreator>(),
factory.GetServices<IProviderSpecificMapperFactory>()
));
@@ -361,17 +364,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);
}
@@ -401,7 +404,7 @@ namespace Umbraco.Extensions
builder.Services.AddSingleton<ISqlSyntaxProvider, SqlServerSyntaxProvider>();
builder.Services.AddSingleton<IBulkSqlInsertProvider, SqlServerBulkSqlInsertProvider>();
builder.Services.AddSingleton<IEmbeddedDatabaseCreator, NoopEmbeddedDatabaseCreator>();
builder.Services.AddSingleton<IDatabaseCreator, SqlServerDatabaseCreator>();
return builder;
}

View File

@@ -10,38 +10,48 @@
<legend>What type of database do you use?</legend>
<label class="control-label" for="dbType">Database type</label>
<div class="controls">
<select id="dbType" name="dbType"
<select id="dbType"
ng-options="db.id as db.name for db in dbs"
required ng-model="installer.current.model.dbType">
required
ng-model="installer.current.model.dbType">
</select>
</div>
</div>
<div class="controls" ng-if="installer.current.model.dbType == 'SqlCe' ">
<div class="controls" ng-if="installer.current.model.dbType == 'SqlLocalDb'">
<p>Great! No need to configure anything, you can simply click the <strong>continue</strong> button below to continue to the next step</p>
</div>
<div ng-if="installer.current.model.dbType == 'Custom' ">
<div class="controls" ng-if="installer.current.model.dbType == 'SqlCe'">
<p>Great! No need to configure anything, you can simply click the <strong>continue</strong> button below to continue to the next step</p>
</div>
<div ng-if="installer.current.model.dbType == 'Custom'">
<legend>What is the exact connection string we should use?</legend>
<div class="control-group">
<label class="control-label" for="server">Connection string</label>
<label class="control-label" for="Custom_connectionString">Connection string</label>
<div class="controls">
<textarea class="input-block-level" required ng-model="installer.current.model.connectionString" rows="5"></textarea>
<textarea id="Custom_connectionString"
class="input-block-level"
required
ng-model="installer.current.model.connectionString"
rows="5"></textarea>
<small class="inline-help">Enter a valid database connection string.</small>
</div>
</div>
</div>
<div ng-if="installer.current.model.dbType == 'SqlAzure' || installer.current.model.dbType == 'SqlServer' ">
<div ng-if="installer.current.model.dbType == 'SqlAzure' || installer.current.model.dbType == 'SqlServer'">
<div class="row">
<legend>Where do we find your database?</legend>
<div class="span6">
<div class="control-group">
<label class="control-label" for="server">Server</label>
<label class="control-label" for="Sql_Server">Server</label>
<div class="controls">
<input type="text" id="server" name="server"
placeholder="{{ (installer.current.model.dbType == 'SqlAzure') ? 'umbraco-database.database.windows.net' : '127.0.0.1\\SQLEXPRESS'}}"
required ng-model="installer.current.model.server" />
<input type="text" id="Sql_Server"
placeholder="{{ (installer.current.model.dbType == 'SqlAzure') ? 'umbraco-database.database.windows.net' : '(local)\\SQLEXPRESS'}}"
required
ng-model="installer.current.model.server" />
<small class="inline-help">Enter server domain or IP</small>
</div>
</div>
@@ -49,11 +59,12 @@
<div class="span6">
<div class="control-group">
<label class="control-label" for="databaseName">Database name</label>
<label class="control-label" for="Sql_databaseName">Database name</label>
<div class="controls">
<input type="text" id="databaseName" name="installer.current.model.databaseName"
<input type="text" id="Sql_databaseName"
placeholder="umbraco-cms"
required ng-model="installer.current.model.databaseName" />
required
ng-model="installer.current.model.databaseName" />
<small class="inline-help">Enter the name of the database</small>
</div>
</div>
@@ -64,11 +75,12 @@
<legend>What credentials are used to access the database?</legend>
<div class="span6">
<div class="control-group" ng-if="!installer.current.model.integratedAuth">
<label class="control-label" for="login">Login</label>
<label class="control-label" for="Sql_login">Login</label>
<div class="controls">
<input type="text" id="login" name="login"
<input type="text" id="Sql_login"
placeholder="umbraco-db-user"
required ng-model="installer.current.model.login" />
required
ng-model="installer.current.model.login" />
<small class="inline-help">Enter the database user name</small>
</div>
</div>
@@ -76,21 +88,21 @@
<div class="span6">
<div class="control-group" ng-if="!installer.current.model.integratedAuth">
<label class="control-label" for="password">Password</label>
<label class="control-label" for="Sql_password">Password</label>
<div class="controls">
<input type="password" id="password" name="password"
<input type="password" id="Sql_password"
placeholder="umbraco-db-password"
required ng-model="installer.current.model.password" />
required
ng-model="installer.current.model.password" />
<small class="inline-help">Enter the database password</small>
</div>
</div>
</div>
<div class="span12 control-group" ng-if="installer.current.model.dbType =='SqlServer' ">
<div class="span12 control-group" ng-if="installer.current.model.dbType == 'SqlServer'">
<div class="controls">
<label class="checkbox">
<input type="checkbox" id="integratedAuth" name="integratedAuth"
ng-model="installer.current.model.integratedAuth" />
<input type="checkbox" ng-model="installer.current.model.integratedAuth" />
Use integrated authentication
</label>
</div>