v10: Use ForceCreateDatabase during unattended install and extend GetUmbracoConnectionString extension methods (#12397)

* Add extension methods to get the Umbraco connection string/provider name from configuration

* Added tests for configuration extension methods.

* Fix issue with InstallMissingDatabase and ForceCreateDatabase

* Fix comments

* Revert casing change in GenerateConnectionString

* Re-add AddOptions (without config binding) to fix test

* Update src/Umbraco.Core/Configuration/Models/ConnectionStrings.cs

Co-authored-by: Ronald Barendse <ronald@barend.se>

* Update src/Umbraco.Core/Configuration/Models/ConnectionStrings.cs

* Update src/Umbraco.Infrastructure/Runtime/RuntimeState.cs

* Whitespace and documentation updates

* Add DatabaseProviderMetadataExtensions

* Filter before ordering

* Replace DataDirectory placeholder when setting connection string

Co-authored-by: Andy Butland <abutland73@gmail.com>
Co-authored-by: Bjarke Berg <mail@bergmania.dk>
This commit is contained in:
Ronald Barendse
2022-05-17 12:59:01 +02:00
committed by GitHub
parent e82bcb1b76
commit 8e6e262c7f
15 changed files with 487 additions and 243 deletions

View File

@@ -6,16 +6,18 @@ using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Configuration;
/// <summary>
/// Configures ConnectionStrings.
/// Configures the <see cref="ConnectionStrings" /> named option.
/// </summary>
public class ConfigureConnectionStrings : IConfigureNamedOptions<ConnectionStrings>
{
private readonly IConfiguration _configuration;
/// <summary>
/// Initializes a new instance of the <see cref="ConfigureConnectionStrings"/> class.
/// Initializes a new instance of the <see cref="ConfigureConnectionStrings" /> class.
/// </summary>
public ConfigureConnectionStrings(IConfiguration configuration) => _configuration = configuration;
/// <param name="configuration">The configuration.</param>
public ConfigureConnectionStrings(IConfiguration configuration)
=> _configuration = configuration;
/// <inheritdoc />
public void Configure(ConnectionStrings options) => Configure(Options.DefaultName, options);
@@ -35,7 +37,7 @@ public class ConfigureConnectionStrings : IConfigureNamedOptions<ConnectionStrin
}
options.Name = name;
options.ConnectionString = _configuration.GetConnectionString(name);
options.ProviderName = _configuration.GetConnectionString($"{name}{ConnectionStrings.ProviderNamePostfix}") ?? ConnectionStrings.DefaultProviderName;
options.ConnectionString = _configuration.GetUmbracoConnectionString(name, out string? providerName);
options.ProviderName = providerName ?? ConnectionStrings.DefaultProviderName;
}
}

View File

@@ -2,8 +2,10 @@ using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Configuration.Models;
[UmbracoOptions("ConnectionStrings")]
public class ConnectionStrings
/// <summary>
/// Represents a single connection string.
/// </summary>
public class ConnectionStrings // TODO: Rename to [Umbraco]ConnectionString (since v10 this only contains a single connection string)
{
private string? _connectionString;
@@ -15,20 +17,42 @@ public class ConnectionStrings
/// <summary>
/// The DataDirectory placeholder.
/// </summary>
public const string DataDirectoryPlaceholder = "|DataDirectory|";
public const string DataDirectoryPlaceholder = ConfigurationExtensions.DataDirectoryPlaceholder;
/// <summary>
/// The postfix used to identify a connection strings provider setting.
/// </summary>
public const string ProviderNamePostfix = "_ProviderName";
public const string ProviderNamePostfix = ConfigurationExtensions.ProviderNamePostfix;
/// <summary>
/// Gets or sets the name.
/// </summary>
/// <value>
/// The name.
/// </value>
[Obsolete("This property will be removed in Umbraco 12, because this class is now using named options.")]
public string? Name { get; set; }
/// <summary>
/// Gets or sets the connection string.
/// </summary>
/// <value>
/// The connection string.
/// </value>
/// <remarks>
/// When set, the <see cref="DataDirectoryPlaceholder"/> will be replaced with the actual physical path.
/// </remarks>
public string? ConnectionString
{
get => _connectionString;
set => _connectionString = value?.ReplaceDataDirectoryPlaceholder();
set => _connectionString = ConfigurationExtensions.ReplaceDataDirectoryPlaceholder(value);
}
public string? ProviderName { get; set; } = DefaultProviderName;
/// <summary>
/// Gets or sets the name of the provider.
/// </summary>
/// <value>
/// The name of the provider.
/// </value>
public string? ProviderName { get; set; }
}

View File

@@ -1,4 +1,4 @@
namespace Umbraco.Cms.Core
namespace Umbraco.Cms.Core
{
public static partial class Constants
{
@@ -62,7 +62,9 @@
public const string UmbracoDefaultDatabaseName = "Umbraco";
public const string UmbracoConnectionName = "umbracoDbDSN";public const string DefaultUmbracoPath = "~/umbraco";
public const string UmbracoConnectionName = "umbracoDbDSN";
public const string DefaultUmbracoPath = "~/umbraco";
}
}
}

