diff --git a/src/Umbraco.Core/Configuration/Models/RuntimeMode.cs b/src/Umbraco.Core/Configuration/Models/RuntimeMode.cs new file mode 100644 index 0000000000..3f38167af8 --- /dev/null +++ b/src/Umbraco.Core/Configuration/Models/RuntimeMode.cs @@ -0,0 +1,22 @@ +namespace Umbraco.Cms.Core.Configuration.Models; + +/// +/// Represents the configured Umbraco runtime mode. +/// +public enum RuntimeMode +{ + /// + /// The backoffice development mode ensures the runtime is configured for rapidly applying changes within the backoffice. + /// + BackofficeDevelopment = 0, + + /// + /// The development mode ensures the runtime is configured for rapidly applying changes. + /// + Development = 1, + + /// + /// The production mode ensures optimal performance settings are configured and denies any changes that would require recompilations. + /// + Production = 2 +} diff --git a/src/Umbraco.Core/Configuration/Models/RuntimeSettings.cs b/src/Umbraco.Core/Configuration/Models/RuntimeSettings.cs index ac4e51a1c2..7f31c9319b 100644 --- a/src/Umbraco.Core/Configuration/Models/RuntimeSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/RuntimeSettings.cs @@ -1,16 +1,24 @@ // Copyright (c) Umbraco. // See LICENSE for more details. +using System.ComponentModel; + namespace Umbraco.Cms.Core.Configuration.Models; /// -/// Typed configuration options for runtime settings. +/// Typed configuration options for runtime settings. /// [UmbracoOptions(Constants.Configuration.ConfigRuntime)] public class RuntimeSettings { /// - /// Gets or sets a value for the maximum query string length. + /// Gets or sets the runtime mode. + /// + [DefaultValue(RuntimeMode.BackofficeDevelopment)] + public RuntimeMode Mode { get; set; } = RuntimeMode.BackofficeDevelopment; + + /// + /// Gets or sets a value for the maximum query string length. /// public int? MaxQueryStringLength { get; set; } diff --git a/src/Umbraco.Core/Constants-Configuration.cs b/src/Umbraco.Core/Constants-Configuration.cs index 3f7f3188a9..11694fa5c0 100644 --- a/src/Umbraco.Core/Constants-Configuration.cs +++ b/src/Umbraco.Core/Constants-Configuration.cs @@ -5,23 +5,19 @@ public static partial class Constants public static class Configuration { /// - /// Case insensitive prefix for all configurations + /// Case insensitive prefix for all configurations. /// /// - /// ":" is used as marker for nested objects in json. E.g. "Umbraco:CMS:" = {"Umbraco":{"CMS":{....}} + /// ":" is used as marker for nested objects in JSON, e.g. "Umbraco:CMS:" = {"Umbraco":{"CMS":{...}}. /// public const string ConfigPrefix = "Umbraco:CMS:"; - public const string ConfigContentPrefix = ConfigPrefix + "Content:"; public const string ConfigContentNotificationsPrefix = ConfigContentPrefix + "Notifications:"; public const string ConfigCorePrefix = ConfigPrefix + "Core:"; public const string ConfigCustomErrorsPrefix = ConfigPrefix + "CustomErrors:"; public const string ConfigGlobalPrefix = ConfigPrefix + "Global:"; public const string ConfigGlobalId = ConfigGlobalPrefix + "Id"; - - public const string ConfigGlobalDistributedLockingMechanism = - ConfigGlobalPrefix + "DistributedLockingMechanism"; - + public const string ConfigGlobalDistributedLockingMechanism = ConfigGlobalPrefix + "DistributedLockingMechanism"; public const string ConfigHostingPrefix = ConfigPrefix + "Hosting:"; public const string ConfigModelsBuilderPrefix = ConfigPrefix + "ModelsBuilder:"; public const string ConfigSecurityPrefix = ConfigPrefix + "Security:"; @@ -49,6 +45,7 @@ public static partial class Constants public const string ConfigPlugins = ConfigPrefix + "Plugins"; public const string ConfigRequestHandler = ConfigPrefix + "RequestHandler"; public const string ConfigRuntime = ConfigPrefix + "Runtime"; + public const string ConfigRuntimeMode = ConfigRuntime + ":Mode"; public const string ConfigRuntimeMinification = ConfigPrefix + "RuntimeMinification"; public const string ConfigRuntimeMinificationVersion = ConfigRuntimeMinification + ":Version"; public const string ConfigSecurity = ConfigPrefix + "Security"; @@ -62,7 +59,7 @@ public static partial class Constants public const string ConfigContentDashboard = ConfigPrefix + "ContentDashboard"; public const string ConfigHelpPage = ConfigPrefix + "HelpPage"; public const string ConfigInstallDefaultData = ConfigPrefix + "InstallDefaultData"; - public const string ConfigDataTypes = ConfigPrefix + "DataTypes"; + public const string ConfigDataTypes = ConfigPrefix + "DataTypes"; public static class NamedOptions { diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs index 6ac06ae1b7..fd7923eec7 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs @@ -187,9 +187,6 @@ namespace Umbraco.Cms.Core.DependencyInjection Services.AddSingleton(); - // by default, register a noop factory - Services.AddUnique(); - Services.AddUnique(); Services.AddSingleton(f => f.GetRequiredService().CreateDictionary()); diff --git a/src/Umbraco.Core/Extensions/ConfigurationExtensions.cs b/src/Umbraco.Core/Extensions/ConfigurationExtensions.cs index e5428fb4c4..2003079736 100644 --- a/src/Umbraco.Core/Extensions/ConfigurationExtensions.cs +++ b/src/Umbraco.Core/Extensions/ConfigurationExtensions.cs @@ -1,10 +1,11 @@ using Microsoft.Extensions.Configuration; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration.Models; namespace Umbraco.Extensions; /// -/// Extension methods for configuration. +/// Extensions for . /// public static class ConfigurationExtensions { @@ -90,4 +91,14 @@ public static class ConfigurationExtensions return connectionString; } + + /// + /// Gets the Umbraco runtime mode. + /// + /// The configuration. + /// + /// The Umbraco runtime mode. + /// + public static RuntimeMode GetRuntimeMode(this IConfiguration configuration) + => configuration.GetValue(Constants.Configuration.ConfigRuntimeMode); } diff --git a/src/Umbraco.Core/Configuration/Extensions/HealthCheckSettingsExtensions.cs b/src/Umbraco.Core/Extensions/HealthCheckSettingsExtensions.cs similarity index 100% rename from src/Umbraco.Core/Configuration/Extensions/HealthCheckSettingsExtensions.cs rename to src/Umbraco.Core/Extensions/HealthCheckSettingsExtensions.cs diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs index 94134ab0d1..1c874b2efa 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs @@ -51,6 +51,7 @@ using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Mappers; using Umbraco.Cms.Infrastructure.Runtime; +using Umbraco.Cms.Infrastructure.Runtime.RuntimeModeValidators; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Cms.Infrastructure.Search; using Umbraco.Cms.Infrastructure.Serialization; @@ -63,7 +64,7 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection; public static partial class UmbracoBuilderExtensions { /// - /// Adds all core Umbraco services required to run which may be replaced later in the pipeline + /// Adds all core Umbraco services required to run which may be replaced later in the pipeline. /// public static IUmbracoBuilder AddCoreInitialServices(this IUmbracoBuilder builder) { @@ -83,6 +84,14 @@ public static partial class UmbracoBuilderExtensions builder.AddNotificationAsyncHandler(); builder.AddNotificationAsyncHandler(); + // Add runtime mode validation + builder.Services.AddSingleton(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + // composers builder .AddRepositories() @@ -102,11 +111,9 @@ public static partial class UmbracoBuilderExtensions builder.Services.AddSingleton(f => f.GetRequiredService()); builder.Services.AddSingleton(f => f.GetRequiredService()); - builder.Services.AddSingleton(); - - builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(f => f.GetRequiredService()); - builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddScoped(); diff --git a/src/Umbraco.Infrastructure/Runtime/IRuntimeModeValidationService.cs b/src/Umbraco.Infrastructure/Runtime/IRuntimeModeValidationService.cs new file mode 100644 index 0000000000..3741c4065d --- /dev/null +++ b/src/Umbraco.Infrastructure/Runtime/IRuntimeModeValidationService.cs @@ -0,0 +1,18 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Umbraco.Cms.Infrastructure.Runtime; + +/// +/// Provides a service to validate configuration based on the runtime mode. +/// +public interface IRuntimeModeValidationService +{ + /// + /// Validates configuration based on the runtime mode. + /// + /// The validation error message. + /// + /// true when the validation passes; otherwise, false. + /// + bool Validate([NotNullWhen(false)] out string? validationErrorMessage); +} diff --git a/src/Umbraco.Infrastructure/Runtime/IRuntimeModeValidator.cs b/src/Umbraco.Infrastructure/Runtime/IRuntimeModeValidator.cs new file mode 100644 index 0000000000..dcfc39ed83 --- /dev/null +++ b/src/Umbraco.Infrastructure/Runtime/IRuntimeModeValidator.cs @@ -0,0 +1,20 @@ +using System.Diagnostics.CodeAnalysis; +using Umbraco.Cms.Core.Configuration.Models; + +namespace Umbraco.Cms.Infrastructure.Runtime; + +/// +/// Validates configuration based on the runtime mode. +/// +public interface IRuntimeModeValidator +{ + /// + /// Validates configuration based on the specified . + /// + /// The runtime mode. + /// The validation error message. + /// + /// true when the validation passes; otherwise, false. + /// + bool Validate(RuntimeMode runtimeMode, [NotNullWhen(false)] out string? validationErrorMessage); +} diff --git a/src/Umbraco.Infrastructure/Runtime/RuntimeModeValidationService.cs b/src/Umbraco.Infrastructure/Runtime/RuntimeModeValidationService.cs new file mode 100644 index 0000000000..85eec91786 --- /dev/null +++ b/src/Umbraco.Infrastructure/Runtime/RuntimeModeValidationService.cs @@ -0,0 +1,49 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; + +namespace Umbraco.Cms.Infrastructure.Runtime; + +/// +internal class RuntimeModeValidationService : IRuntimeModeValidationService +{ + private readonly IOptionsMonitor _runtimeSettings; + private readonly IServiceProvider _serviceProvider; + + /// + /// Initializes a new instance of the class. + /// + /// The runtime settings. + /// The service provider. + public RuntimeModeValidationService(IOptionsMonitor runtimeSettings, IServiceProvider serviceProvider) + { + _runtimeSettings = runtimeSettings; + _serviceProvider = serviceProvider; + } + + /// + public bool Validate([NotNullWhen(false)] out string? validationErrorMessage) + { + var runtimeMode = _runtimeSettings.CurrentValue.Mode; + var validationMessages = new List(); + + // Runtime mode validators are registered transient, but this service is registered as singleton + foreach (var runtimeModeValidator in _serviceProvider.GetServices()) + { + if (runtimeModeValidator.Validate(runtimeMode, out var validationMessage) == false) + { + validationMessages.Add(validationMessage); + } + } + + if (validationMessages.Count > 0) + { + validationErrorMessage = $"Runtime mode validation failed for {runtimeMode}:\n" + string.Join("\n", validationMessages); + return false; + } + + validationErrorMessage = null; + return true; + } +} diff --git a/src/Umbraco.Infrastructure/Runtime/RuntimeModeValidators/JITOptimizerValidator.cs b/src/Umbraco.Infrastructure/Runtime/RuntimeModeValidators/JITOptimizerValidator.cs new file mode 100644 index 0000000000..d075001ecd --- /dev/null +++ b/src/Umbraco.Infrastructure/Runtime/RuntimeModeValidators/JITOptimizerValidator.cs @@ -0,0 +1,29 @@ +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; + +namespace Umbraco.Cms.Infrastructure.Runtime.RuntimeModeValidators; + +/// +/// Validates whether the JIT/runtime optimizer of the entry assembly is enabled in production runtime mode. +/// +/// +/// This can be ensured by building the application using the Release configuration. +/// +/// +public class JITOptimizerValidator : RuntimeModeProductionValidatorBase +{ + /// + protected override bool Validate([NotNullWhen(false)] out string? validationErrorMessage) + { + DebuggableAttribute? debuggableAttribute = Assembly.GetEntryAssembly()?.GetCustomAttribute(); + if (debuggableAttribute != null && debuggableAttribute.IsJITOptimizerDisabled) + { + validationErrorMessage = "The JIT/runtime optimizer of the entry assembly needs to be enabled in production mode."; + return false; + } + + validationErrorMessage = null; + return true; + } +} diff --git a/src/Umbraco.Infrastructure/Runtime/RuntimeModeValidators/ModelsBuilderModeValidator.cs b/src/Umbraco.Infrastructure/Runtime/RuntimeModeValidators/ModelsBuilderModeValidator.cs new file mode 100644 index 0000000000..06f7735d60 --- /dev/null +++ b/src/Umbraco.Infrastructure/Runtime/RuntimeModeValidators/ModelsBuilderModeValidator.cs @@ -0,0 +1,43 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration; +using Umbraco.Cms.Core.Configuration.Models; + +namespace Umbraco.Cms.Infrastructure.Runtime.RuntimeModeValidators; + +/// +/// Validates whether the ModelsBuilder mode is not set to when in development runtime mode and set to when in production runtime mode. +/// +/// +public class ModelsBuilderModeValidator : IRuntimeModeValidator +{ + private readonly IOptionsMonitor _modelsBuilderSettings; + + /// + /// Initializes a new instance of the class. + /// + /// The models builder settings. + public ModelsBuilderModeValidator(IOptionsMonitor modelsBuilderSettings) + => _modelsBuilderSettings = modelsBuilderSettings; + + /// + public bool Validate(RuntimeMode runtimeMode, [NotNullWhen(false)] out string? validationErrorMessage) + { + ModelsMode modelsMode = _modelsBuilderSettings.CurrentValue.ModelsMode; + + if (runtimeMode == RuntimeMode.Development && modelsMode == ModelsMode.InMemoryAuto) + { + validationErrorMessage = "ModelsBuilder mode cannot be set to InMemoryAuto in development mode."; + return false; + } + + if (runtimeMode == RuntimeMode.Production && modelsMode != ModelsMode.Nothing) + { + validationErrorMessage = "ModelsBuilder mode needs to be set to Nothing in production mode."; + return false; + } + + validationErrorMessage = null; + return true; + } +} diff --git a/src/Umbraco.Infrastructure/Runtime/RuntimeModeValidators/RuntimeMinificationValidator.cs b/src/Umbraco.Infrastructure/Runtime/RuntimeModeValidators/RuntimeMinificationValidator.cs new file mode 100644 index 0000000000..01bc0dd3dc --- /dev/null +++ b/src/Umbraco.Infrastructure/Runtime/RuntimeModeValidators/RuntimeMinificationValidator.cs @@ -0,0 +1,34 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; + +namespace Umbraco.Cms.Infrastructure.Runtime.RuntimeModeValidators; + +/// +/// Validates whether the runtime minification cache buster is not set to when in production runtime mode. +/// +/// +public class RuntimeMinificationValidator : RuntimeModeProductionValidatorBase +{ + private readonly IOptionsMonitor _runtimeMinificationSettings; + + /// + /// Initializes a new instance of the class. + /// + /// The runtime minification settings. + public RuntimeMinificationValidator(IOptionsMonitor runtimeMinificationSettings) + => _runtimeMinificationSettings = runtimeMinificationSettings; + + /// + protected override bool Validate([NotNullWhen(false)] out string? validationErrorMessage) + { + if (_runtimeMinificationSettings.CurrentValue.CacheBuster == RuntimeMinificationCacheBuster.Timestamp) + { + validationErrorMessage = "Runtime minification setting needs to be set to a fixed cache buster (like Version or AppDomain) in production mode."; + return false; + } + + validationErrorMessage = null; + return true; + } +} diff --git a/src/Umbraco.Infrastructure/Runtime/RuntimeModeValidators/RuntimeModeProductionValidatorBase.cs b/src/Umbraco.Infrastructure/Runtime/RuntimeModeValidators/RuntimeModeProductionValidatorBase.cs new file mode 100644 index 0000000000..7d23c0138b --- /dev/null +++ b/src/Umbraco.Infrastructure/Runtime/RuntimeModeValidators/RuntimeModeProductionValidatorBase.cs @@ -0,0 +1,32 @@ +using System.Diagnostics.CodeAnalysis; +using Umbraco.Cms.Core.Configuration.Models; + +namespace Umbraco.Cms.Infrastructure.Runtime.RuntimeModeValidators; + +/// +/// Validates configuration based on the production runtime mode. +/// +/// +public abstract class RuntimeModeProductionValidatorBase : IRuntimeModeValidator +{ + /// + public bool Validate(RuntimeMode runtimeMode, [NotNullWhen(false)] out string? validationErrorMessage) + { + if (runtimeMode == RuntimeMode.Production) + { + return Validate(out validationErrorMessage); + } + + validationErrorMessage = null; + return true; + } + + /// + /// Validates configuration based on the production runtime mode. + /// + /// The validation error message. + /// + /// true when the validation passes; otherwise, false. + /// + protected abstract bool Validate([NotNullWhen(false)] out string? validationErrorMessage); +} diff --git a/src/Umbraco.Infrastructure/Runtime/RuntimeModeValidators/UmbracoApplicationUrlValidator.cs b/src/Umbraco.Infrastructure/Runtime/RuntimeModeValidators/UmbracoApplicationUrlValidator.cs new file mode 100644 index 0000000000..7d990dda5d --- /dev/null +++ b/src/Umbraco.Infrastructure/Runtime/RuntimeModeValidators/UmbracoApplicationUrlValidator.cs @@ -0,0 +1,34 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; + +namespace Umbraco.Cms.Infrastructure.Runtime.RuntimeModeValidators; + +/// +/// Validates whether a fixed Umbraco application URL is set when in production runtime mode. +/// +/// +public class UmbracoApplicationUrlValidator : RuntimeModeProductionValidatorBase +{ + private readonly IOptionsMonitor _webRoutingSettings; + + /// + /// Initializes a new instance of the class. + /// + /// The web routing settings. + public UmbracoApplicationUrlValidator(IOptionsMonitor webRoutingSettings) + => _webRoutingSettings = webRoutingSettings; + + /// + protected override bool Validate([NotNullWhen(false)] out string? validationErrorMessage) + { + if (string.IsNullOrWhiteSpace(_webRoutingSettings.CurrentValue.UmbracoApplicationUrl)) + { + validationErrorMessage = "Umbraco application URL needs to be set in production mode."; + return false; + } + + validationErrorMessage = null; + return true; + } +} diff --git a/src/Umbraco.Infrastructure/Runtime/RuntimeModeValidators/UseHttpsValidator.cs b/src/Umbraco.Infrastructure/Runtime/RuntimeModeValidators/UseHttpsValidator.cs new file mode 100644 index 0000000000..1a02581ae6 --- /dev/null +++ b/src/Umbraco.Infrastructure/Runtime/RuntimeModeValidators/UseHttpsValidator.cs @@ -0,0 +1,34 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; + +namespace Umbraco.Cms.Infrastructure.Runtime.RuntimeModeValidators; + +/// +/// Validates whether HTTPS is enforced when in production runtime mode. +/// +/// +public class UseHttpsValidator : RuntimeModeProductionValidatorBase +{ + private readonly IOptionsMonitor _globalSettings; + + /// + /// Initializes a new instance of the class. + /// + /// The global settings. + public UseHttpsValidator(IOptionsMonitor globalSettings) + => _globalSettings = globalSettings; + + /// + protected override bool Validate([NotNullWhen(false)] out string? validationErrorMessage) + { + if (!_globalSettings.CurrentValue.UseHttps) + { + validationErrorMessage = "Using HTTPS should be enforced in production mode."; + return false; + } + + validationErrorMessage = null; + return true; + } +} diff --git a/src/Umbraco.Infrastructure/Runtime/RuntimeState.cs b/src/Umbraco.Infrastructure/Runtime/RuntimeState.cs index 88c5a9389b..74b00d3644 100644 --- a/src/Umbraco.Infrastructure/Runtime/RuntimeState.cs +++ b/src/Umbraco.Infrastructure/Runtime/RuntimeState.cs @@ -12,151 +12,200 @@ using Umbraco.Cms.Infrastructure.Migrations.Upgrade; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Cms.Infrastructure.Runtime +namespace Umbraco.Cms.Infrastructure.Runtime; + +/// +/// Represents the state of the Umbraco runtime. +/// +public class RuntimeState : IRuntimeState { + internal const string PendingPackageMigrationsStateKey = "PendingPackageMigrations"; + + private readonly IOptions _globalSettings = null!; + private readonly IOptions _unattendedSettings = null!; + private readonly IUmbracoVersion _umbracoVersion = null!; + private readonly IUmbracoDatabaseFactory _databaseFactory = null!; + private readonly ILogger _logger = null!; + private readonly PendingPackageMigrations _packageMigrationState = null!; + private readonly Dictionary _startupState = new Dictionary(); + private readonly IConflictingRouteService _conflictingRouteService = null!; + private readonly IEnumerable _databaseProviderMetadata = null!; + private readonly IRuntimeModeValidationService _runtimeModeValidationService = null!; /// - /// Represents the state of the Umbraco runtime. + /// The initial + /// The initial /// - public class RuntimeState : IRuntimeState + public static RuntimeState Booting() => new RuntimeState() { Level = RuntimeLevel.Boot }; + + /// + /// Initializes a new instance of the class. + /// + private RuntimeState() + { } + + /// + /// Initializes a new instance of the class. + /// + public RuntimeState( + IOptions globalSettings, + IOptions unattendedSettings, + IUmbracoVersion umbracoVersion, + IUmbracoDatabaseFactory databaseFactory, + ILogger logger, + PendingPackageMigrations packageMigrationState, + IConflictingRouteService conflictingRouteService, + IEnumerable databaseProviderMetadata, + IRuntimeModeValidationService runtimeModeValidationService) { - internal const string PendingPackageMigrationsStateKey = "PendingPackageMigrations"; + _globalSettings = globalSettings; + _unattendedSettings = unattendedSettings; + _umbracoVersion = umbracoVersion; + _databaseFactory = databaseFactory; + _logger = logger; + _packageMigrationState = packageMigrationState; + _conflictingRouteService = conflictingRouteService; + _databaseProviderMetadata = databaseProviderMetadata; + _runtimeModeValidationService = runtimeModeValidationService; + } - private readonly IOptions _globalSettings = null!; - private readonly IOptions _unattendedSettings = null!; - private readonly IUmbracoVersion _umbracoVersion = null!; - private readonly IUmbracoDatabaseFactory _databaseFactory = null!; - private readonly ILogger _logger = null!; - private readonly PendingPackageMigrations _packageMigrationState = null!; - private readonly Dictionary _startupState = new Dictionary(); - private readonly IConflictingRouteService _conflictingRouteService = null!; - private readonly IEnumerable _databaseProviderMetadata = null!; + /// + /// Initializes a new instance of the class. + /// + [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, + IEnumerable databaseProviderMetadata) + : this( + globalSettings, + unattendedSettings, + umbracoVersion, + databaseFactory, + logger, + packageMigrationState, + conflictingRouteService, + databaseProviderMetadata, + StaticServiceProvider.Instance.GetRequiredService()) + { } - /// - /// The initial - /// The initial - /// - public static RuntimeState Booting() => new RuntimeState() { Level = RuntimeLevel.Boot }; + [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()) + { } - private RuntimeState() - { } + /// + /// Initializes a new instance of the class. + /// + [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) + : this( + globalSettings, + unattendedSettings, + umbracoVersion, + databaseFactory, + logger, + packageMigrationState, + StaticServiceProvider.Instance.GetRequiredService()) + { } - public RuntimeState( - IOptions globalSettings, - IOptions unattendedSettings, - IUmbracoVersion umbracoVersion, - IUmbracoDatabaseFactory databaseFactory, - ILogger logger, - PendingPackageMigrations packageMigrationState, - IConflictingRouteService conflictingRouteService, - IEnumerable databaseProviderMetadata) + /// + public Version Version => _umbracoVersion.Version; + + /// + public string VersionComment => _umbracoVersion.Comment; + + /// + public SemVersion SemanticVersion => _umbracoVersion.SemanticVersion; + + /// + public string? CurrentMigrationState { get; private set; } + + /// + public string? FinalMigrationState { get; private set; } + + /// + public RuntimeLevel Level { get; internal set; } = RuntimeLevel.Unknown; + + /// + public RuntimeLevelReason Reason { get; internal set; } = RuntimeLevelReason.Unknown; + + /// + public BootFailedException? BootFailedException { get; internal set; } + + /// + public IReadOnlyDictionary StartupState => _startupState; + + /// + public void DetermineRuntimeLevel() + { + if (_databaseFactory.Configured == false) { - _globalSettings = globalSettings; - _unattendedSettings = unattendedSettings; - _umbracoVersion = umbracoVersion; - _databaseFactory = databaseFactory; - _logger = logger; - _packageMigrationState = packageMigrationState; - _conflictingRouteService = conflictingRouteService; - _databaseProviderMetadata = databaseProviderMetadata; + // local version *does* match code version, but the database is not configured + // install - may happen with Deploy/Cloud/etc + _logger.LogDebug("Database is not configured, need to install Umbraco."); + + Level = RuntimeLevel.Install; + Reason = RuntimeLevelReason.InstallNoDatabase; + + return; } - [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. - /// - [Obsolete("use ctor with all params")] - public RuntimeState( - IOptions globalSettings, - IOptions unattendedSettings, - IUmbracoVersion umbracoVersion, - IUmbracoDatabaseFactory databaseFactory, - ILogger logger, - PendingPackageMigrations packageMigrationState) - : this( - globalSettings, - unattendedSettings, - umbracoVersion, - databaseFactory, - logger, - packageMigrationState, - StaticServiceProvider.Instance.GetRequiredService()) - { } - - /// - public Version Version => _umbracoVersion.Version; - - /// - public string VersionComment => _umbracoVersion.Comment; - - /// - public SemVersion SemanticVersion => _umbracoVersion.SemanticVersion; - - /// - public string? CurrentMigrationState { get; private set; } - - /// - public string? FinalMigrationState { get; private set; } - - /// - public RuntimeLevel Level { get; internal set; } = RuntimeLevel.Unknown; - - /// - public RuntimeLevelReason Reason { get; internal set; } = RuntimeLevelReason.Unknown; - - /// - public BootFailedException? BootFailedException { get; internal set; } - - /// - public IReadOnlyDictionary StartupState => _startupState; - - /// - public void DetermineRuntimeLevel() + // Validate runtime mode + if (_runtimeModeValidationService.Validate(out var validationErrorMessage) == false) { - if (_databaseFactory.Configured == false) - { - // local version *does* match code version, but the database is not configured - // install - may happen with Deploy/Cloud/etc - _logger.LogDebug("Database is not configured, need to install Umbraco."); - Level = RuntimeLevel.Install; - Reason = RuntimeLevelReason.InstallNoDatabase; - return; - } + _logger.LogError(validationErrorMessage); - // Check if we have multiple controllers with the same name. - if (_conflictingRouteService.HasConflictingRoutes(out string controllerName)) - { - Level = RuntimeLevel.BootFailed; - Reason = RuntimeLevelReason.BootFailedOnException; - BootFailedException = new BootFailedException($"Conflicting routes, you cannot have multiple controllers with the same name: {controllerName}"); + Level = RuntimeLevel.BootFailed; + Reason = RuntimeLevelReason.BootFailedOnException; + BootFailedException = new BootFailedException(validationErrorMessage); - return; - } + return; + } - // Check the database state, whether we can connect or if it's in an upgrade or empty state, etc... + // Check if we have multiple controllers with the same name. + if (_conflictingRouteService.HasConflictingRoutes(out string controllerName)) + { + var message = $"Conflicting routes, you cannot have multiple controllers with the same name: {controllerName}"; + _logger.LogError(message); - switch (GetUmbracoDatabaseState(_databaseFactory)) - { - case UmbracoDatabaseState.CannotConnect: + Level = RuntimeLevel.BootFailed; + Reason = RuntimeLevelReason.BootFailedOnException; + BootFailedException = new BootFailedException(message); + + return; + } + + // Check the database state, whether we can connect or if it's in an upgrade or empty state, etc... + switch (GetUmbracoDatabaseState(_databaseFactory)) + { + case UmbracoDatabaseState.CannotConnect: { // cannot connect to configured database, this is bad, fail _logger.LogDebug("Could not connect to database."); @@ -174,14 +223,14 @@ namespace Umbraco.Cms.Infrastructure.Runtime BootFailedException = new BootFailedException("A connection string is configured but Umbraco could not connect to the database."); throw BootFailedException; } - case UmbracoDatabaseState.NotInstalled: + case UmbracoDatabaseState.NotInstalled: { // ok to install on an empty database Level = RuntimeLevel.Install; Reason = RuntimeLevelReason.InstallEmptyDatabase; return; } - case UmbracoDatabaseState.NeedsUpgrade: + case UmbracoDatabaseState.NeedsUpgrade: { // the db version does not match... but we do have a migration table // so, at least one valid table, so we quite probably are installed & need to upgrade @@ -193,26 +242,26 @@ namespace Umbraco.Cms.Infrastructure.Runtime Reason = RuntimeLevelReason.UpgradeMigrations; } break; - case UmbracoDatabaseState.NeedsPackageMigration: + case UmbracoDatabaseState.NeedsPackageMigration: - // no matter what the level is run for package migrations. - // they either run unattended, or only manually via the back office. - Level = RuntimeLevel.Run; + // no matter what the level is run for package migrations. + // they either run unattended, or only manually via the back office. + Level = RuntimeLevel.Run; - if (_unattendedSettings.Value.PackageMigrationsUnattended) - { - _logger.LogDebug("Package migrations need to execute."); - Reason = RuntimeLevelReason.UpgradePackageMigrations; - } - else - { - _logger.LogInformation("Package migrations need to execute but unattended package migrations is disabled. They will need to be run from the back office."); - Reason = RuntimeLevelReason.Run; - } + if (_unattendedSettings.Value.PackageMigrationsUnattended) + { + _logger.LogDebug("Package migrations need to execute."); + Reason = RuntimeLevelReason.UpgradePackageMigrations; + } + else + { + _logger.LogInformation("Package migrations need to execute but unattended package migrations is disabled. They will need to be run from the back office."); + Reason = RuntimeLevelReason.Run; + } - break; - case UmbracoDatabaseState.Ok: - default: + break; + case UmbracoDatabaseState.Ok: + default: { @@ -221,116 +270,115 @@ namespace Umbraco.Cms.Infrastructure.Runtime Reason = RuntimeLevelReason.Run; } break; - } - } - - 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, - CannotConnect, - NotInstalled, - NeedsUpgrade, - NeedsPackageMigration - } - - private UmbracoDatabaseState GetUmbracoDatabaseState(IUmbracoDatabaseFactory databaseFactory) - { - try - { - if (!TryDbConnect(databaseFactory)) - { - return UmbracoDatabaseState.CannotConnect; - } - - // no scope, no service - just directly accessing the database - using (IUmbracoDatabase database = databaseFactory.CreateDatabase()) - { - if (!database.IsUmbracoInstalled()) - { - return UmbracoDatabaseState.NotInstalled; - } - - // Make ONE SQL call to determine Umbraco upgrade vs package migrations state. - // All will be prefixed with the same key. - IReadOnlyDictionary? keyValues = database.GetFromKeyValueTable(Constants.Conventions.Migrations.KeyValuePrefix); - - // This could need both an upgrade AND package migrations to execute but - // we will process them one at a time, first the upgrade, then the package migrations. - if (DoesUmbracoRequireUpgrade(keyValues)) - { - return UmbracoDatabaseState.NeedsUpgrade; - } - - IReadOnlyList packagesRequiringMigration = _packageMigrationState.GetPendingPackageMigrations(keyValues); - if (packagesRequiringMigration.Count > 0) - { - _startupState[PendingPackageMigrationsStateKey] = packagesRequiringMigration; - - return UmbracoDatabaseState.NeedsPackageMigration; - } - } - - return UmbracoDatabaseState.Ok; - } - catch (Exception e) - { - // can connect to the database so cannot check the upgrade state... oops - _logger.LogWarning(e, "Could not check the upgrade state."); - - // else it is bad enough that we want to throw - Reason = RuntimeLevelReason.BootFailedCannotCheckUpgradeState; - BootFailedException = new BootFailedException("Could not check the upgrade state.", e); - throw BootFailedException; - } - } - - private bool DoesUmbracoRequireUpgrade(IReadOnlyDictionary? keyValues) - { - var upgrader = new Upgrader(new UmbracoPlan(_umbracoVersion)); - var stateValueKey = upgrader.StateValueKey; - - if(keyValues?.TryGetValue(stateValueKey, out var value) ?? false) - { - CurrentMigrationState = value; - } - - FinalMigrationState = upgrader.Plan.FinalState; - - _logger.LogDebug("Final upgrade state is {FinalMigrationState}, database contains {DatabaseState}", FinalMigrationState, CurrentMigrationState ?? ""); - - return CurrentMigrationState != FinalMigrationState; - } - - private bool TryDbConnect(IUmbracoDatabaseFactory databaseFactory) - { - // anything other than install wants a database - see if we can connect - // (since this is an already existing database, assume localdb is ready) - bool canConnect; - var tries = _globalSettings.Value.InstallMissingDatabase ? 2 : 5; - for (var i = 0; ;) - { - canConnect = databaseFactory.CanConnect; - if (canConnect || ++i == tries) - { - break; - } - - _logger.LogDebug("Could not immediately connect to database, trying again."); - Thread.Sleep(1000); - } - - return canConnect; } } + + 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, + CannotConnect, + NotInstalled, + NeedsUpgrade, + NeedsPackageMigration + } + + private UmbracoDatabaseState GetUmbracoDatabaseState(IUmbracoDatabaseFactory databaseFactory) + { + try + { + if (!TryDbConnect(databaseFactory)) + { + return UmbracoDatabaseState.CannotConnect; + } + + // no scope, no service - just directly accessing the database + using (IUmbracoDatabase database = databaseFactory.CreateDatabase()) + { + if (!database.IsUmbracoInstalled()) + { + return UmbracoDatabaseState.NotInstalled; + } + + // Make ONE SQL call to determine Umbraco upgrade vs package migrations state. + // All will be prefixed with the same key. + IReadOnlyDictionary? keyValues = database.GetFromKeyValueTable(Constants.Conventions.Migrations.KeyValuePrefix); + + // This could need both an upgrade AND package migrations to execute but + // we will process them one at a time, first the upgrade, then the package migrations. + if (DoesUmbracoRequireUpgrade(keyValues)) + { + return UmbracoDatabaseState.NeedsUpgrade; + } + + IReadOnlyList packagesRequiringMigration = _packageMigrationState.GetPendingPackageMigrations(keyValues); + if (packagesRequiringMigration.Count > 0) + { + _startupState[PendingPackageMigrationsStateKey] = packagesRequiringMigration; + + return UmbracoDatabaseState.NeedsPackageMigration; + } + } + + return UmbracoDatabaseState.Ok; + } + catch (Exception e) + { + // can connect to the database so cannot check the upgrade state... oops + _logger.LogWarning(e, "Could not check the upgrade state."); + + // else it is bad enough that we want to throw + Reason = RuntimeLevelReason.BootFailedCannotCheckUpgradeState; + BootFailedException = new BootFailedException("Could not check the upgrade state.", e); + throw BootFailedException; + } + } + + private bool DoesUmbracoRequireUpgrade(IReadOnlyDictionary? keyValues) + { + var upgrader = new Upgrader(new UmbracoPlan(_umbracoVersion)); + var stateValueKey = upgrader.StateValueKey; + + if (keyValues?.TryGetValue(stateValueKey, out var value) ?? false) + { + CurrentMigrationState = value; + } + + FinalMigrationState = upgrader.Plan.FinalState; + + _logger.LogDebug("Final upgrade state is {FinalMigrationState}, database contains {DatabaseState}", FinalMigrationState, CurrentMigrationState ?? ""); + + return CurrentMigrationState != FinalMigrationState; + } + + private bool TryDbConnect(IUmbracoDatabaseFactory databaseFactory) + { + // anything other than install wants a database - see if we can connect + // (since this is an already existing database, assume localdb is ready) + bool canConnect; + var tries = _globalSettings.Value.InstallMissingDatabase ? 2 : 5; + for (var i = 0; ;) + { + canConnect = databaseFactory.CanConnect; + if (canConnect || ++i == tries) + { + break; + } + + _logger.LogDebug("Could not immediately connect to database, trying again."); + Thread.Sleep(1000); + } + + return canConnect; + } } diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs index b72de65682..0d8cc49bdf 100644 --- a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs @@ -23,39 +23,38 @@ using Umbraco.Cms.Web.BackOffice.Trees; namespace Umbraco.Extensions; /// -/// Extension methods for for the Umbraco back office +/// Extension methods for for the Umbraco back office /// public static partial class UmbracoBuilderExtensions { /// - /// Adds all required components to run the Umbraco back office + /// Adds all required components to run the Umbraco back office /// - public static IUmbracoBuilder - AddBackOffice(this IUmbracoBuilder builder, Action? configureMvc = null) => builder - .AddConfiguration() - .AddUmbracoCore() - .AddWebComponents() - .AddRuntimeMinifier() - .AddBackOfficeCore() - .AddBackOfficeAuthentication() - .AddBackOfficeIdentity() - .AddMembersIdentity() - .AddBackOfficeAuthorizationPolicies() - .AddUmbracoProfiler() - .AddMvcAndRazor(configureMvc) - .AddWebServer() - .AddPreviewSupport() - .AddHostedServices() - .AddNuCache() - .AddDistributedCache() - .AddModelsBuilderDashboard() - .AddUnattendedInstallInstallCreateUser() - .AddCoreNotifications() - .AddLogViewer() - .AddExamine() - .AddExamineIndexes() - .AddControllersWithAmbiguousConstructors() - .AddSupplemenataryLocalizedTextFileSources(); + public static IUmbracoBuilder AddBackOffice(this IUmbracoBuilder builder, Action? configureMvc = null) => builder + .AddConfiguration() + .AddUmbracoCore() + .AddWebComponents() + .AddRuntimeMinifier() + .AddBackOfficeCore() + .AddBackOfficeAuthentication() + .AddBackOfficeIdentity() + .AddMembersIdentity() + .AddBackOfficeAuthorizationPolicies() + .AddUmbracoProfiler() + .AddMvcAndRazor(configureMvc) + .AddWebServer() + .AddPreviewSupport() + .AddHostedServices() + .AddNuCache() + .AddDistributedCache() + .TryAddModelsBuilderDashboard() + .AddUnattendedInstallInstallCreateUser() + .AddCoreNotifications() + .AddLogViewer() + .AddExamine() + .AddExamineIndexes() + .AddControllersWithAmbiguousConstructors() + .AddSupplemenataryLocalizedTextFileSources(); public static IUmbracoBuilder AddUnattendedInstallInstallCreateUser(this IUmbracoBuilder builder) { diff --git a/src/Umbraco.Web.BackOffice/ModelsBuilder/DisableModelsBuilderNotificationHandler.cs b/src/Umbraco.Web.BackOffice/ModelsBuilder/DisableModelsBuilderNotificationHandler.cs index 970fe7e778..2e11af0d1f 100644 --- a/src/Umbraco.Web.BackOffice/ModelsBuilder/DisableModelsBuilderNotificationHandler.cs +++ b/src/Umbraco.Web.BackOffice/ModelsBuilder/DisableModelsBuilderNotificationHandler.cs @@ -5,7 +5,7 @@ using Umbraco.Cms.Core.Notifications; namespace Umbraco.Cms.Web.BackOffice.ModelsBuilder; /// -/// Used in conjunction with +/// Used in conjunction with /// internal class DisableModelsBuilderNotificationHandler : INotificationHandler { diff --git a/src/Umbraco.Web.BackOffice/ModelsBuilder/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/ModelsBuilder/UmbracoBuilderExtensions.cs index f9bdcd1b77..a0a0aeec8c 100644 --- a/src/Umbraco.Web.BackOffice/ModelsBuilder/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.BackOffice/ModelsBuilder/UmbracoBuilderExtensions.cs @@ -1,30 +1,59 @@ -using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Dashboards; using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Web.Common.Controllers; using Umbraco.Cms.Web.Common.ModelsBuilder; using Umbraco.Extensions; namespace Umbraco.Cms.Web.BackOffice.ModelsBuilder; /// -/// Extension methods for for the common Umbraco functionality +/// Extension methods for for the common Umbraco functionality /// public static class UmbracoBuilderExtensions { /// - /// Adds the ModelsBuilder dashboard. + /// Adds the ModelsBuilder dashboard, but only when not in production mode. /// - public static IUmbracoBuilder AddModelsBuilderDashboard(this IUmbracoBuilder builder) + internal static IUmbracoBuilder TryAddModelsBuilderDashboard(this IUmbracoBuilder builder) { - builder.Services.AddUnique(); + if (builder.Config.GetRuntimeMode() == RuntimeMode.Production) + { + builder.RemoveModelsBuilderDashboard(); + } + else + { + builder.AddModelsBuilderDashboard(); + } + return builder; } /// - /// Can be called if using an external models builder to remove the embedded models builder controller features + /// Adds the ModelsBuilder dashboard (dashboard and API controller are automatically added). /// - public static IUmbracoBuilder DisableModelsBuilderControllers(this IUmbracoBuilder builder) + public static IUmbracoBuilder AddModelsBuilderDashboard(this IUmbracoBuilder builder) { - builder.Services.AddSingleton(); + builder.Services.AddUnique(); + return builder; } + + /// + /// Removes the ModelsBuilder dashboard (and API controller). + /// + public static IUmbracoBuilder RemoveModelsBuilderDashboard(this IUmbracoBuilder builder) + { + builder.Dashboards().Remove(); + builder.WithCollectionBuilder().Remove(); + + return builder; + } + + /// + /// Can be called if using an external models builder to remove the embedded models builder controller features. + /// + public static IUmbracoBuilder DisableModelsBuilderControllers(this IUmbracoBuilder builder) + => builder.AddNotificationHandler(); } diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs index 666342dc10..40b84a0987 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs @@ -224,13 +224,12 @@ public static partial class UmbracoBuilderExtensions { // TODO: We need to figure out if we can work around this because calling AddControllersWithViews modifies the global app and order is very important // this will directly affect developers who need to call that themselves. - // We need to have runtime compilation of views when using umbraco. We could consider having only this when a specific config is set. - // But as far as I can see, there are still precompiled views, even when this is activated, so maybe it is okay. - IMvcBuilder mvcBuilder = builder.Services - .AddControllersWithViews(); + IMvcBuilder mvcBuilder = builder.Services.AddControllersWithViews(); - FixForDotnet6Preview1(builder.Services); - mvcBuilder.AddRazorRuntimeCompilation(); + if (builder.Config.GetRuntimeMode() != RuntimeMode.Production) + { + mvcBuilder.AddRazorRuntimeCompilation(); + } mvcBuilding?.Invoke(mvcBuilder); @@ -421,30 +420,4 @@ public static partial class UmbracoBuilderExtensions wrappedWebRoutingSettings, webHostEnvironment); } - - /// - /// This fixes an issue for .NET6 Preview1, that in AddRazorRuntimeCompilation cannot remove the existing - /// IViewCompilerProvider. - /// - /// - /// When running .NET6 Preview1 there is an issue with looks to be fixed when running ASP.NET Core 6. - /// This issue is because the default implementation of IViewCompilerProvider has changed, so the - /// AddRazorRuntimeCompilation extension can't remove the default and replace with the runtimeviewcompiler. - /// This method basically does the same as the ASP.NET Core 6 version of AddRazorRuntimeCompilation - /// https://github.com/dotnet/aspnetcore/blob/f7dc5e24af7f9692a1db66741954b90b42d84c3a/src/Mvc/Mvc.Razor.RuntimeCompilation/src/DependencyInjection/RazorRuntimeCompilationMvcCoreBuilderExtensions.cs#L71-L80 - /// While running .NET5 this does nothing as the ImplementationType has another FullName, and this is handled by the - /// .NET5 version of AddRazorRuntimeCompilation - /// - private static void FixForDotnet6Preview1(IServiceCollection services) - { - ServiceDescriptor? compilerProvider = services.FirstOrDefault(f => - f.ServiceType == typeof(IViewCompilerProvider) && - f.ImplementationType?.Assembly == typeof(IViewCompilerProvider).Assembly && - f.ImplementationType.FullName == "Microsoft.AspNetCore.Mvc.Razor.Compilation.DefaultViewCompiler"); - - if (compilerProvider != null) - { - services.Remove(compilerProvider); - } - } } diff --git a/src/Umbraco.Web.Common/ModelsBuilder/DependencyInjection/UmbracoBuilderDependencyInjectionExtensions.cs b/src/Umbraco.Web.Common/ModelsBuilder/DependencyInjection/UmbracoBuilderDependencyInjectionExtensions.cs index 7749c4cbc9..95ae91d7b7 100644 --- a/src/Umbraco.Web.Common/ModelsBuilder/DependencyInjection/UmbracoBuilderDependencyInjectionExtensions.cs +++ b/src/Umbraco.Web.Common/ModelsBuilder/DependencyInjection/UmbracoBuilderDependencyInjectionExtensions.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Mvc.Razor; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; @@ -81,8 +82,7 @@ public static class UmbracoBuilderDependencyInjectionExtensions /// public static IUmbracoBuilder AddModelsBuilder(this IUmbracoBuilder builder) { - var umbServices = - new UniqueServiceDescriptor(typeof(UmbracoServices), typeof(UmbracoServices), ServiceLifetime.Singleton); + var umbServices = new UniqueServiceDescriptor(typeof(UmbracoServices), typeof(UmbracoServices), ServiceLifetime.Singleton); if (builder.Services.Contains(umbServices)) { // if this ext method is called more than once just exit @@ -91,44 +91,35 @@ public static class UmbracoBuilderDependencyInjectionExtensions builder.Services.Add(umbServices); - builder.AddInMemoryModelsRazorEngine(); + if (builder.Config.GetRuntimeMode() == RuntimeMode.BackofficeDevelopment) + { + // Configure services to allow InMemoryAuto mode + builder.AddInMemoryModelsRazorEngine(); - // TODO: I feel like we could just do builder.AddNotificationHandler() and it - // would automatically just register for all implemented INotificationHandler{T}? - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + } + if (builder.Config.GetRuntimeMode() != RuntimeMode.Production) + { + // Configure service to allow models generation + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + } + + builder.Services.TryAddSingleton(); + + // Register required services for ModelsBuilderDashboardController builder.Services.AddSingleton(); builder.Services.AddSingleton(); - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - - // This is what the community MB would replace, all of the above services are fine to be registered - // even if the community MB is in place. - builder.Services.AddSingleton(factory => - { - ModelsBuilderSettings config = factory.GetRequiredService>().Value; - if (config.ModelsMode == ModelsMode.InMemoryAuto) - { - return factory.GetRequiredService(); - } - - return factory.CreateDefaultPublishedModelFactory(); - }); - - if (!builder.Services.Any(x => x.ServiceType == typeof(IModelsBuilderDashboardProvider))) - { - builder.Services.AddUnique(); - } - return builder; } @@ -152,6 +143,23 @@ public static class UmbracoBuilderDependencyInjectionExtensions }, s.GetRequiredService())); + builder.Services.AddSingleton(); + + // This is what the community MB would replace, all of the above services are fine to be registered + // even if the community MB is in place. + builder.Services.AddSingleton(factory => + { + ModelsBuilderSettings modelsBuilderSettings = factory.GetRequiredService>().Value; + if (modelsBuilderSettings.ModelsMode == ModelsMode.InMemoryAuto) + { + return factory.GetRequiredService(); + } + else + { + return factory.CreateDefaultPublishedModelFactory(); + } + }); + return builder; } } diff --git a/src/Umbraco.Web.Common/ModelsBuilder/NoopModelsBuilderDashboardProvider.cs b/src/Umbraco.Web.Common/ModelsBuilder/NoopModelsBuilderDashboardProvider.cs index 1be67c575c..169f6af0f5 100644 --- a/src/Umbraco.Web.Common/ModelsBuilder/NoopModelsBuilderDashboardProvider.cs +++ b/src/Umbraco.Web.Common/ModelsBuilder/NoopModelsBuilderDashboardProvider.cs @@ -2,5 +2,5 @@ namespace Umbraco.Cms.Web.Common.ModelsBuilder; public class NoopModelsBuilderDashboardProvider : IModelsBuilderDashboardProvider { - public string GetUrl() => string.Empty; + public string GetUrl() => null!; } diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 42e894a7f3..5f067d8a9d 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -1,75 +1,57 @@ - - + net6.0 Umbraco.Cms.Web.UI - - bin/Release/Umbraco.Web.UI.xml - - - - true - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - + + + + + + + + + false false - false + true $(ProjectDir)appsettings-schema.json $(ProjectDir)../JsonSchema/JsonSchema.csproj - + - - - - - - + - + - - + + - + - diff --git a/templates/UmbracoProject/UmbracoProject.csproj b/templates/UmbracoProject/UmbracoProject.csproj index efaa0edbb0..574ff04452 100644 --- a/templates/UmbracoProject/UmbracoProject.csproj +++ b/templates/UmbracoProject/UmbracoProject.csproj @@ -21,17 +21,14 @@ - - true - - - + false false + true