diff --git a/src/Umbraco.Core/Configuration/ConfigureConnectionStrings.cs b/src/Umbraco.Core/Configuration/ConfigureConnectionStrings.cs index bd982c18b9..829d19bb53 100644 --- a/src/Umbraco.Core/Configuration/ConfigureConnectionStrings.cs +++ b/src/Umbraco.Core/Configuration/ConfigureConnectionStrings.cs @@ -6,16 +6,18 @@ using Umbraco.Extensions; namespace Umbraco.Cms.Core.Configuration; /// -/// Configures ConnectionStrings. +/// Configures the named option. /// public class ConfigureConnectionStrings : IConfigureNamedOptions { private readonly IConfiguration _configuration; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - public ConfigureConnectionStrings(IConfiguration configuration) => _configuration = configuration; + /// The configuration. + public ConfigureConnectionStrings(IConfiguration configuration) + => _configuration = configuration; /// public void Configure(ConnectionStrings options) => Configure(Options.DefaultName, options); @@ -35,7 +37,7 @@ public class ConfigureConnectionStrings : IConfigureNamedOptions +/// Represents a single connection string. +/// +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 /// /// The DataDirectory placeholder. /// - public const string DataDirectoryPlaceholder = "|DataDirectory|"; + public const string DataDirectoryPlaceholder = ConfigurationExtensions.DataDirectoryPlaceholder; /// /// The postfix used to identify a connection strings provider setting. /// - public const string ProviderNamePostfix = "_ProviderName"; + public const string ProviderNamePostfix = ConfigurationExtensions.ProviderNamePostfix; + /// + /// Gets or sets the name. + /// + /// + /// The name. + /// + [Obsolete("This property will be removed in Umbraco 12, because this class is now using named options.")] public string? Name { get; set; } + /// + /// Gets or sets the connection string. + /// + /// + /// The connection string. + /// + /// + /// When set, the will be replaced with the actual physical path. + /// public string? ConnectionString { get => _connectionString; - set => _connectionString = value?.ReplaceDataDirectoryPlaceholder(); + set => _connectionString = ConfigurationExtensions.ReplaceDataDirectoryPlaceholder(value); } - public string? ProviderName { get; set; } = DefaultProviderName; + /// + /// Gets or sets the name of the provider. + /// + /// + /// The name of the provider. + /// + public string? ProviderName { get; set; } } diff --git a/src/Umbraco.Core/Constants-System.cs b/src/Umbraco.Core/Constants-System.cs index 8b19781d39..0ad9852671 100644 --- a/src/Umbraco.Core/Constants-System.cs +++ b/src/Umbraco.Core/Constants-System.cs @@ -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"; } } } diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs index a8a374fef2..57607d347a 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs @@ -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; + +/// +/// Extension methods for +/// +public static partial class UmbracoBuilderExtensions { - /// - /// Extension methods for - /// - public static partial class UmbracoBuilderExtensions + private static IUmbracoBuilder AddUmbracoOptions(this IUmbracoBuilder builder, Action>? configure = null) + where TOptions : class { - - private static IUmbracoBuilder AddUmbracoOptions(this IUmbracoBuilder builder, Action>? configure = null) - where TOptions : class + var umbracoOptionsAttribute = typeof(TOptions).GetCustomAttribute(); + if (umbracoOptionsAttribute is null) { - var umbracoOptionsAttribute = typeof(TOptions).GetCustomAttribute(); - if (umbracoOptionsAttribute is null) + throw new ArgumentException($"{typeof(TOptions)} do not have the UmbracoOptionsAttribute."); + } + + var optionsBuilder = builder.Services.AddOptions() + .Bind( + builder.Config.GetSection(umbracoOptionsAttribute.ConfigurationKey), + o => o.BindNonPublicProperties = umbracoOptionsAttribute.BindNonPublicProperties) + .ValidateDataAnnotations(); + + configure?.Invoke(optionsBuilder); + + return builder; + } + + /// + /// Add Umbraco configuration services and options + /// + public static IUmbracoBuilder AddConfiguration(this IUmbracoBuilder builder) + { + // Register configuration validators. + builder.Services.AddSingleton, ContentSettingsValidator>(); + builder.Services.AddSingleton, GlobalSettingsValidator>(); + builder.Services.AddSingleton, HealthChecksSettingsValidator>(); + builder.Services.AddSingleton, RequestHandlerSettingsValidator>(); + builder.Services.AddSingleton, UnattendedSettingsValidator>(); + + // Register configuration sections. + builder + .AddUmbracoOptions() + .AddUmbracoOptions() + .AddUmbracoOptions() + .AddUmbracoOptions() + .AddUmbracoOptions() + .AddUmbracoOptions(optionsBuilder => optionsBuilder.PostConfigure(options => { - throw new ArgumentException($"{typeof(TOptions)} do not have the UmbracoOptionsAttribute."); - } - - var optionsBuilder = builder.Services.AddOptions() - .Bind( - builder.Config.GetSection(umbracoOptionsAttribute.ConfigurationKey), - o => o.BindNonPublicProperties = umbracoOptionsAttribute.BindNonPublicProperties - ) - .ValidateDataAnnotations(); - - configure?.Invoke(optionsBuilder); - - return builder; - } - - /// - /// Add Umbraco configuration services and options - /// - public static IUmbracoBuilder AddConfiguration(this IUmbracoBuilder builder) - { - // Register configuration validators. - builder.Services.AddSingleton, ContentSettingsValidator>(); - builder.Services.AddSingleton, GlobalSettingsValidator>(); - builder.Services.AddSingleton, HealthChecksSettingsValidator>(); - builder.Services.AddSingleton, RequestHandlerSettingsValidator>(); - builder.Services.AddSingleton, UnattendedSettingsValidator>(); - - // Register configuration sections. - builder - .AddUmbracoOptions() - .AddUmbracoOptions() - .AddUmbracoOptions() - .AddUmbracoOptions() - .AddUmbracoOptions() - .AddUmbracoOptions() - .AddUmbracoOptions(optionsBuilder => optionsBuilder.PostConfigure(options => + if (string.IsNullOrEmpty(options.UmbracoMediaPhysicalRootPath)) { - if (string.IsNullOrEmpty(options.UmbracoMediaPhysicalRootPath)) - { - options.UmbracoMediaPhysicalRootPath = options.UmbracoMediaPath; - } - })) - .AddUmbracoOptions() - .AddUmbracoOptions() - .AddUmbracoOptions() - .AddUmbracoOptions() - .AddUmbracoOptions() - .AddUmbracoOptions() - .AddUmbracoOptions() - .AddUmbracoOptions() - .AddUmbracoOptions() - .AddUmbracoOptions() - .AddUmbracoOptions() - .AddUmbracoOptions() - .AddUmbracoOptions() - .AddUmbracoOptions() - .AddUmbracoOptions() - .AddUmbracoOptions() - .AddUmbracoOptions() - .AddUmbracoOptions() - .AddUmbracoOptions() - .AddUmbracoOptions() - .AddUmbracoOptions() - .AddUmbracoOptions() - .AddUmbracoOptions() - .AddUmbracoOptions(); + options.UmbracoMediaPhysicalRootPath = options.UmbracoMediaPath; + } + })) + .AddUmbracoOptions() + .AddUmbracoOptions() + .AddUmbracoOptions() + .AddUmbracoOptions() + .AddUmbracoOptions() + .AddUmbracoOptions() + .AddUmbracoOptions() + .AddUmbracoOptions() + .AddUmbracoOptions() + .AddUmbracoOptions() + .AddUmbracoOptions() + .AddUmbracoOptions() + .AddUmbracoOptions() + .AddUmbracoOptions() + .AddUmbracoOptions() + .AddUmbracoOptions() + .AddUmbracoOptions() + .AddUmbracoOptions() + .AddUmbracoOptions() + .AddUmbracoOptions() + .AddUmbracoOptions() + .AddUmbracoOptions() + .AddUmbracoOptions() + .AddUmbracoOptions(); - builder.Services.AddSingleton, ConfigureConnectionStrings>(); + builder.Services.AddSingleton, ConfigureConnectionStrings>(); - builder.Services.Configure( - Constants.Configuration.NamedOptions.InstallDefaultData.Languages, - builder.Config.GetSection($"{Constants.Configuration.ConfigInstallDefaultData}:{Constants.Configuration.NamedOptions.InstallDefaultData.Languages}")); - builder.Services.Configure( - Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - builder.Config.GetSection($"{Constants.Configuration.ConfigInstallDefaultData}:{Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes}")); - builder.Services.Configure( - Constants.Configuration.NamedOptions.InstallDefaultData.MediaTypes, - builder.Config.GetSection($"{Constants.Configuration.ConfigInstallDefaultData}:{Constants.Configuration.NamedOptions.InstallDefaultData.MediaTypes}")); - builder.Services.Configure( - Constants.Configuration.NamedOptions.InstallDefaultData.MemberTypes, - builder.Config.GetSection($"{Constants.Configuration.ConfigInstallDefaultData}:{Constants.Configuration.NamedOptions.InstallDefaultData.MemberTypes}")); + builder.Services.Configure( + Constants.Configuration.NamedOptions.InstallDefaultData.Languages, + builder.Config.GetSection($"{Constants.Configuration.ConfigInstallDefaultData}:{Constants.Configuration.NamedOptions.InstallDefaultData.Languages}")); + builder.Services.Configure( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + builder.Config.GetSection($"{Constants.Configuration.ConfigInstallDefaultData}:{Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes}")); + builder.Services.Configure( + Constants.Configuration.NamedOptions.InstallDefaultData.MediaTypes, + builder.Config.GetSection($"{Constants.Configuration.ConfigInstallDefaultData}:{Constants.Configuration.NamedOptions.InstallDefaultData.MediaTypes}")); + builder.Services.Configure( + Constants.Configuration.NamedOptions.InstallDefaultData.MemberTypes, + builder.Config.GetSection($"{Constants.Configuration.ConfigInstallDefaultData}:{Constants.Configuration.NamedOptions.InstallDefaultData.MemberTypes}")); - builder.Services.Configure(options => options.MergeReplacements(builder.Config)); + builder.Services.Configure(options => options.MergeReplacements(builder.Config)); - return builder; - } + return builder; } } diff --git a/src/Umbraco.Core/Extensions/ConfigurationExtensions.cs b/src/Umbraco.Core/Extensions/ConfigurationExtensions.cs new file mode 100644 index 0000000000..d719876d9f --- /dev/null +++ b/src/Umbraco.Core/Extensions/ConfigurationExtensions.cs @@ -0,0 +1,103 @@ +using Microsoft.Extensions.Configuration; +using Umbraco.Cms.Core; + +namespace Umbraco.Extensions; + +/// +/// Extension methods for configuration. +/// +public static class ConfigurationExtensions +{ + /// + /// The DataDirectory name. + /// + internal const string DataDirectoryName = "DataDirectory"; + + /// + /// The DataDirectory placeholder. + /// + internal const string DataDirectoryPlaceholder = "|DataDirectory|"; + + /// + /// The postfix used to identify a connection string provider setting. + /// + internal const string ProviderNamePostfix = "_ProviderName"; + + /// + /// Gets the provider name for the connection string name (shorthand for GetSection("ConnectionStrings")[name + "_ProviderName"]). + /// + /// The configuration. + /// The connection string key. + /// + /// The provider name. + /// + /// + /// This uses the same convention as the Configuration API for connection string environment variables. + /// + public static string? GetConnectionStringProviderName(this IConfiguration configuration, string name) + => configuration.GetConnectionString(name + ProviderNamePostfix); + + /// + /// Gets the Umbraco connection string (shorthand for GetSection("ConnectionStrings")[name] and replacing the |DataDirectory| placeholder). + /// + /// The configuration. + /// The connection string key. + /// + /// The Umbraco connection string. + /// + public static string? GetUmbracoConnectionString(this IConfiguration configuration, string name = Constants.System.UmbracoConnectionName) + => configuration.GetUmbracoConnectionString(name, out _); + + /// + /// Gets the Umbraco connection string and provider name (shorthand for GetSection("ConnectionStrings")[Constants.System.UmbracoConnectionName] and replacing the |DataDirectory| placeholder). + /// + /// The configuration. + /// The provider name. + /// + /// The Umbraco connection string. + /// + public static string? GetUmbracoConnectionString(this IConfiguration configuration, out string? providerName) + => configuration.GetUmbracoConnectionString(Constants.System.UmbracoConnectionName, out providerName); + + /// + /// Gets the Umbraco connection string and provider name (shorthand for GetSection("ConnectionStrings")[name] and replacing the |DataDirectory| placeholder). + /// + /// The configuration. + /// The name. + /// The provider name. + /// + /// The Umbraco connection string. + /// + 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; + } +} diff --git a/src/Umbraco.Core/Extensions/ConnectionStringExtensions.cs b/src/Umbraco.Core/Extensions/ConnectionStringExtensions.cs index 6af569e514..3077a570dc 100644 --- a/src/Umbraco.Core/Extensions/ConnectionStringExtensions.cs +++ b/src/Umbraco.Core/Extensions/ConnectionStringExtensions.cs @@ -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; + +/// +/// Extension methods for a connection string. +/// +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); - - /// - /// Gets a connection string from configuration with placeholders replaced. - /// - public static string? GetUmbracoConnectionString( - this IConfiguration configuration, - string connectionStringName = Constants.System.UmbracoConnectionName) => - configuration.GetConnectionString(connectionStringName).ReplaceDataDirectoryPlaceholder(); - - /// - /// Replaces instances of the |DataDirectory| placeholder in a string with the value of AppDomain DataDirectory. - /// - public static string? ReplaceDataDirectoryPlaceholder(this string input) - { - var dataDirectory = AppDomain.CurrentDomain.GetData("DataDirectory")?.ToString(); - return input?.Replace(ConnectionStrings.DataDirectoryPlaceholder, dataDirectory); - } - } + /// + /// Determines whether the connection string is configured (set to a non-empty value). + /// + /// The connection string. + /// + /// true if the connection string is configured; otherwise, false. + /// + public static bool IsConnectionStringConfigured(this ConnectionStrings connectionString) + => connectionString != null && + !string.IsNullOrWhiteSpace(connectionString.ConnectionString) && + !string.IsNullOrWhiteSpace(connectionString.ProviderName); } diff --git a/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseConfigureStep.cs b/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseConfigureStep.cs index 44879f31b8..f01b65aaa7 100644 --- a/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseConfigureStep.cs +++ b/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseConfigureStep.cs @@ -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 { private readonly DatabaseBuilder _databaseBuilder; @@ -45,44 +39,33 @@ namespace Umbraco.Cms.Infrastructure.Install.InstallSteps return Task.FromResult(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; } } diff --git a/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs b/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs index 38ac000452..4f5dbc3cc2 100644 --- a/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs +++ b/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs @@ -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, diff --git a/src/Umbraco.Infrastructure/Install/UnattendedUpgrader.cs b/src/Umbraco.Infrastructure/Install/UnattendedUpgrader.cs index 312106ecca..3b891b88c4 100644 --- a/src/Umbraco.Infrastructure/Install/UnattendedUpgrader.cs +++ b/src/Umbraco.Infrastructure/Install/UnattendedUpgrader.cs @@ -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 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) diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs index 542494458e..c4a37d4374 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs @@ -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 diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseProviderMetadataExtensions.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseProviderMetadataExtensions.cs new file mode 100644 index 0000000000..d0ad59fbb8 --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseProviderMetadataExtensions.cs @@ -0,0 +1,55 @@ +using Umbraco.Cms.Core.Install.Models; + +namespace Umbraco.Cms.Infrastructure.Persistence; + +/// +/// Extension methods for . +/// +public static class DatabaseProviderMetadataExtensions +{ + /// + /// Gets the available database provider metadata. + /// + /// The database provider metadata. + /// If set to true only returns providers that support quick install. + /// + /// The available database provider metadata. + /// + public static IEnumerable GetAvailable(this IEnumerable databaseProviderMetadata, bool onlyQuickInstall = false) + => databaseProviderMetadata.Where(x => (!onlyQuickInstall || x.SupportsQuickInstall) && x.IsAvailable).OrderBy(x => x.SortOrder); + + /// + /// Determines whether a database can be created for the specified provider name while ignoring the value of . + /// + /// The database provider metadata. + /// The name of the provider. + /// + /// true if a database can be created for the specified provider name; otherwise, false. + /// + public static bool CanForceCreateDatabase(this IEnumerable databaseProviderMetadata, string? providerName) + => databaseProviderMetadata.FirstOrDefault(x => x.ProviderName == providerName)?.ForceCreateDatabase == true; + + /// + /// Generates the connection string. + /// + /// The database provider metadata. + /// The name of the database, uses the default database name when null. + /// The server. + /// The login. + /// The password. + /// Indicates whether integrated authentication should be used (when supported by the provider). + /// + /// The generated connection string. + /// + 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 + }); +} diff --git a/src/Umbraco.Infrastructure/Runtime/RuntimeState.cs b/src/Umbraco.Infrastructure/Runtime/RuntimeState.cs index 5ab9993f43..ca41e200f2 100644 --- a/src/Umbraco.Infrastructure/Runtime/RuntimeState.cs +++ b/src/Umbraco.Infrastructure/Runtime/RuntimeState.cs @@ -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 /// public class RuntimeState : IRuntimeState { - internal const string PendingPacakgeMigrationsStateKey = "PendingPackageMigrations"; + internal const string PendingPackageMigrationsStateKey = "PendingPackageMigrations"; + private readonly IOptions _globalSettings = null!; private readonly IOptions _unattendedSettings = null!; private readonly IUmbracoVersion _umbracoVersion = null!; @@ -33,6 +30,7 @@ namespace Umbraco.Cms.Infrastructure.Runtime private readonly PendingPackageMigrations _packageMigrationState = null!; private readonly Dictionary _startupState = new Dictionary(); private readonly IConflictingRouteService _conflictingRouteService = null!; + private readonly IEnumerable _databaseProviderMetadata = null!; /// /// The initial @@ -41,17 +39,17 @@ namespace Umbraco.Cms.Infrastructure.Runtime public static RuntimeState Booting() => new RuntimeState() { Level = RuntimeLevel.Boot }; private RuntimeState() - { - } + { } public RuntimeState( - IOptions globalSettings, - IOptions unattendedSettings, - IUmbracoVersion umbracoVersion, - IUmbracoDatabaseFactory databaseFactory, - ILogger logger, - PendingPackageMigrations packageMigrationState, - IConflictingRouteService conflictingRouteService) + IOptions globalSettings, + IOptions unattendedSettings, + IUmbracoVersion umbracoVersion, + IUmbracoDatabaseFactory databaseFactory, + ILogger logger, + PendingPackageMigrations packageMigrationState, + IConflictingRouteService conflictingRouteService, + IEnumerable 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, + IOptions unattendedSettings, + IUmbracoVersion umbracoVersion, + IUmbracoDatabaseFactory databaseFactory, + ILogger logger, + PendingPackageMigrations packageMigrationState, + IConflictingRouteService conflictingRouteService) + : this( + globalSettings, + unattendedSettings, + umbracoVersion, + databaseFactory, + logger, + packageMigrationState, + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetServices()) + { } + /// /// Initializes a new instance of the class. /// @@ -81,8 +100,7 @@ namespace Umbraco.Cms.Infrastructure.Runtime logger, packageMigrationState, StaticServiceProvider.Instance.GetRequiredService()) - { - } + { } /// 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 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; } } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Configuration/Models/ConnectionStringsTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Configuration/Models/ConnectionStringsTests.cs deleted file mode 100644 index 5398dfc7fd..0000000000 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Configuration/Models/ConnectionStringsTests.cs +++ /dev/null @@ -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")); - } - } -} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Configuration/ConfigureConnectionStringsTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Configuration/ConfigureConnectionStringsTests.cs similarity index 95% rename from tests/Umbraco.Tests.UnitTests/Umbraco.Configuration/ConfigureConnectionStringsTests.cs rename to tests/Umbraco.Tests.UnitTests/Umbraco.Core/Configuration/ConfigureConnectionStringsTests.cs index 4e7d6f7f1f..98ec3e01e6 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Configuration/ConfigureConnectionStringsTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Configuration/ConfigureConnectionStringsTests.cs @@ -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().Bind(configuration.GetSection("ConnectionStrings")); + services.AddOptions(); services.AddSingleton, ConfigureConnectionStrings>(); services.AddSingleton(configuration); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/ConfigurationExtensionsTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/ConfigurationExtensionsTests.cs new file mode 100644 index 0000000000..94fbc6fcfa --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/ConfigurationExtensionsTests.cs @@ -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 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 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 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 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 mockedConfig = CreateConfig(ConfiguredConnectionString, ConfiguredProviderName); + + string connectionString = mockedConfig.Object.GetUmbracoConnectionString(out string providerName); + + AssertResults( + ConfiguredConnectionString, + ConfiguredProviderName, + connectionString, + providerName); + } + + private static Mock CreateConfig(string configuredConnectionString, string configuredProviderName = ConnectionStrings.DefaultProviderName) + { + var mockConfigSection = new Mock(); + mockConfigSection + .SetupGet(m => m[It.Is(s => s == Constants.System.UmbracoConnectionName)]) + .Returns(configuredConnectionString); + mockConfigSection + .SetupGet(m => m[It.Is(s => s == $"{Constants.System.UmbracoConnectionName}{ConnectionStrings.ProviderNamePostfix}")]) + .Returns(configuredProviderName); + + var mockedConfig = new Mock(); + mockedConfig + .Setup(a => a.GetSection(It.Is(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); + } + } +}