View File

@@ -1,5 +1,3 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
@@ -9,105 +7,101 @@ using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Configuration.Models.Validation;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.DependencyInjection
namespace Umbraco.Cms.Core.DependencyInjection;
/// <summary>
/// Extension methods for <see cref="IUmbracoBuilder" />
/// </summary>
public static partial class UmbracoBuilderExtensions
{
/// <summary>
/// Extension methods for <see cref="IUmbracoBuilder"/>
/// </summary>
public static partial class UmbracoBuilderExtensions
private static IUmbracoBuilder AddUmbracoOptions<TOptions>(this IUmbracoBuilder builder, Action<OptionsBuilder<TOptions>>? configure = null)
where TOptions : class
{
private static IUmbracoBuilder AddUmbracoOptions<TOptions>(this IUmbracoBuilder builder, Action<OptionsBuilder<TOptions>>? configure = null)
where TOptions : class
var umbracoOptionsAttribute = typeof(TOptions).GetCustomAttribute<UmbracoOptionsAttribute>();
if (umbracoOptionsAttribute is null)
{
var umbracoOptionsAttribute = typeof(TOptions).GetCustomAttribute<UmbracoOptionsAttribute>();
if (umbracoOptionsAttribute is null)
throw new ArgumentException($"{typeof(TOptions)} do not have the UmbracoOptionsAttribute.");
}
var optionsBuilder = builder.Services.AddOptions<TOptions>()
.Bind(
builder.Config.GetSection(umbracoOptionsAttribute.ConfigurationKey),
o => o.BindNonPublicProperties = umbracoOptionsAttribute.BindNonPublicProperties)
.ValidateDataAnnotations();
configure?.Invoke(optionsBuilder);
return builder;
}
/// <summary>
/// Add Umbraco configuration services and options
/// </summary>
public static IUmbracoBuilder AddConfiguration(this IUmbracoBuilder builder)
{
// Register configuration validators.
builder.Services.AddSingleton<IValidateOptions<ContentSettings>, ContentSettingsValidator>();
builder.Services.AddSingleton<IValidateOptions<GlobalSettings>, GlobalSettingsValidator>();
builder.Services.AddSingleton<IValidateOptions<HealthChecksSettings>, HealthChecksSettingsValidator>();
builder.Services.AddSingleton<IValidateOptions<RequestHandlerSettings>, RequestHandlerSettingsValidator>();
builder.Services.AddSingleton<IValidateOptions<UnattendedSettings>, UnattendedSettingsValidator>();
// Register configuration sections.
builder
.AddUmbracoOptions<ModelsBuilderSettings>()
.AddUmbracoOptions<ActiveDirectorySettings>()
.AddUmbracoOptions<ContentSettings>()
.AddUmbracoOptions<CoreDebugSettings>()
.AddUmbracoOptions<ExceptionFilterSettings>()
.AddUmbracoOptions<GlobalSettings>(optionsBuilder => optionsBuilder.PostConfigure(options =>
{
throw new ArgumentException($"{typeof(TOptions)} do not have the UmbracoOptionsAttribute.");
}
var optionsBuilder = builder.Services.AddOptions<TOptions>()
.Bind(
builder.Config.GetSection(umbracoOptionsAttribute.ConfigurationKey),
o => o.BindNonPublicProperties = umbracoOptionsAttribute.BindNonPublicProperties
)
.ValidateDataAnnotations();
configure?.Invoke(optionsBuilder);
return builder;
}
/// <summary>
/// Add Umbraco configuration services and options
/// </summary>
public static IUmbracoBuilder AddConfiguration(this IUmbracoBuilder builder)
{
// Register configuration validators.
builder.Services.AddSingleton<IValidateOptions<ContentSettings>, ContentSettingsValidator>();
builder.Services.AddSingleton<IValidateOptions<GlobalSettings>, GlobalSettingsValidator>();
builder.Services.AddSingleton<IValidateOptions<HealthChecksSettings>, HealthChecksSettingsValidator>();
builder.Services.AddSingleton<IValidateOptions<RequestHandlerSettings>, RequestHandlerSettingsValidator>();
builder.Services.AddSingleton<IValidateOptions<UnattendedSettings>, UnattendedSettingsValidator>();
// Register configuration sections.
builder
.AddUmbracoOptions<ModelsBuilderSettings>()
.AddUmbracoOptions<ConnectionStrings>()
.AddUmbracoOptions<ActiveDirectorySettings>()
.AddUmbracoOptions<ContentSettings>()
.AddUmbracoOptions<CoreDebugSettings>()
.AddUmbracoOptions<ExceptionFilterSettings>()
.AddUmbracoOptions<GlobalSettings>(optionsBuilder => optionsBuilder.PostConfigure(options =>
if (string.IsNullOrEmpty(options.UmbracoMediaPhysicalRootPath))
{
if (string.IsNullOrEmpty(options.UmbracoMediaPhysicalRootPath))
{
options.UmbracoMediaPhysicalRootPath = options.UmbracoMediaPath;
}
}))
.AddUmbracoOptions<HealthChecksSettings>()
.AddUmbracoOptions<HostingSettings>()
.AddUmbracoOptions<ImagingSettings>()
.AddUmbracoOptions<IndexCreatorSettings>()
.AddUmbracoOptions<KeepAliveSettings>()
.AddUmbracoOptions<LoggingSettings>()
.AddUmbracoOptions<MemberPasswordConfigurationSettings>()
.AddUmbracoOptions<NuCacheSettings>()
.AddUmbracoOptions<RequestHandlerSettings>()
.AddUmbracoOptions<RuntimeSettings>()
.AddUmbracoOptions<SecuritySettings>()
.AddUmbracoOptions<TourSettings>()
.AddUmbracoOptions<TypeFinderSettings>()
.AddUmbracoOptions<UserPasswordConfigurationSettings>()
.AddUmbracoOptions<WebRoutingSettings>()
.AddUmbracoOptions<UmbracoPluginSettings>()
.AddUmbracoOptions<UnattendedSettings>()
.AddUmbracoOptions<RichTextEditorSettings>()
.AddUmbracoOptions<BasicAuthSettings>()
.AddUmbracoOptions<RuntimeMinificationSettings>()
.AddUmbracoOptions<LegacyPasswordMigrationSettings>()
.AddUmbracoOptions<PackageMigrationSettings>()
.AddUmbracoOptions<ContentDashboardSettings>()
.AddUmbracoOptions<HelpPageSettings>();
options.UmbracoMediaPhysicalRootPath = options.UmbracoMediaPath;
}
}))
.AddUmbracoOptions<HealthChecksSettings>()
.AddUmbracoOptions<HostingSettings>()
.AddUmbracoOptions<ImagingSettings>()
.AddUmbracoOptions<IndexCreatorSettings>()
.AddUmbracoOptions<KeepAliveSettings>()
.AddUmbracoOptions<LoggingSettings>()
.AddUmbracoOptions<MemberPasswordConfigurationSettings>()
.AddUmbracoOptions<NuCacheSettings>()
.AddUmbracoOptions<RequestHandlerSettings>()
.AddUmbracoOptions<RuntimeSettings>()
.AddUmbracoOptions<SecuritySettings>()
.AddUmbracoOptions<TourSettings>()
.AddUmbracoOptions<TypeFinderSettings>()
.AddUmbracoOptions<UserPasswordConfigurationSettings>()
.AddUmbracoOptions<WebRoutingSettings>()
.AddUmbracoOptions<UmbracoPluginSettings>()
.AddUmbracoOptions<UnattendedSettings>()
.AddUmbracoOptions<RichTextEditorSettings>()
.AddUmbracoOptions<BasicAuthSettings>()
.AddUmbracoOptions<RuntimeMinificationSettings>()
.AddUmbracoOptions<LegacyPasswordMigrationSettings>()
.AddUmbracoOptions<PackageMigrationSettings>()
.AddUmbracoOptions<ContentDashboardSettings>()
.AddUmbracoOptions<HelpPageSettings>();
builder.Services.AddSingleton<IConfigureOptions<ConnectionStrings>, ConfigureConnectionStrings>();
builder.Services.AddSingleton<IConfigureOptions<ConnectionStrings>, ConfigureConnectionStrings>();
builder.Services.Configure<InstallDefaultDataSettings>(
Constants.Configuration.NamedOptions.InstallDefaultData.Languages,
builder.Config.GetSection($"{Constants.Configuration.ConfigInstallDefaultData}:{Constants.Configuration.NamedOptions.InstallDefaultData.Languages}"));
builder.Services.Configure<InstallDefaultDataSettings>(
Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes,
builder.Config.GetSection($"{Constants.Configuration.ConfigInstallDefaultData}:{Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes}"));
builder.Services.Configure<InstallDefaultDataSettings>(
Constants.Configuration.NamedOptions.InstallDefaultData.MediaTypes,
builder.Config.GetSection($"{Constants.Configuration.ConfigInstallDefaultData}:{Constants.Configuration.NamedOptions.InstallDefaultData.MediaTypes}"));
builder.Services.Configure<InstallDefaultDataSettings>(
Constants.Configuration.NamedOptions.InstallDefaultData.MemberTypes,
builder.Config.GetSection($"{Constants.Configuration.ConfigInstallDefaultData}:{Constants.Configuration.NamedOptions.InstallDefaultData.MemberTypes}"));
builder.Services.Configure<InstallDefaultDataSettings>(
Constants.Configuration.NamedOptions.InstallDefaultData.Languages,
builder.Config.GetSection($"{Constants.Configuration.ConfigInstallDefaultData}:{Constants.Configuration.NamedOptions.InstallDefaultData.Languages}"));
builder.Services.Configure<InstallDefaultDataSettings>(
Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes,
builder.Config.GetSection($"{Constants.Configuration.ConfigInstallDefaultData}:{Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes}"));
builder.Services.Configure<InstallDefaultDataSettings>(
Constants.Configuration.NamedOptions.InstallDefaultData.MediaTypes,
builder.Config.GetSection($"{Constants.Configuration.ConfigInstallDefaultData}:{Constants.Configuration.NamedOptions.InstallDefaultData.MediaTypes}"));
builder.Services.Configure<InstallDefaultDataSettings>(
Constants.Configuration.NamedOptions.InstallDefaultData.MemberTypes,
builder.Config.GetSection($"{Constants.Configuration.ConfigInstallDefaultData}:{Constants.Configuration.NamedOptions.InstallDefaultData.MemberTypes}"));
builder.Services.Configure<RequestHandlerSettings>(options => options.MergeReplacements(builder.Config));
builder.Services.Configure<RequestHandlerSettings>(options => options.MergeReplacements(builder.Config));
return builder;
}
return builder;
}
}

View File

@@ -0,0 +1,103 @@
using Microsoft.Extensions.Configuration;
using Umbraco.Cms.Core;
namespace Umbraco.Extensions;
/// <summary>
/// Extension methods for configuration.
/// </summary>
public static class ConfigurationExtensions
{
/// <summary>
/// The DataDirectory name.
/// </summary>
internal const string DataDirectoryName = "DataDirectory";
/// <summary>
/// The DataDirectory placeholder.
/// </summary>
internal const string DataDirectoryPlaceholder = "|DataDirectory|";
/// <summary>
/// The postfix used to identify a connection string provider setting.
/// </summary>
internal const string ProviderNamePostfix = "_ProviderName";
/// <summary>
/// Gets the provider name for the connection string name (shorthand for <c>GetSection("ConnectionStrings")[name + "_ProviderName"]</c>).
/// </summary>
/// <param name="configuration">The configuration.</param>
/// <param name="name">The connection string key.</param>
/// <returns>
/// The provider name.
/// </returns>
/// <remarks>
/// This uses the same convention as the <a href="https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?view=aspnetcore-6.0#connection-string-prefixes">Configuration API for connection string environment variables</a>.
/// </remarks>
public static string? GetConnectionStringProviderName(this IConfiguration configuration, string name)
=> configuration.GetConnectionString(name + ProviderNamePostfix);
/// <summary>
/// Gets the Umbraco connection string (shorthand for <c>GetSection("ConnectionStrings")[name]</c> and replacing the <c>|DataDirectory|</c> placeholder).
/// </summary>
/// <param name="configuration">The configuration.</param>
/// <param name="name">The connection string key.</param>
/// <returns>
/// The Umbraco connection string.
/// </returns>
public static string? GetUmbracoConnectionString(this IConfiguration configuration, string name = Constants.System.UmbracoConnectionName)
=> configuration.GetUmbracoConnectionString(name, out _);
/// <summary>
/// Gets the Umbraco connection string and provider name (shorthand for <c>GetSection("ConnectionStrings")[Constants.System.UmbracoConnectionName]</c> and replacing the <c>|DataDirectory|</c> placeholder).
/// </summary>
/// <param name="configuration">The configuration.</param>
/// <param name="providerName">The provider name.</param>
/// <returns>
/// The Umbraco connection string.
/// </returns>
public static string? GetUmbracoConnectionString(this IConfiguration configuration, out string? providerName)
=> configuration.GetUmbracoConnectionString(Constants.System.UmbracoConnectionName, out providerName);
/// <summary>
/// Gets the Umbraco connection string and provider name (shorthand for <c>GetSection("ConnectionStrings")[name]</c> and replacing the <c>|DataDirectory|</c> placeholder).
/// </summary>
/// <param name="configuration">The configuration.</param>
/// <param name="name">The name.</param>
/// <param name="providerName">The provider name.</param>
/// <returns>
/// The Umbraco connection string.
/// </returns>
public static string? GetUmbracoConnectionString(this IConfiguration configuration, string name, out string? providerName)
{
string? connectionString = configuration.GetConnectionString(name);
if (!string.IsNullOrEmpty(connectionString))
{
// Replace data directory
connectionString = ReplaceDataDirectoryPlaceholder(connectionString);
// Get provider name
providerName = configuration.GetConnectionStringProviderName(name);
}
else
{
providerName = null;
}
return connectionString;
}
internal static string? ReplaceDataDirectoryPlaceholder(string? connectionString)
{
if (!string.IsNullOrEmpty(connectionString))
{
string? dataDirectory = AppDomain.CurrentDomain.GetData(DataDirectoryName)?.ToString();
if (!string.IsNullOrEmpty(dataDirectory))
{
return connectionString.Replace(DataDirectoryPlaceholder, dataDirectory);
}
}
return connectionString;
}
}

View File

@@ -1,35 +1,24 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System;
using Microsoft.Extensions.Configuration;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
namespace Umbraco.Extensions
namespace Umbraco.Extensions;
/// <summary>
/// Extension methods for a connection string.
/// </summary>
public static class ConnectionStringExtensions
{
public static class ConnectionStringExtensions
{
public static bool IsConnectionStringConfigured(this ConnectionStrings connectionString)
=> connectionString != null &&
!string.IsNullOrWhiteSpace(connectionString.ConnectionString) &&
!string.IsNullOrWhiteSpace(connectionString.ProviderName);
/// <summary>
/// Gets a connection string from configuration with placeholders replaced.
/// </summary>
public static string? GetUmbracoConnectionString(
this IConfiguration configuration,
string connectionStringName = Constants.System.UmbracoConnectionName) =>
configuration.GetConnectionString(connectionStringName).ReplaceDataDirectoryPlaceholder();
/// <summary>
/// Replaces instances of the |DataDirectory| placeholder in a string with the value of AppDomain DataDirectory.
/// </summary>
public static string? ReplaceDataDirectoryPlaceholder(this string input)
{
var dataDirectory = AppDomain.CurrentDomain.GetData("DataDirectory")?.ToString();
return input?.Replace(ConnectionStrings.DataDirectoryPlaceholder, dataDirectory);
}
}
/// <summary>
/// Determines whether the connection string is configured (set to a non-empty value).
/// </summary>
/// <param name="connectionString">The connection string.</param>
/// <returns>
/// <c>true</c> if the connection string is configured; otherwise, <c>false</c>.
/// </returns>
public static bool IsConnectionStringConfigured(this ConnectionStrings connectionString)
=> connectionString != null &&
!string.IsNullOrWhiteSpace(connectionString.ConnectionString) &&
!string.IsNullOrWhiteSpace(connectionString.ProviderName);
}

View File

@@ -1,7 +1,3 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
@@ -13,9 +9,7 @@ using Umbraco.Extensions;
namespace Umbraco.Cms.Infrastructure.Install.InstallSteps
{
[InstallSetupStep(InstallationType.NewInstall,
"DatabaseConfigure", "database", 10, "Setting up a database, so Umbraco has a place to store your website",
PerformsAppRestart = true)]
[InstallSetupStep(InstallationType.NewInstall, "DatabaseConfigure", "database", 10, "Setting up a database, so Umbraco has a place to store your website", PerformsAppRestart = true)]
public class DatabaseConfigureStep : InstallSetupStep<DatabaseModel>
{
private readonly DatabaseBuilder _databaseBuilder;
@@ -45,44 +39,33 @@ namespace Umbraco.Cms.Infrastructure.Install.InstallSteps
return Task.FromResult<InstallSetupResult?>(null);
}
public override object ViewModel
public override object ViewModel => new
{
get
{
var options = _databaseProviderMetadata
.Where(x => x.IsAvailable)
.OrderBy(x => x.SortOrder)
.ToList();
databases = _databaseProviderMetadata.GetAvailable().ToList()
};
return new
{
databases = options
};
}
}
public override string View => ShouldDisplayView() ? base.View : "";
public override string View => ShouldDisplayView() ? base.View : string.Empty;
public override bool RequiresExecution(DatabaseModel model) => ShouldDisplayView();
private bool ShouldDisplayView()
{
//If the connection string is already present in web.config we don't need to show the settings page and we jump to installing/upgrading.
// If the connection string is already present in web.config we don't need to show the settings page and we jump to installing/upgrading.
var databaseSettings = _connectionStrings.Get(Core.Constants.System.UmbracoConnectionName);
if (databaseSettings.IsConnectionStringConfigured())
{
try
{
//Since a connection string was present we verify the db can connect and query
_ = _databaseBuilder.ValidateSchema();
// Since a connection string was present we verify the db can connect and query
_databaseBuilder.ValidateSchema();
return false;
}
catch (Exception ex)
{
// Something went wrong, could not connect so probably need to reconfigure
_logger.LogError(ex, "An error occurred, reconfiguring...");
//something went wrong, could not connect so probably need to reconfigure
return true;
}
}

View File

@@ -1,10 +1,5 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using Umbraco.Cms.Core.Configuration.Models;
@@ -118,10 +113,7 @@ namespace Umbraco.Cms.Infrastructure.Install.InstallSteps
{
get
{
var quickInstallSettings = _databaseProviderMetadata
.Where(x => x.SupportsQuickInstall)
.Where(x => x.IsAvailable)
.OrderBy(x => x.SortOrder)
var quickInstallSettings = _databaseProviderMetadata.GetAvailable(true)
.Select(x => new
{
displayName = x.DisplayName,

View File

@@ -69,10 +69,10 @@ namespace Umbraco.Cms.Infrastructure.Install
break;
case RuntimeLevelReason.UpgradePackageMigrations:
{
if (!_runtimeState.StartupState.TryGetValue(RuntimeState.PendingPacakgeMigrationsStateKey, out var pm)
if (!_runtimeState.StartupState.TryGetValue(RuntimeState.PendingPackageMigrationsStateKey, out var pm)
|| pm is not IReadOnlyList<string> pendingMigrations)
{
throw new InvalidOperationException($"The required key {RuntimeState.PendingPacakgeMigrationsStateKey} does not exist in startup state");
throw new InvalidOperationException($"The required key {RuntimeState.PendingPackageMigrationsStateKey} does not exist in startup state");
}
if (pendingMigrations.Count == 0)

View File

@@ -1,7 +1,4 @@
using System;
using System.Collections.Generic;
using System.Data.Common;
using System.Linq;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core;
@@ -139,20 +136,15 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install
// if the database model is null then we will attempt quick install.
if (databaseSettings == null)
{
providerMeta = _databaseProviderMetadata
.OrderBy(x => x.SortOrder)
.Where(x => x.SupportsQuickInstall)
.FirstOrDefault(x => x.IsAvailable);
providerMeta = _databaseProviderMetadata.GetAvailable(true).FirstOrDefault();
databaseSettings = new DatabaseModel
{
DatabaseName = providerMeta?.DefaultDatabaseName!,
DatabaseName = providerMeta?.DefaultDatabaseName!
};
}
else
{
providerMeta = _databaseProviderMetadata
.FirstOrDefault(x => x.Id == databaseSettings.DatabaseProviderMetadataId);
providerMeta = _databaseProviderMetadata.FirstOrDefault(x => x.Id == databaseSettings.DatabaseProviderMetadataId);
}
if (providerMeta == null)
@@ -177,7 +169,6 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install
return true;
}
private void Configure(string connectionString, string? providerName, bool installMissingDatabase)
{
// Update existing connection string

View File

@@ -0,0 +1,55 @@
using Umbraco.Cms.Core.Install.Models;
namespace Umbraco.Cms.Infrastructure.Persistence;
/// <summary>
/// Extension methods for <see cref="IDatabaseProviderMetadata" />.
/// </summary>
public static class DatabaseProviderMetadataExtensions
{
/// <summary>
/// Gets the available database provider metadata.
/// </summary>
/// <param name="databaseProviderMetadata">The database provider metadata.</param>
/// <param name="onlyQuickInstall">If set to <c>true</c> only returns providers that support quick install.</param>
/// <returns>
/// The available database provider metadata.
/// </returns>
public static IEnumerable<IDatabaseProviderMetadata> GetAvailable(this IEnumerable<IDatabaseProviderMetadata> databaseProviderMetadata, bool onlyQuickInstall = false)
=> databaseProviderMetadata.Where(x => (!onlyQuickInstall || x.SupportsQuickInstall) && x.IsAvailable).OrderBy(x => x.SortOrder);
/// <summary>
/// Determines whether a database can be created for the specified provider name while ignoring the value of <see cref="GlobalSettings.InstallMissingDatabase" />.
/// </summary>
/// <param name="databaseProviderMetadata">The database provider metadata.</param>
/// <param name="providerName">The name of the provider.</param>
/// <returns>
/// <c>true</c> if a database can be created for the specified provider name; otherwise, <c>false</c>.
/// </returns>
public static bool CanForceCreateDatabase(this IEnumerable<IDatabaseProviderMetadata> databaseProviderMetadata, string? providerName)
=> databaseProviderMetadata.FirstOrDefault(x => x.ProviderName == providerName)?.ForceCreateDatabase == true;
/// <summary>
/// Generates the connection string.
/// </summary>
/// <param name="databaseProviderMetadata">The database provider metadata.</param>
/// <param name="databaseName">The name of the database, uses the default database name when <c>null</c>.</param>
/// <param name="server">The server.</param>
/// <param name="login">The login.</param>
/// <param name="password">The password.</param>
/// <param name="integratedAuth">Indicates whether integrated authentication should be used (when supported by the provider).</param>
/// <returns>
/// The generated connection string.
/// </returns>
public static string? GenerateConnectionString(this IDatabaseProviderMetadata databaseProviderMetadata, string? databaseName = null, string? server = null, string? login = null, string? password = null, bool? integratedAuth = null)
=> databaseProviderMetadata.GenerateConnectionString(new DatabaseModel()
{
DatabaseProviderMetadataId = databaseProviderMetadata.Id,
ProviderName = databaseProviderMetadata.ProviderName,
DatabaseName = databaseName ?? databaseProviderMetadata.DefaultDatabaseName,
Server = server ?? string.Empty,
Login = login ?? string.Empty,
Password = password ?? string.Empty,
IntegratedAuth = integratedAuth == true && databaseProviderMetadata.SupportsIntegratedAuthentication
});
}

View File

@@ -1,6 +1,3 @@
using System;
using System.Collections.Generic;
using System.Threading;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@@ -14,7 +11,6 @@ using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Infrastructure.Migrations.Upgrade;
using Umbraco.Cms.Infrastructure.Persistence;
using Umbraco.Cms.Web.Common.DependencyInjection;
using Umbraco.Extensions;
namespace Umbraco.Cms.Infrastructure.Runtime
{
@@ -24,7 +20,8 @@ namespace Umbraco.Cms.Infrastructure.Runtime
/// </summary>
public class RuntimeState : IRuntimeState
{
internal const string PendingPacakgeMigrationsStateKey = "PendingPackageMigrations";
internal const string PendingPackageMigrationsStateKey = "PendingPackageMigrations";
private readonly IOptions<GlobalSettings> _globalSettings = null!;
private readonly IOptions<UnattendedSettings> _unattendedSettings = null!;
private readonly IUmbracoVersion _umbracoVersion = null!;
@@ -33,6 +30,7 @@ namespace Umbraco.Cms.Infrastructure.Runtime
private readonly PendingPackageMigrations _packageMigrationState = null!;
private readonly Dictionary<string, object> _startupState = new Dictionary<string, object>();
private readonly IConflictingRouteService _conflictingRouteService = null!;
private readonly IEnumerable<IDatabaseProviderMetadata> _databaseProviderMetadata = null!;
/// <summary>
/// The initial <see cref="RuntimeState"/>
@@ -41,17 +39,17 @@ namespace Umbraco.Cms.Infrastructure.Runtime
public static RuntimeState Booting() => new RuntimeState() { Level = RuntimeLevel.Boot };
private RuntimeState()
{
}
{ }
public RuntimeState(
IOptions<GlobalSettings> globalSettings,
IOptions<UnattendedSettings> unattendedSettings,
IUmbracoVersion umbracoVersion,
IUmbracoDatabaseFactory databaseFactory,
ILogger<RuntimeState> logger,
PendingPackageMigrations packageMigrationState,
IConflictingRouteService conflictingRouteService)
IOptions<GlobalSettings> globalSettings,
IOptions<UnattendedSettings> unattendedSettings,
IUmbracoVersion umbracoVersion,
IUmbracoDatabaseFactory databaseFactory,
ILogger<RuntimeState> logger,
PendingPackageMigrations packageMigrationState,
IConflictingRouteService conflictingRouteService,
IEnumerable<IDatabaseProviderMetadata> databaseProviderMetadata)
{
_globalSettings = globalSettings;
_unattendedSettings = unattendedSettings;
@@ -60,8 +58,29 @@ namespace Umbraco.Cms.Infrastructure.Runtime
_logger = logger;
_packageMigrationState = packageMigrationState;
_conflictingRouteService = conflictingRouteService;
_databaseProviderMetadata = databaseProviderMetadata;
}
[Obsolete("Use ctor with all params. This will be removed in Umbraco 12")]
public RuntimeState(
IOptions<GlobalSettings> globalSettings,
IOptions<UnattendedSettings> unattendedSettings,
IUmbracoVersion umbracoVersion,
IUmbracoDatabaseFactory databaseFactory,
ILogger<RuntimeState> logger,
PendingPackageMigrations packageMigrationState,
IConflictingRouteService conflictingRouteService)
: this(
globalSettings,
unattendedSettings,
umbracoVersion,
databaseFactory,
logger,
packageMigrationState,
StaticServiceProvider.Instance.GetRequiredService<IConflictingRouteService>(),
StaticServiceProvider.Instance.GetServices<IDatabaseProviderMetadata>())
{ }
/// <summary>
/// Initializes a new instance of the <see cref="RuntimeState"/> class.
/// </summary>
@@ -81,8 +100,7 @@ namespace Umbraco.Cms.Infrastructure.Runtime
logger,
packageMigrationState,
StaticServiceProvider.Instance.GetRequiredService<IConflictingRouteService>())
{
}
{ }
/// <inheritdoc />
public Version Version => _umbracoVersion.Version;
@@ -143,7 +161,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 || CanAutoInstallMissingDatabase(_databaseFactory))
if (_globalSettings.Value.InstallMissingDatabase || _databaseProviderMetadata.CanForceCreateDatabase(_databaseFactory.ProviderName))
{
// ok to install on a configured but missing database
Level = RuntimeLevel.Install;
@@ -257,7 +275,7 @@ namespace Umbraco.Cms.Infrastructure.Runtime
IReadOnlyList<string> packagesRequiringMigration = _packageMigrationState.GetPendingPackageMigrations(keyValues);
if (packagesRequiringMigration.Count > 0)
{
_startupState[PendingPacakgeMigrationsStateKey] = packagesRequiringMigration;
_startupState[PendingPackageMigrationsStateKey] = packagesRequiringMigration;
return UmbracoDatabaseState.NeedsPackageMigration;
}
@@ -311,8 +329,5 @@ namespace Umbraco.Cms.Infrastructure.Runtime
return canConnect;
}
private bool CanAutoInstallMissingDatabase(IUmbracoDatabaseFactory databaseFactory)
=> databaseFactory.ConnectionString?.InvariantContains("(localdb)") == true;
}
}

View File

@@ -1,36 +0,0 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System;
using NUnit.Framework;
using Umbraco.Cms.Core.Configuration;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Tests.UnitTests.AutoFixture;
using Constants = Umbraco.Cms.Core.Constants;
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Configuration.Models
{
[TestFixture]
public class ConnectionStringsTests
{
[Test]
public void ProviderName_WhenNotExplicitlySet_HasDefaultSet()
{
var sut = new ConnectionStrings();
Assert.That(sut.ProviderName, Is.EqualTo(ConnectionStrings.DefaultProviderName));
}
[Test]
[AutoMoqData]
public void ConnectionString_WhenSetterCalled_ReplacesDataDirectoryPlaceholder(string aDataDirectory)
{
AppDomain.CurrentDomain.SetData("DataDirectory", aDataDirectory);
var sut = new ConnectionStrings
{
ConnectionString = $"{ConnectionStrings.DataDirectoryPlaceholder}/foo"
};
Assert.That(sut.ConnectionString, Contains.Substring($"{aDataDirectory}/foo"));
}
}
}

View File

@@ -9,7 +9,7 @@ using Umbraco.Cms.Core.Configuration;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Tests.UnitTests.AutoFixture;
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Configuration;
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Configuration;
[TestFixture]
public class ConfigureConnectionStringsTests
@@ -27,7 +27,7 @@ public class ConfigureConnectionStringsTests
var configuration = configurationBuilder.Build();
var services = new ServiceCollection();
services.AddOptions<ConnectionStrings>().Bind(configuration.GetSection("ConnectionStrings"));
services.AddOptions();
services.AddSingleton<IConfigureOptions<ConnectionStrings>, ConfigureConnectionStrings>();
services.AddSingleton<IConfiguration>(configuration);

View File

@@ -0,0 +1,130 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System;
using Microsoft.Extensions.Configuration;
using Moq;
using NUnit.Framework;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Extensions;
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Extensions
{
[TestFixture]
public class ConfigurationExtensionsTests
{
private const string DataDirectory = @"C:\Data";
[Test]
public void CanParseSqlServerConnectionString()
{
const string ConfiguredConnectionString = @"Server=.\SQLEXPRESS;Database=UmbracoCms;Integrated Security=true";
Mock<IConfiguration> mockedConfig = CreateConfig(ConfiguredConnectionString);
string connectionString = mockedConfig.Object.GetUmbracoConnectionString(out string providerName);
AssertResults(
ConfiguredConnectionString,
"Microsoft.Data.SqlClient",
connectionString,
providerName);
}
[Test]
public void CanParseLocalDbConnectionString()
{
const string ConfiguredConnectionString = @"Server=(LocalDb)\MyInstance;Integrated Security=true;";
Mock<IConfiguration> mockedConfig = CreateConfig(ConfiguredConnectionString);
string connectionString = mockedConfig.Object.GetUmbracoConnectionString(out string providerName);
AssertResults(
ConfiguredConnectionString,
"Microsoft.Data.SqlClient",
connectionString,
providerName);
}
[Test]
public void CanParseLocalDbConnectionStringWithDataDirectory()
{
const string ConfiguredConnectionString = @"Data Source=(LocalDb)\MyInstance;Initial Catalog=UmbracoDb;Integrated Security=SSPI;AttachDBFilename=|DataDirectory|\Umbraco.mdf";
Mock<IConfiguration> mockedConfig = CreateConfig(ConfiguredConnectionString);
SetDataDirectory();
string connectionString = mockedConfig.Object.GetUmbracoConnectionString(out string providerName);
AssertResults(
@"Data Source=(LocalDb)\MyInstance;Initial Catalog=UmbracoDb;Integrated Security=SSPI;AttachDBFilename=C:\Data\Umbraco.mdf",
"Microsoft.Data.SqlClient",
connectionString,
providerName);
}
[Test]
public void CanParseSQLiteConnectionStringWithDataDirectory()
{
const string ConfiguredConnectionString = "Data Source=|DataDirectory|/Umbraco.sqlite.db;Cache=Shared;Foreign Keys=True;Pooling=True";
const string ConfiguredProviderName = "Microsoft.Data.SQLite";
Mock<IConfiguration> mockedConfig = CreateConfig(ConfiguredConnectionString, ConfiguredProviderName);
SetDataDirectory();
string connectionString = mockedConfig.Object.GetUmbracoConnectionString(out string providerName);
AssertResults(
@"Data Source=C:\Data/Umbraco.sqlite.db;Cache=Shared;Foreign Keys=True;Pooling=True",
"Microsoft.Data.SQLite",
connectionString,
providerName);
}
[Test]
public void CanParseConnectionStringWithNamedProvider()
{
const string ConfiguredConnectionString = @"Server=.\SQLEXPRESS;Database=UmbracoCms;Integrated Security=true";
const string ConfiguredProviderName = "MyProvider";
Mock<IConfiguration> mockedConfig = CreateConfig(ConfiguredConnectionString, ConfiguredProviderName);
string connectionString = mockedConfig.Object.GetUmbracoConnectionString(out string providerName);
AssertResults(
ConfiguredConnectionString,
ConfiguredProviderName,
connectionString,
providerName);
}
private static Mock<IConfiguration> CreateConfig(string configuredConnectionString, string configuredProviderName = ConnectionStrings.DefaultProviderName)
{
var mockConfigSection = new Mock<IConfigurationSection>();
mockConfigSection
.SetupGet(m => m[It.Is<string>(s => s == Constants.System.UmbracoConnectionName)])
.Returns(configuredConnectionString);
mockConfigSection
.SetupGet(m => m[It.Is<string>(s => s == $"{Constants.System.UmbracoConnectionName}{ConnectionStrings.ProviderNamePostfix}")])
.Returns(configuredProviderName);
var mockedConfig = new Mock<IConfiguration>();
mockedConfig
.Setup(a => a.GetSection(It.Is<string>(s => s == "ConnectionStrings")))
.Returns(mockConfigSection.Object);
return mockedConfig;
}
private static void SetDataDirectory() =>
AppDomain.CurrentDomain.SetData("DataDirectory", DataDirectory);
private static void AssertResults(string expectedConnectionString, string expectedProviderName, string connectionString, string providerName)
{
Assert.AreEqual(expectedConnectionString, connectionString);
Assert.AreEqual(expectedProviderName, providerName);
}
}
}