From 4f6e491c5f4599911bc122638c9e0388203b1cb3 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 8 Jun 2021 10:07:57 -0600 Subject: [PATCH 01/11] Refactor MigrationPlan to separate the executor from the plan itself. --- .../Migrations/IMigrationPlanExecutor.cs | 7 + .../Migrations/MergeBuilder.cs | 5 +- .../Migrations/MigrationPlan.cs | 142 +++++------------- .../Packaging/PackageMigrationPlan.cs | 14 ++ .../UmbracoBuilder.CoreServices.cs | 2 + .../Migrations/Install/DatabaseBuilder.cs | 9 +- .../Migrations/MigrationPlanExecutor.cs | 105 +++++++++++++ .../Migrations/Upgrade/UmbracoPlan.cs | 3 +- .../Migrations/Upgrade/Upgrader.cs | 36 ++--- src/Umbraco.Infrastructure/RuntimeState.cs | 16 -- .../Migrations/AdvancedMigrationTests.cs | 12 +- .../Migrations/MigrationPlanTests.cs | 6 +- .../Migrations/PostMigrationTests.cs | 18 +-- 13 files changed, 207 insertions(+), 168 deletions(-) create mode 100644 src/Umbraco.Core/Migrations/IMigrationPlanExecutor.cs rename src/{Umbraco.Infrastructure => Umbraco.Core}/Migrations/MergeBuilder.cs (96%) rename src/{Umbraco.Infrastructure => Umbraco.Core}/Migrations/MigrationPlan.cs (68%) create mode 100644 src/Umbraco.Core/Packaging/PackageMigrationPlan.cs create mode 100644 src/Umbraco.Infrastructure/Migrations/MigrationPlanExecutor.cs diff --git a/src/Umbraco.Core/Migrations/IMigrationPlanExecutor.cs b/src/Umbraco.Core/Migrations/IMigrationPlanExecutor.cs new file mode 100644 index 0000000000..0ba8bc1ecd --- /dev/null +++ b/src/Umbraco.Core/Migrations/IMigrationPlanExecutor.cs @@ -0,0 +1,7 @@ +namespace Umbraco.Cms.Core.Migrations +{ + public interface IMigrationPlanExecutor + { + string Execute(MigrationPlan plan, string fromState); + } +} diff --git a/src/Umbraco.Infrastructure/Migrations/MergeBuilder.cs b/src/Umbraco.Core/Migrations/MergeBuilder.cs similarity index 96% rename from src/Umbraco.Infrastructure/Migrations/MergeBuilder.cs rename to src/Umbraco.Core/Migrations/MergeBuilder.cs index 4385fd54b8..a00a774db4 100644 --- a/src/Umbraco.Infrastructure/Migrations/MergeBuilder.cs +++ b/src/Umbraco.Core/Migrations/MergeBuilder.cs @@ -1,8 +1,7 @@ -using System; +using System; using System.Collections.Generic; -using Umbraco.Cms.Core.Migrations; -namespace Umbraco.Cms.Infrastructure.Migrations +namespace Umbraco.Cms.Core.Migrations { /// /// Represents a migration plan builder for merges. diff --git a/src/Umbraco.Infrastructure/Migrations/MigrationPlan.cs b/src/Umbraco.Core/Migrations/MigrationPlan.cs similarity index 68% rename from src/Umbraco.Infrastructure/Migrations/MigrationPlan.cs rename to src/Umbraco.Core/Migrations/MigrationPlan.cs index 3bde224640..19bd551075 100644 --- a/src/Umbraco.Infrastructure/Migrations/MigrationPlan.cs +++ b/src/Umbraco.Core/Migrations/MigrationPlan.cs @@ -1,14 +1,12 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; -using Microsoft.Extensions.Logging; -using Umbraco.Cms.Core.Migrations; -using Umbraco.Cms.Core.Scoping; using Umbraco.Extensions; using Type = System.Type; -namespace Umbraco.Cms.Infrastructure.Migrations +namespace Umbraco.Cms.Core.Migrations { + /// /// Represents a migration plan. /// @@ -26,8 +24,10 @@ namespace Umbraco.Cms.Infrastructure.Migrations /// The name of the plan. public MigrationPlan(string name) { - if (name == null) throw new ArgumentNullException(nameof(name)); - if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(name)); + if (name == null) + throw new ArgumentNullException(nameof(name)); + if (string.IsNullOrWhiteSpace(name)) + throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(name)); Name = name; } @@ -37,6 +37,8 @@ namespace Umbraco.Cms.Infrastructure.Migrations /// public IReadOnlyDictionary Transitions => _transitions; + public IReadOnlyList PostMigrationTypes => _postMigrationTypes; + /// /// Gets the name of the plan. /// @@ -45,12 +47,18 @@ namespace Umbraco.Cms.Infrastructure.Migrations // adds a transition private MigrationPlan Add(string sourceState, string targetState, Type migration) { - if (sourceState == null) throw new ArgumentNullException(nameof(sourceState)); - if (targetState == null) throw new ArgumentNullException(nameof(targetState)); - if (string.IsNullOrWhiteSpace(targetState)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(targetState)); - if (sourceState == targetState) throw new ArgumentException("Source and target state cannot be identical."); - if (migration == null) throw new ArgumentNullException(nameof(migration)); - if (!migration.Implements()) throw new ArgumentException($"Type {migration.Name} does not implement IMigration.", nameof(migration)); + if (sourceState == null) + throw new ArgumentNullException(nameof(sourceState)); + if (targetState == null) + throw new ArgumentNullException(nameof(targetState)); + if (string.IsNullOrWhiteSpace(targetState)) + throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(targetState)); + if (sourceState == targetState) + throw new ArgumentException("Source and target state cannot be identical."); + if (migration == null) + throw new ArgumentNullException(nameof(migration)); + if (!migration.Implements()) + throw new ArgumentException($"Type {migration.Name} does not implement IMigration.", nameof(migration)); sourceState = sourceState.Trim(); targetState = targetState.Trim(); @@ -112,7 +120,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations /// The previous target state, which we need to recover from through . /// The new target state. public MigrationPlan ToWithReplace(string recoverState, string targetState) - where TMigrationNew: IMigration + where TMigrationNew : IMigration where TMigrationRecover : IMigration { To(targetState); @@ -139,13 +147,20 @@ namespace Umbraco.Cms.Infrastructure.Migrations /// public MigrationPlan ToWithClone(string startState, string endState, string targetState) { - if (startState == null) throw new ArgumentNullException(nameof(startState)); - if (string.IsNullOrWhiteSpace(startState)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(startState)); - if (endState == null) throw new ArgumentNullException(nameof(endState)); - if (string.IsNullOrWhiteSpace(endState)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(endState)); - if (targetState == null) throw new ArgumentNullException(nameof(targetState)); - if (string.IsNullOrWhiteSpace(targetState)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(targetState)); - if (startState == endState) throw new ArgumentException("Start and end states cannot be identical."); + if (startState == null) + throw new ArgumentNullException(nameof(startState)); + if (string.IsNullOrWhiteSpace(startState)) + throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(startState)); + if (endState == null) + throw new ArgumentNullException(nameof(endState)); + if (string.IsNullOrWhiteSpace(endState)) + throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(endState)); + if (targetState == null) + throw new ArgumentNullException(nameof(targetState)); + if (string.IsNullOrWhiteSpace(targetState)) + throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(targetState)); + if (startState == endState) + throw new ArgumentException("Start and end states cannot be identical."); startState = startState.Trim(); endState = endState.Trim(); @@ -173,15 +188,6 @@ namespace Umbraco.Cms.Infrastructure.Migrations return this; } - /// - /// Prepares post-migrations. - /// - /// - /// This can be overriden to filter, complement, and/or re-order post-migrations. - /// - protected virtual IEnumerable PreparePostMigrations(IEnumerable types) - => types; - /// /// Adds a post-migration to the plan. /// @@ -254,7 +260,8 @@ namespace Umbraco.Cms.Infrastructure.Migrations var verified = new List(); foreach (var transition in _transitions.Values) { - if (transition == null || verified.Contains(transition.SourceState)) continue; + if (transition == null || verified.Contains(transition.SourceState)) + continue; var visited = new List { transition.SourceState }; var nextTransition = _transitions[transition.TargetState]; @@ -275,82 +282,11 @@ namespace Umbraco.Cms.Infrastructure.Migrations /// /// Throws an exception when the initial state is unknown. /// - protected virtual void ThrowOnUnknownInitialState(string state) + public virtual void ThrowOnUnknownInitialState(string state) { throw new InvalidOperationException($"The migration plan does not support migrating from state \"{state}\"."); } - /// - /// Executes the plan. - /// - /// A scope. - /// The state to start execution at. - /// A migration builder. - /// A logger. - /// - /// The final state. - /// The plan executes within the scope, which must then be completed. - public string Execute(IScope scope, string fromState, IMigrationBuilder migrationBuilder, ILogger logger, ILoggerFactory loggerFactory) - { - Validate(); - - if (migrationBuilder == null) throw new ArgumentNullException(nameof(migrationBuilder)); - if (logger == null) throw new ArgumentNullException(nameof(logger)); - - logger.LogInformation("Starting '{MigrationName}'...", Name); - - var origState = fromState ?? string.Empty; - - logger.LogInformation("At {OrigState}", string.IsNullOrWhiteSpace(origState) ? "origin": origState); - - if (!_transitions.TryGetValue(origState, out var transition)) - ThrowOnUnknownInitialState(origState); - - var context = new MigrationContext(scope.Database, loggerFactory.CreateLogger()); - context.PostMigrations.AddRange(_postMigrationTypes); - - while (transition != null) - { - logger.LogInformation("Execute {MigrationType}", transition.MigrationType.Name); - - var migration = migrationBuilder.Build(transition.MigrationType, context); - migration.Migrate(); - - var nextState = transition.TargetState; - origState = nextState; - - logger.LogInformation("At {OrigState}", origState); - - // throw a raw exception here: this should never happen as the plan has - // been validated - this is just a paranoid safety test - if (!_transitions.TryGetValue(origState, out transition)) - throw new InvalidOperationException($"Unknown state \"{origState}\"."); - } - - // prepare and de-duplicate post-migrations, only keeping the 1st occurence - var temp = new HashSet(); - var postMigrationTypes = PreparePostMigrations(context.PostMigrations) - .Where(x => !temp.Contains(x)) - .Select(x => { temp.Add(x); return x; }); - - // run post-migrations - foreach (var postMigrationType in postMigrationTypes) - { - logger.LogInformation($"PostMigration: {postMigrationType.FullName}."); - var postMigration = migrationBuilder.Build(postMigrationType, context); - postMigration.Migrate(); - } - - logger.LogInformation("Done (pending scope completion)."); - - // safety check - again, this should never happen as the plan has been validated, - // and this is just a paranoid safety test - if (origState != _finalState) - throw new InvalidOperationException($"Internal error, reached state {origState} which is not final state {_finalState}"); - - return origState; - } - /// /// Follows a path (for tests and debugging). /// diff --git a/src/Umbraco.Core/Packaging/PackageMigrationPlan.cs b/src/Umbraco.Core/Packaging/PackageMigrationPlan.cs new file mode 100644 index 0000000000..114aea8f24 --- /dev/null +++ b/src/Umbraco.Core/Packaging/PackageMigrationPlan.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Umbraco.Cms.Core.Migrations; + +namespace Umbraco.Cms.Core.Packaging +{ + public abstract class PackageMigrationPlan : MigrationPlan + { + protected PackageMigrationPlan(string name) : base(name) + { + } + } +} diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs index 14457e9687..8e230c4963 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs @@ -14,6 +14,7 @@ using Umbraco.Cms.Core.Logging.Serilog.Enrichers; using Umbraco.Cms.Core.Mail; using Umbraco.Cms.Core.Manifest; using Umbraco.Cms.Core.Media; +using Umbraco.Cms.Core.Migrations; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Packaging; using Umbraco.Cms.Core.PropertyEditors; @@ -103,6 +104,7 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection builder.Services.AddUnique(factory => new DefaultShortStringHelper(new DefaultShortStringHelperConfig().WithDefault(factory.GetRequiredService>().Value))); + builder.Services.AddUnique(); builder.Services.AddUnique(factory => new MigrationBuilder(factory)); builder.Services.AddUnique(); diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs index 0da54785f2..a9ab97573a 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.Logging; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Hosting; +using Umbraco.Cms.Core.Migrations; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Migrations.Upgrade; @@ -28,6 +29,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install private readonly ILoggerFactory _loggerFactory; private readonly IDbProviderFactoryCreator _dbProviderFactoryCreator; private readonly IConfigManipulator _configManipulator; + private readonly IMigrationPlanExecutor _migrationPlanExecutor; private readonly DatabaseSchemaCreatorFactory _databaseSchemaCreatorFactory; private DatabaseSchemaResult _databaseSchemaValidationResult; @@ -39,25 +41,26 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install IScopeProvider scopeProvider, IUmbracoDatabaseFactory databaseFactory, IRuntimeState runtime, - ILogger logger, ILoggerFactory loggerFactory, IMigrationBuilder migrationBuilder, IKeyValueService keyValueService, IHostingEnvironment hostingEnvironment, IDbProviderFactoryCreator dbProviderFactoryCreator, IConfigManipulator configManipulator, + IMigrationPlanExecutor migrationPlanExecutor, DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory) { _scopeProvider = scopeProvider; _databaseFactory = databaseFactory; _runtime = runtime; - _logger = logger; + _logger = loggerFactory.CreateLogger(); _loggerFactory = loggerFactory; _migrationBuilder = migrationBuilder; _keyValueService = keyValueService; _hostingEnvironment = hostingEnvironment; _dbProviderFactoryCreator = dbProviderFactoryCreator; _configManipulator = configManipulator; + _migrationPlanExecutor = migrationPlanExecutor; _databaseSchemaCreatorFactory = databaseSchemaCreatorFactory; } @@ -423,7 +426,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install // upgrade var upgrader = new Upgrader(plan); - upgrader.Execute(_scopeProvider, _migrationBuilder, _keyValueService, _loggerFactory.CreateLogger(), _loggerFactory); + upgrader.Execute(_migrationPlanExecutor, _scopeProvider, _keyValueService); var message = "

Upgrade completed!

"; diff --git a/src/Umbraco.Infrastructure/Migrations/MigrationPlanExecutor.cs b/src/Umbraco.Infrastructure/Migrations/MigrationPlanExecutor.cs new file mode 100644 index 0000000000..8c372ebe72 --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/MigrationPlanExecutor.cs @@ -0,0 +1,105 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core.Migrations; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Extensions; +using Type = System.Type; + +namespace Umbraco.Cms.Infrastructure.Migrations +{ + public class MigrationPlanExecutor : IMigrationPlanExecutor + { + private readonly IScopeProvider _scopeProvider; + private readonly ILoggerFactory _loggerFactory; + private readonly IMigrationBuilder _migrationBuilder; + private readonly ILogger _logger; + + public MigrationPlanExecutor(IScopeProvider scopeProvider, ILoggerFactory loggerFactory, IMigrationBuilder migrationBuilder) + { + _scopeProvider = scopeProvider; + _loggerFactory = loggerFactory; + _migrationBuilder = migrationBuilder; + _logger = _loggerFactory.CreateLogger(); + } + + /// + /// Executes the plan. + /// + /// A scope. + /// The state to start execution at. + /// A migration builder. + /// A logger. + /// + /// The final state. + /// The plan executes within the scope, which must then be completed. + public string Execute(MigrationPlan plan, string fromState) + { + plan.Validate(); + + _logger.LogInformation("Starting '{MigrationName}'...", plan.Name); + + var origState = fromState ?? string.Empty; + + _logger.LogInformation("At {OrigState}", string.IsNullOrWhiteSpace(origState) ? "origin" : origState); + + if (!plan.Transitions.TryGetValue(origState, out MigrationPlan.Transition transition)) + { + plan.ThrowOnUnknownInitialState(origState); + } + + using (IScope scope = _scopeProvider.CreateScope(autoComplete: true)) + { + var context = new MigrationContext(scope.Database, _loggerFactory.CreateLogger()); + context.PostMigrations.AddRange(plan.PostMigrationTypes); + + while (transition != null) + { + _logger.LogInformation("Execute {MigrationType}", transition.MigrationType.Name); + + var migration = _migrationBuilder.Build(transition.MigrationType, context); + migration.Migrate(); + + var nextState = transition.TargetState; + origState = nextState; + + _logger.LogInformation("At {OrigState}", origState); + + // throw a raw exception here: this should never happen as the plan has + // been validated - this is just a paranoid safety test + if (!plan.Transitions.TryGetValue(origState, out transition)) + { + throw new InvalidOperationException($"Unknown state \"{origState}\"."); + } + } + + // prepare and de-duplicate post-migrations, only keeping the 1st occurence + var temp = new HashSet(); + var postMigrationTypes = context.PostMigrations + .Where(x => !temp.Contains(x)) + .Select(x => { temp.Add(x); return x; }); + + // run post-migrations + foreach (var postMigrationType in postMigrationTypes) + { + _logger.LogInformation($"PostMigration: {postMigrationType.FullName}."); + var postMigration = _migrationBuilder.Build(postMigrationType, context); + postMigration.Migrate(); + } + } + + _logger.LogInformation("Done (pending scope completion)."); + + // safety check - again, this should never happen as the plan has been validated, + // and this is just a paranoid safety test + var finalState = plan.FinalState; + if (origState != finalState) + { + throw new InvalidOperationException($"Internal error, reached state {origState} which is not final state {finalState}"); + } + + return origState; + } + } +} diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs index 12d8a4afd2..be031d8667 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs @@ -1,5 +1,6 @@ using System; using Umbraco.Cms.Core.Configuration; +using Umbraco.Cms.Core.Migrations; using Umbraco.Cms.Core.Semver; using Umbraco.Cms.Infrastructure.Migrations.Upgrade.Common; using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; @@ -85,7 +86,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade } } - protected override void ThrowOnUnknownInitialState(string state) + public override void ThrowOnUnknownInitialState(string state) { if (TryGetInitStateVersion(state, out var initVersion)) { diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/Upgrader.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/Upgrader.cs index 58b7f9e07b..8b9060a45d 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/Upgrader.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/Upgrader.cs @@ -1,5 +1,5 @@ -using System; -using Microsoft.Extensions.Logging; +using System; +using Umbraco.Cms.Core.Migrations; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; @@ -37,23 +37,16 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade /// Executes. /// /// A scope provider. - /// A migration builder. /// A key-value service. - /// A logger. - /// A logger factory - public void Execute(IScopeProvider scopeProvider, IMigrationBuilder migrationBuilder, IKeyValueService keyValueService, ILogger logger, ILoggerFactory loggerFactory) + public void Execute(IMigrationPlanExecutor migrationPlanExecutor, IScopeProvider scopeProvider, IKeyValueService keyValueService) { if (scopeProvider == null) throw new ArgumentNullException(nameof(scopeProvider)); - if (migrationBuilder == null) throw new ArgumentNullException(nameof(migrationBuilder)); if (keyValueService == null) throw new ArgumentNullException(nameof(keyValueService)); - if (logger == null) throw new ArgumentNullException(nameof(logger)); var plan = Plan; using (var scope = scopeProvider.CreateScope()) { - BeforeMigrations(scope, logger); - // read current state var currentState = keyValueService.GetValue(StateValueKey); var forceState = false; @@ -65,33 +58,24 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade } // execute plan - var state = plan.Execute(scope, currentState, migrationBuilder, loggerFactory.CreateLogger(), loggerFactory); + var state = migrationPlanExecutor.Execute(plan, currentState); if (string.IsNullOrWhiteSpace(state)) + { throw new Exception("Plan execution returned an invalid null or empty state."); + } // save new state if (forceState) + { keyValueService.SetValue(StateValueKey, state); + } else if (currentState != state) + { keyValueService.SetValue(StateValueKey, currentState, state); - - AfterMigrations(scope, logger); + } scope.Complete(); } } - - /// - /// Executes as part of the upgrade scope and before all migrations have executed. - /// - public virtual void BeforeMigrations(IScope scope, ILogger logger) - { } - - /// - /// Executes as part of the upgrade scope and after all migrations have executed. - /// - public virtual void AfterMigrations(IScope scope, ILogger logger) - { } - } } diff --git a/src/Umbraco.Infrastructure/RuntimeState.cs b/src/Umbraco.Infrastructure/RuntimeState.cs index 894536e4e7..bef9adb76d 100644 --- a/src/Umbraco.Infrastructure/RuntimeState.cs +++ b/src/Umbraco.Infrastructure/RuntimeState.cs @@ -263,22 +263,6 @@ namespace Umbraco.Cms.Core } } - private bool EnsureUmbracoUpgradeState(IUmbracoDatabaseFactory databaseFactory, ILogger logger) - { - var upgrader = new Upgrader(new UmbracoPlan(_umbracoVersion)); - var stateValueKey = upgrader.StateValueKey; - - // no scope, no service - just directly accessing the database - using (var database = databaseFactory.CreateDatabase()) - { - CurrentMigrationState = database.GetFromKeyValueTable(stateValueKey); - FinalMigrationState = upgrader.Plan.FinalState; - } - - logger.LogDebug("Final upgrade state is {FinalMigrationState}, database contains {DatabaseState}", FinalMigrationState, CurrentMigrationState ?? ""); - - return CurrentMigrationState == FinalMigrationState; - } private bool DoesUmbracoRequireUpgrade(IUmbracoDatabase database) { var upgrader = new Upgrader(new UmbracoPlan(_umbracoVersion)); diff --git a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Migrations/AdvancedMigrationTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Migrations/AdvancedMigrationTests.cs index 5ecf2f5503..e87ddfc53d 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Migrations/AdvancedMigrationTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Migrations/AdvancedMigrationTests.cs @@ -9,6 +9,7 @@ using Moq; using NUnit.Framework; using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Migrations; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Migrations; @@ -29,6 +30,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Migrations private IUmbracoVersion UmbracoVersion => GetRequiredService(); private IEventAggregator EventAggregator => GetRequiredService(); + private IMigrationPlanExecutor MigrationPlanExecutor => GetRequiredService(); [Test] public void CreateTableOfTDto() @@ -53,7 +55,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Migrations .From(string.Empty) .To("done")); - upgrader.Execute(ScopeProvider, builder, Mock.Of(), _loggerFactory.CreateLogger(), _loggerFactory); + upgrader.Execute(MigrationPlanExecutor, ScopeProvider, Mock.Of()); var helper = new DatabaseSchemaCreator(scope.Database, LoggerFactory.CreateLogger(), LoggerFactory, UmbracoVersion, EventAggregator); bool exists = helper.TableExists("umbracoUser"); @@ -90,7 +92,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Migrations .To("a") .To("done")); - upgrader.Execute(ScopeProvider, builder, Mock.Of(), _loggerFactory.CreateLogger(), _loggerFactory); + upgrader.Execute(MigrationPlanExecutor, ScopeProvider, Mock.Of()); scope.Complete(); } } @@ -125,7 +127,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Migrations .To("b") .To("done")); - upgrader.Execute(ScopeProvider, builder, Mock.Of(), _loggerFactory.CreateLogger(), _loggerFactory); + upgrader.Execute(MigrationPlanExecutor, ScopeProvider, Mock.Of()); scope.Complete(); } } @@ -160,7 +162,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Migrations .To("b") .To("done")); - upgrader.Execute(ScopeProvider, builder, Mock.Of(), _loggerFactory.CreateLogger(), _loggerFactory); + upgrader.Execute(MigrationPlanExecutor, ScopeProvider, Mock.Of()); scope.Complete(); } } @@ -192,7 +194,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Migrations .To("a") .To("done")); - upgrader.Execute(ScopeProvider, builder, Mock.Of(), _loggerFactory.CreateLogger(), _loggerFactory); + upgrader.Execute(MigrationPlanExecutor, ScopeProvider, Mock.Of()); scope.Complete(); } } diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/MigrationPlanTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/MigrationPlanTests.cs index c6090a7bc2..35fcf88f2e 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/MigrationPlanTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/MigrationPlanTests.cs @@ -57,6 +57,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Migrations } }); + var executor = new MigrationPlanExecutor(scopeProvider, loggerFactory, migrationBuilder); + MigrationPlan plan = new MigrationPlan("default") .From(string.Empty) .To("{4A9A1A8F-0DA1-4BCF-AD06-C19D79152E35}") @@ -72,8 +74,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Migrations // read current state var sourceState = kvs.GetValue("Umbraco.Tests.MigrationPlan") ?? string.Empty; - // execute plan - state = plan.Execute(s, sourceState, migrationBuilder, loggerFactory.CreateLogger(), loggerFactory); + // execute plan + state = executor.Execute(plan, sourceState); // save new state kvs.SetValue("Umbraco.Tests.MigrationPlan", sourceState, state); diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/PostMigrationTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/PostMigrationTests.cs index 4b1e2aa526..3952dea607 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/PostMigrationTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/PostMigrationTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using System; @@ -24,6 +24,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Migrations public class PostMigrationTests { private static readonly ILoggerFactory s_loggerFactory = NullLoggerFactory.Instance; + private IMigrationPlanExecutor GetMigrationPlanExecutor(IScopeProvider scopeProvider, IMigrationBuilder builder) + => new MigrationPlanExecutor(scopeProvider, s_loggerFactory, builder); [Test] public void ExecutesPlanPostMigration() @@ -63,12 +65,11 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Migrations TestPostMigration.MigrateCount = 0; var upgrader = new Upgrader(plan); + IMigrationPlanExecutor executor = GetMigrationPlanExecutor(scopeProvider, builder); upgrader.Execute( + executor, scopeProvider, - builder, - Mock.Of(), - s_loggerFactory.CreateLogger(), - s_loggerFactory); + Mock.Of()); Assert.AreEqual(1, TestPostMigration.MigrateCount); } @@ -115,12 +116,11 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Migrations new MigrationContext(database, s_loggerFactory.CreateLogger()); var upgrader = new Upgrader(plan); + IMigrationPlanExecutor executor = GetMigrationPlanExecutor(scopeProvider, builder); upgrader.Execute( + executor, scopeProvider, - builder, - Mock.Of(), - s_loggerFactory.CreateLogger(), - s_loggerFactory); + Mock.Of()); Assert.AreEqual(1, TestMigration.MigrateCount); Assert.AreEqual(1, TestPostMigration.MigrateCount); From a46ac7c98cd6557a906b7db58ee603a7f9baac1f Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 8 Jun 2021 10:13:05 -0600 Subject: [PATCH 02/11] cleanup --- .../Umbraco.Infrastructure/Migrations/AdvancedMigrationTests.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Migrations/AdvancedMigrationTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Migrations/AdvancedMigrationTests.cs index e87ddfc53d..27f3ac2c48 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Migrations/AdvancedMigrationTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Migrations/AdvancedMigrationTests.cs @@ -26,8 +26,6 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Migrations [UmbracoTest(Database = UmbracoTestOptions.Database.NewEmptyPerTest)] public class AdvancedMigrationTests : UmbracoIntegrationTest { - private readonly ILoggerFactory _loggerFactory = NullLoggerFactory.Instance; - private IUmbracoVersion UmbracoVersion => GetRequiredService(); private IEventAggregator EventAggregator => GetRequiredService(); private IMigrationPlanExecutor MigrationPlanExecutor => GetRequiredService(); From 77017f25b310369cf867cffac9c087412ead8f82 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 8 Jun 2021 10:58:17 -0600 Subject: [PATCH 03/11] Adds KeyValueService Find and tests --- .../Extensions/TypeLoaderExtensions.cs | 18 +++++++------- .../Packaging/PackageMigrationPlan.cs | 6 ++++- .../Repositories/IKeyValueRepository.cs | 9 ++++++- src/Umbraco.Core/Services/IKeyValueService.cs | 12 +++++++++- .../Persistence/Mappers/AccessMapper.cs | 3 ++- .../Persistence/Mappers/KeyValueMapper.cs | 22 +++++++++++++++++ .../Mappers/MapperCollectionBuilder.cs | 1 + .../Implement/KeyValueRepository.cs | 21 +++++++++------- .../Services/Implement/KeyValueService.cs | 12 +++++++++- .../Services/KeyValueServiceTests.cs | 24 ++++++++++++++++++- 10 files changed, 105 insertions(+), 23 deletions(-) create mode 100644 src/Umbraco.Infrastructure/Persistence/Mappers/KeyValueMapper.cs diff --git a/src/Umbraco.Core/Extensions/TypeLoaderExtensions.cs b/src/Umbraco.Core/Extensions/TypeLoaderExtensions.cs index e6e67f6153..b2d9618f30 100644 --- a/src/Umbraco.Core/Extensions/TypeLoaderExtensions.cs +++ b/src/Umbraco.Core/Extensions/TypeLoaderExtensions.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Composing; +using Umbraco.Cms.Core.Packaging; using Umbraco.Cms.Core.PropertyEditors; namespace Umbraco.Extensions @@ -14,17 +15,18 @@ namespace Umbraco.Extensions /// /// Gets all classes implementing . /// - public static IEnumerable GetDataEditors(this TypeLoader mgr) - { - return mgr.GetTypes(); - } + public static IEnumerable GetDataEditors(this TypeLoader mgr) => mgr.GetTypes(); /// /// Gets all classes implementing ICacheRefresher. /// - public static IEnumerable GetCacheRefreshers(this TypeLoader mgr) - { - return mgr.GetTypes(); - } + public static IEnumerable GetCacheRefreshers(this TypeLoader mgr) => mgr.GetTypes(); + + /// + /// Gest all classes implementing + /// + /// + /// + public static IEnumerable GetPackageMigrationPlans(this TypeLoader mgr) => mgr.GetTypes(); } } diff --git a/src/Umbraco.Core/Packaging/PackageMigrationPlan.cs b/src/Umbraco.Core/Packaging/PackageMigrationPlan.cs index 114aea8f24..3688f5467a 100644 --- a/src/Umbraco.Core/Packaging/PackageMigrationPlan.cs +++ b/src/Umbraco.Core/Packaging/PackageMigrationPlan.cs @@ -1,11 +1,15 @@ using System; using System.Collections.Generic; using System.Text; +using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Migrations; namespace Umbraco.Cms.Core.Packaging { - public abstract class PackageMigrationPlan : MigrationPlan + /// + /// Base class for package migration plans + /// + public abstract class PackageMigrationPlan : MigrationPlan, IDiscoverable { protected PackageMigrationPlan(string name) : base(name) { diff --git a/src/Umbraco.Core/Persistence/Repositories/IKeyValueRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IKeyValueRepository.cs index e9625892d4..9d5119898c 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IKeyValueRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IKeyValueRepository.cs @@ -1,8 +1,15 @@ -using Umbraco.Cms.Core.Models; +using System.Collections.Generic; +using Umbraco.Cms.Core.Models; namespace Umbraco.Cms.Core.Persistence.Repositories { public interface IKeyValueRepository : IReadRepository, IWriteRepository { + /// + /// Returns key/value pairs for all keys with the specified prefix. + /// + /// + /// + IReadOnlyDictionary Find(string keyPrefix); } } diff --git a/src/Umbraco.Core/Services/IKeyValueService.cs b/src/Umbraco.Core/Services/IKeyValueService.cs index 4c24ca19de..456733094c 100644 --- a/src/Umbraco.Core/Services/IKeyValueService.cs +++ b/src/Umbraco.Core/Services/IKeyValueService.cs @@ -1,4 +1,7 @@ -namespace Umbraco.Cms.Core.Services +using System.Collections; +using System.Collections.Generic; + +namespace Umbraco.Cms.Core.Services { /// /// Manages the simplified key/value store. @@ -11,6 +14,13 @@ /// Returns null if no value was found for the key. string GetValue(string key); + /// + /// Returns key/value pairs for all keys with the specified prefix. + /// + /// + /// + IReadOnlyDictionary Find(string keyPrefix); + /// /// Sets a value. /// diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/AccessMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/AccessMapper.cs index ddfcae09f1..8d88b2d7df 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/AccessMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/AccessMapper.cs @@ -1,9 +1,10 @@ -using System; +using System; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; namespace Umbraco.Cms.Infrastructure.Persistence.Mappers { + [MapperFor(typeof(PublicAccessEntry))] public sealed class AccessMapper : BaseMapper { diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/KeyValueMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/KeyValueMapper.cs new file mode 100644 index 0000000000..a133d4066c --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/KeyValueMapper.cs @@ -0,0 +1,22 @@ +using System; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; + +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers +{ + [MapperFor(typeof(KeyValue))] + [MapperFor(typeof(IKeyValue))] + public sealed class KeyValueMapper : BaseMapper + { + public KeyValueMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) + { } + + protected override void DefineMaps() + { + DefineMap(nameof(KeyValue.Identifier), nameof(KeyValueDto.Key)); + DefineMap(nameof(KeyValue.Value), nameof(KeyValueDto.Value)); + DefineMap(nameof(KeyValue.UpdateDate), nameof(KeyValueDto.UpdateDate)); + } + } +} diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/MapperCollectionBuilder.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/MapperCollectionBuilder.cs index e797319810..8b993365a0 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/MapperCollectionBuilder.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/MapperCollectionBuilder.cs @@ -32,6 +32,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Mappers Add(); Add(); Add(); + Add(); Add(); Add(); Add(); diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/KeyValueRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/KeyValueRepository.cs index 74988d2026..a4ffe7ee9a 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/KeyValueRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/KeyValueRepository.cs @@ -9,6 +9,7 @@ using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement @@ -19,6 +20,11 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement : base(scopeAccessor, AppCaches.NoCache, logger) { } + /// + public IReadOnlyDictionary Find(string keyPrefix) + => Get(Query().Where(entity => entity.Identifier.StartsWith(keyPrefix))) + .ToDictionary(x => x.Identifier, x => x.Value); + #region Overrides of IReadWriteQueryRepository public override void Save(IKeyValue entity) @@ -47,15 +53,9 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement return sql; } - protected override string GetBaseWhereClause() - { - return Cms.Core.Constants.DatabaseSchema.Tables.KeyValue + ".key = @id"; - } + protected override string GetBaseWhereClause() => Core.Constants.DatabaseSchema.Tables.KeyValue + ".key = @id"; - protected override IEnumerable GetDeleteClauses() - { - return Enumerable.Empty(); - } + protected override IEnumerable GetDeleteClauses() => Enumerable.Empty(); protected override IKeyValue PerformGet(string id) { @@ -73,7 +73,10 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement protected override IEnumerable PerformGetByQuery(IQuery query) { - throw new NotSupportedException(); + var sqlClause = GetBaseQuery(false); + var translator = new SqlTranslator(sqlClause, query); + var sql = translator.Translate(); + return Database.Fetch(sql).Select(Map); } protected override void PersistNewItem(IKeyValue entity) diff --git a/src/Umbraco.Infrastructure/Services/Implement/KeyValueService.cs b/src/Umbraco.Infrastructure/Services/Implement/KeyValueService.cs index bcd35ee7f4..73bdbdc7e8 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/KeyValueService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/KeyValueService.cs @@ -1,4 +1,5 @@ -using System; +using System; +using System.Collections.Generic; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; @@ -25,6 +26,15 @@ namespace Umbraco.Cms.Core.Services.Implement } } + /// + public IReadOnlyDictionary Find(string keyPrefix) + { + using (var scope = _scopeProvider.CreateScope(autoComplete: true)) + { + return _repository.Find(keyPrefix); + } + } + /// public void SetValue(string key, string value) { diff --git a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/KeyValueServiceTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/KeyValueServiceTests.cs index 3437e1b286..0aa1892390 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/KeyValueServiceTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/KeyValueServiceTests.cs @@ -1,6 +1,7 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. +using System.Collections.Generic; using System.Threading; using NUnit.Framework; using Umbraco.Cms.Core.Services; @@ -19,6 +20,27 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services { private IKeyValueService KeyValueService => GetRequiredService(); + [Test] + public void Can_Query_For_Key_Prefix() + { + // Arrange + KeyValueService.SetValue("test1", "hello1"); + KeyValueService.SetValue("test2", "hello2"); + KeyValueService.SetValue("test3", "hello3"); + KeyValueService.SetValue("test4", "hello4"); + + // Act + IReadOnlyDictionary value = KeyValueService.Find("test"); + + // Assert + + Assert.AreEqual(4, value.Count); + Assert.AreEqual("hello1", value["test1"]); + Assert.AreEqual("hello2", value["test2"]); + Assert.AreEqual("hello3", value["test3"]); + Assert.AreEqual("hello4", value["test4"]); + } + [Test] public void GetValue_ForMissingKey_ReturnsNull() { From 26f989124de975438f5b4270ac3b0c0b6a0e1505 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 8 Jun 2021 14:56:45 -0600 Subject: [PATCH 04/11] New package migration collection, updates runtime state to check for package migrations, adds tests for this. --- .../Composing/LazyCollectionBuilderBase.cs | 2 +- src/Umbraco.Core/Composing/RuntimeHash.cs | 4 +- .../Models/UnattendedSettings.cs | 9 +- src/Umbraco.Core/Constants-Conventions.cs | 7 + src/Umbraco.Core/Constants-System.cs | 5 +- .../UmbracoBuilder.Collections.cs | 13 +- .../Extensions/TypeLoaderExtensions.cs | 14 +- src/Umbraco.Core/Migrations/MigrationPlan.cs | 17 +- .../Packaging/PackageMigrationPlan.cs | 6 + .../PackageMigrationPlanCollection.cs | 15 ++ .../PackageMigrationPlanCollectionBuilder.cs | 9 + src/Umbraco.Core/RuntimeLevel.cs | 7 +- src/Umbraco.Core/RuntimeLevelReason.cs | 7 +- .../Migrations/Install/DatabaseBuilder.cs | 2 +- .../Migrations/Upgrade/UmbracoPlan.cs | 2 +- .../Migrations/Upgrade/Upgrader.cs | 8 +- .../Querying/ExpressionVisitorBase.cs | 38 ++-- .../Querying/SqlExpressionExtensions.cs | 29 +-- .../Persistence/UmbracoDatabaseExtensions.cs | 28 ++- src/Umbraco.Infrastructure/RuntimeState.cs | 175 ++++++++++++------ .../Umbraco.Core/RuntimeStateTests.cs | 100 ++++++++++ 21 files changed, 377 insertions(+), 120 deletions(-) create mode 100644 src/Umbraco.Core/Packaging/PackageMigrationPlanCollection.cs create mode 100644 src/Umbraco.Core/Packaging/PackageMigrationPlanCollectionBuilder.cs create mode 100644 src/Umbraco.Tests.Integration/Umbraco.Core/RuntimeStateTests.cs diff --git a/src/Umbraco.Core/Composing/LazyCollectionBuilderBase.cs b/src/Umbraco.Core/Composing/LazyCollectionBuilderBase.cs index d8721b0d19..d41813f7d8 100644 --- a/src/Umbraco.Core/Composing/LazyCollectionBuilderBase.cs +++ b/src/Umbraco.Core/Composing/LazyCollectionBuilderBase.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; diff --git a/src/Umbraco.Core/Composing/RuntimeHash.cs b/src/Umbraco.Core/Composing/RuntimeHash.cs index ae13b49915..4eb70cea1f 100644 --- a/src/Umbraco.Core/Composing/RuntimeHash.cs +++ b/src/Umbraco.Core/Composing/RuntimeHash.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; using System.Linq; using Umbraco.Cms.Core.Logging; @@ -40,7 +40,7 @@ namespace Umbraco.Cms.Core.Composing /// file properties (false) or the file contents (true). private string GetFileHash(IEnumerable<(FileSystemInfo fileOrFolder, bool scanFileContent)> filesAndFolders) { - using (_logger.DebugDuration("Determining hash of code files on disk", "Hash determined")) + using (_logger.DebugDuration("Determining hash of code files on disk", "Hash determined")) { // get the distinct file infos to hash var uniqInfos = new HashSet(); diff --git a/src/Umbraco.Core/Configuration/Models/UnattendedSettings.cs b/src/Umbraco.Core/Configuration/Models/UnattendedSettings.cs index 26c3ee72c4..34af683fbe 100644 --- a/src/Umbraco.Core/Configuration/Models/UnattendedSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/UnattendedSettings.cs @@ -1,4 +1,4 @@ -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; namespace Umbraco.Cms.Core.Configuration.Models { @@ -24,6 +24,13 @@ namespace Umbraco.Cms.Core.Configuration.Models /// public bool UpgradeUnattended { get; set; } = false; + /// + /// Gets or sets a value indicating whether unattended package migrations are enabled. + /// + /// + /// This is true by default. + /// + public bool PackageMigrationsUnattended { get; set; } = true; /// /// Gets or sets a value to use for creating a user with a name for Unattended Installs diff --git a/src/Umbraco.Core/Constants-Conventions.cs b/src/Umbraco.Core/Constants-Conventions.cs index 9ebbf21369..8915942a3b 100644 --- a/src/Umbraco.Core/Constants-Conventions.cs +++ b/src/Umbraco.Core/Constants-Conventions.cs @@ -7,6 +7,13 @@ namespace Umbraco.Cms.Core /// public static class Conventions { + public static class Migrations + { + public const string UmbracoUpgradePlanName = "Umbraco.Core"; + public const string KeyValuePrefix = "Umbraco.Core.Upgrader.State+"; + public const string UmbracoUpgradePlanKey = KeyValuePrefix + UmbracoUpgradePlanName; + } + public static class PermissionCategories { public const string ContentCategory = "content"; diff --git a/src/Umbraco.Core/Constants-System.cs b/src/Umbraco.Core/Constants-System.cs index 44ee99c420..ddff380c08 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 { @@ -59,8 +59,7 @@ public const string RecycleBinMediaPathPrefix = "-1,-21,"; public const int DefaultLabelDataTypeId = -92; - public const string UmbracoConnectionName = "umbracoDbDSN"; - public const string UmbracoUpgradePlanName = "Umbraco.Core"; + public const string UmbracoConnectionName = "umbracoDbDSN"; } } } diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs index ec526d8c0f..eddb8c5b2c 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs @@ -8,6 +8,7 @@ using Umbraco.Cms.Core.HealthChecks; using Umbraco.Cms.Core.HealthChecks.NotificationMethods; using Umbraco.Cms.Core.Manifest; using Umbraco.Cms.Core.Media.EmbedProviders; +using Umbraco.Cms.Core.Packaging; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.PropertyEditors.Validators; using Umbraco.Cms.Core.Routing; @@ -32,7 +33,9 @@ namespace Umbraco.Cms.Core.DependencyInjection { builder.CacheRefreshers().Add(() => builder.TypeLoader.GetCacheRefreshers()); builder.DataEditors().Add(() => builder.TypeLoader.GetDataEditors()); - builder.Actions().Add(() => builder.TypeLoader.GetTypes()); + builder.Actions().Add(() => builder.TypeLoader.GetActions()); + builder.PackageMigrationPlans().Add(() => builder.TypeLoader.GetPackageMigrationPlans()); + // register known content apps builder.ContentApps() .Append() @@ -42,6 +45,7 @@ namespace Umbraco.Cms.Core.DependencyInjection .Append() .Append() .Append(); + // all built-in finders in the correct order, // devs can then modify this list on application startup builder.ContentFinders() @@ -116,6 +120,13 @@ namespace Umbraco.Cms.Core.DependencyInjection builder.BackOfficeAssets(); } + /// + /// Gets the package migration plans collection builder. + /// + /// The builder. + public static PackageMigrationPlanCollectionBuilder PackageMigrationPlans(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); + /// /// Gets the actions collection builder. /// diff --git a/src/Umbraco.Core/Extensions/TypeLoaderExtensions.cs b/src/Umbraco.Core/Extensions/TypeLoaderExtensions.cs index b2d9618f30..6ac5432806 100644 --- a/src/Umbraco.Core/Extensions/TypeLoaderExtensions.cs +++ b/src/Umbraco.Core/Extensions/TypeLoaderExtensions.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using Umbraco.Cms.Core.Actions; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Packaging; @@ -13,20 +14,27 @@ namespace Umbraco.Extensions public static class TypeLoaderExtensions { /// - /// Gets all classes implementing . + /// Gets all types implementing . /// public static IEnumerable GetDataEditors(this TypeLoader mgr) => mgr.GetTypes(); /// - /// Gets all classes implementing ICacheRefresher. + /// Gets all types implementing ICacheRefresher. /// public static IEnumerable GetCacheRefreshers(this TypeLoader mgr) => mgr.GetTypes(); /// - /// Gest all classes implementing + /// Gets all types implementing /// /// /// public static IEnumerable GetPackageMigrationPlans(this TypeLoader mgr) => mgr.GetTypes(); + + /// + /// Gets all types implementing + /// + /// + /// + public static IEnumerable GetActions(this TypeLoader mgr) => mgr.GetTypes(); } } diff --git a/src/Umbraco.Core/Migrations/MigrationPlan.cs b/src/Umbraco.Core/Migrations/MigrationPlan.cs index 19bd551075..774eb5c450 100644 --- a/src/Umbraco.Core/Migrations/MigrationPlan.cs +++ b/src/Umbraco.Core/Migrations/MigrationPlan.cs @@ -25,9 +25,14 @@ namespace Umbraco.Cms.Core.Migrations public MigrationPlan(string name) { if (name == null) + { throw new ArgumentNullException(nameof(name)); + } + if (string.IsNullOrWhiteSpace(name)) + { throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(name)); + } Name = name; } @@ -48,7 +53,7 @@ namespace Umbraco.Cms.Core.Migrations private MigrationPlan Add(string sourceState, string targetState, Type migration) { if (sourceState == null) - throw new ArgumentNullException(nameof(sourceState)); + throw new ArgumentNullException(nameof(sourceState), $"{nameof(sourceState)} is null, {nameof(MigrationPlan)}.{nameof(MigrationPlan.From)} must not have been called."); if (targetState == null) throw new ArgumentNullException(nameof(targetState)); if (string.IsNullOrWhiteSpace(targetState)) @@ -90,6 +95,9 @@ namespace Umbraco.Cms.Core.Migrations public MigrationPlan To(string targetState) => To(targetState); + public MigrationPlan To(Guid targetState) + => To(targetState.ToString()); + /// /// Adds a transition to a target state through a migration. /// @@ -97,12 +105,19 @@ namespace Umbraco.Cms.Core.Migrations where TMigration : IMigration => To(targetState, typeof(TMigration)); + public MigrationPlan To(Guid targetState) + where TMigration : IMigration + => To(targetState, typeof(TMigration)); + /// /// Adds a transition to a target state through a migration. /// public MigrationPlan To(string targetState, Type migration) => Add(_prevState, targetState, migration); + public MigrationPlan To(Guid targetState, Type migration) + => Add(_prevState, targetState.ToString(), migration); + /// /// Sets the starting state. /// diff --git a/src/Umbraco.Core/Packaging/PackageMigrationPlan.cs b/src/Umbraco.Core/Packaging/PackageMigrationPlan.cs index 3688f5467a..576fbdc343 100644 --- a/src/Umbraco.Core/Packaging/PackageMigrationPlan.cs +++ b/src/Umbraco.Core/Packaging/PackageMigrationPlan.cs @@ -13,6 +13,12 @@ namespace Umbraco.Cms.Core.Packaging { protected PackageMigrationPlan(string name) : base(name) { + // A call to From must be done first + From(string.Empty); + + DefinePlan(); } + + protected abstract void DefinePlan(); } } diff --git a/src/Umbraco.Core/Packaging/PackageMigrationPlanCollection.cs b/src/Umbraco.Core/Packaging/PackageMigrationPlanCollection.cs new file mode 100644 index 0000000000..2a17add2e6 --- /dev/null +++ b/src/Umbraco.Core/Packaging/PackageMigrationPlanCollection.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using Umbraco.Cms.Core.Composing; + +namespace Umbraco.Cms.Core.Packaging +{ + /// + /// A collection of + /// + public class PackageMigrationPlanCollection : BuilderCollectionBase + { + public PackageMigrationPlanCollection(IEnumerable items) : base(items) + { + } + } +} diff --git a/src/Umbraco.Core/Packaging/PackageMigrationPlanCollectionBuilder.cs b/src/Umbraco.Core/Packaging/PackageMigrationPlanCollectionBuilder.cs new file mode 100644 index 0000000000..bf496852c6 --- /dev/null +++ b/src/Umbraco.Core/Packaging/PackageMigrationPlanCollectionBuilder.cs @@ -0,0 +1,9 @@ +using Umbraco.Cms.Core.Composing; + +namespace Umbraco.Cms.Core.Packaging +{ + public class PackageMigrationPlanCollectionBuilder : LazyCollectionBuilderBase + { + protected override PackageMigrationPlanCollectionBuilder This => this; + } +} diff --git a/src/Umbraco.Core/RuntimeLevel.cs b/src/Umbraco.Core/RuntimeLevel.cs index f08a8b9d95..4330c33d94 100644 --- a/src/Umbraco.Core/RuntimeLevel.cs +++ b/src/Umbraco.Core/RuntimeLevel.cs @@ -32,9 +32,14 @@ /// Upgrade = 3, + /// + /// The runtime has detected that Package Migrations need to be executed. + /// + PackageMigrations = 4, + /// /// The runtime has detected an up-to-date Umbraco install and is running. /// - Run = 4 + Run = 100 } } diff --git a/src/Umbraco.Core/RuntimeLevelReason.cs b/src/Umbraco.Core/RuntimeLevelReason.cs index 863843a537..94192c83b2 100644 --- a/src/Umbraco.Core/RuntimeLevelReason.cs +++ b/src/Umbraco.Core/RuntimeLevelReason.cs @@ -1,4 +1,4 @@ -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core { /// /// Describes the reason for the runtime level. @@ -65,6 +65,11 @@ /// UpgradeMigrations, + /// + /// Umbraco runs the current version but some package migrations have not run. + /// + UpgradePackageMigrations, + /// /// Umbraco is running. /// diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs index a9ab97573a..691400121f 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs @@ -412,7 +412,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install /// configured and it is possible to connect to the database. /// Runs whichever migrations need to run. /// - public Result UpgradeSchemaAndData(MigrationPlan plan) + public Result UpgradeSchemaAndData(UmbracoPlan plan) { try { diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs index be031d8667..18fe2e1ea2 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs @@ -27,7 +27,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade /// Initializes a new instance of the class. /// public UmbracoPlan(IUmbracoVersion umbracoVersion) - : base(Cms.Core.Constants.System.UmbracoUpgradePlanName) + : base(Core.Constants.Conventions.Migrations.UmbracoUpgradePlanName) { _umbracoVersion = umbracoVersion; DefinePlan(); diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/Upgrader.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/Upgrader.cs index 8b9060a45d..27ff665e11 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/Upgrader.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/Upgrader.cs @@ -1,4 +1,5 @@ using System; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Migrations; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; @@ -13,10 +14,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade /// /// Initializes a new instance of the class. /// - public Upgrader(MigrationPlan plan) - { - Plan = plan; - } + public Upgrader(MigrationPlan plan) => Plan = plan; /// /// Gets the name of the migration plan. @@ -31,7 +29,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade /// /// Gets the key for the state value. /// - public virtual string StateValueKey => "Umbraco.Core.Upgrader.State+" + Name; + public virtual string StateValueKey => Constants.Conventions.Migrations.KeyValuePrefix + Name; /// /// Executes. diff --git a/src/Umbraco.Infrastructure/Persistence/Querying/ExpressionVisitorBase.cs b/src/Umbraco.Infrastructure/Persistence/Querying/ExpressionVisitorBase.cs index 6e08bad7c3..984c85f4fe 100644 --- a/src/Umbraco.Infrastructure/Persistence/Querying/ExpressionVisitorBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/Querying/ExpressionVisitorBase.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; @@ -453,19 +453,19 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Querying else goto case "Contains**String"; - case "SqlWildcard": + case nameof(SqlExpressionExtensions.SqlWildcard): case "StartsWith": case "EndsWith": case "Contains**String": // see "Contains" above case "Equals": - case "SqlStartsWith": - case "SqlEndsWith": - case "SqlContains": - case "SqlEquals": - case "InvariantStartsWith": - case "InvariantEndsWith": - case "InvariantContains": - case "InvariantEquals": + case nameof(SqlExpressionExtensions.SqlStartsWith): + case nameof(SqlExpressionExtensions.SqlEndsWith): + case nameof(SqlExpressionExtensions.SqlContains): + case nameof(SqlExpressionExtensions.SqlEquals): + case nameof(StringExtensions.InvariantStartsWith): + case nameof(StringExtensions.InvariantEndsWith): + case nameof(StringExtensions.InvariantContains): + case nameof(StringExtensions.InvariantEquals): string compareValue; @@ -699,31 +699,31 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Querying { switch (verb) { - case "SqlWildcard": + case nameof(SqlExpressionExtensions.SqlWildcard): SqlParameters.Add(RemoveQuote(val)); return Visited ? string.Empty : SqlSyntax.GetStringColumnWildcardComparison(col, SqlParameters.Count - 1, columnType); case "Equals": - case "InvariantEquals": - case "SqlEquals": + case nameof(StringExtensions.InvariantEquals): + case nameof(SqlExpressionExtensions.SqlEquals): SqlParameters.Add(RemoveQuote(val)); return Visited ? string.Empty : SqlSyntax.GetStringColumnEqualComparison(col, SqlParameters.Count - 1, columnType); case "StartsWith": - case "InvariantStartsWith": - case "SqlStartsWith": + case nameof(StringExtensions.InvariantStartsWith): + case nameof(SqlExpressionExtensions.SqlStartsWith): SqlParameters.Add(RemoveQuote(val) + SqlSyntax.GetWildcardPlaceholder()); return Visited ? string.Empty : SqlSyntax.GetStringColumnWildcardComparison(col, SqlParameters.Count - 1, columnType); case "EndsWith": - case "InvariantEndsWith": - case "SqlEndsWith": + case nameof(StringExtensions.InvariantEndsWith): + case nameof(SqlExpressionExtensions.SqlEndsWith): SqlParameters.Add(SqlSyntax.GetWildcardPlaceholder() + RemoveQuote(val)); return Visited ? string.Empty : SqlSyntax.GetStringColumnWildcardComparison(col, SqlParameters.Count - 1, columnType); case "Contains": - case "InvariantContains": - case "SqlContains": + case nameof(StringExtensions.InvariantContains): + case nameof(SqlExpressionExtensions.SqlContains): var wildcardPlaceholder = SqlSyntax.GetWildcardPlaceholder(); SqlParameters.Add(wildcardPlaceholder + RemoveQuote(val) + wildcardPlaceholder); return Visited ? string.Empty : SqlSyntax.GetStringColumnWildcardComparison(col, SqlParameters.Count - 1, columnType); diff --git a/src/Umbraco.Infrastructure/Persistence/Querying/SqlExpressionExtensions.cs b/src/Umbraco.Infrastructure/Persistence/Querying/SqlExpressionExtensions.cs index 03c5acf92f..99c4c3fd1a 100644 --- a/src/Umbraco.Infrastructure/Persistence/Querying/SqlExpressionExtensions.cs +++ b/src/Umbraco.Infrastructure/Persistence/Querying/SqlExpressionExtensions.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using Umbraco.Extensions; @@ -25,10 +25,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Querying return (value ?? fallbackValue).Equals(other ?? fallbackValue); } - public static bool SqlIn(this IEnumerable collection, T item) - { - return collection.Contains(item); - } + public static bool SqlIn(this IEnumerable collection, T item) => collection.Contains(item); public static bool SqlWildcard(this string str, string txt, TextColumnType columnType) { @@ -39,24 +36,14 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Querying return wildcardmatch.IsMatch(str); } - public static bool SqlContains(this string str, string txt, TextColumnType columnType) - { - return str.InvariantContains(txt); - } +#pragma warning disable IDE0060 // Remove unused parameter + public static bool SqlContains(this string str, string txt, TextColumnType columnType) => str.InvariantContains(txt); - public static bool SqlEquals(this string str, string txt, TextColumnType columnType) - { - return str.InvariantEquals(txt); - } + public static bool SqlEquals(this string str, string txt, TextColumnType columnType) => str.InvariantEquals(txt); - public static bool SqlStartsWith(this string str, string txt, TextColumnType columnType) - { - return str.InvariantStartsWith(txt); - } + public static bool SqlStartsWith(this string str, string txt, TextColumnType columnType) => str.InvariantStartsWith(txt); - public static bool SqlEndsWith(this string str, string txt, TextColumnType columnType) - { - return str.InvariantEndsWith(txt); - } + public static bool SqlEndsWith(this string str, string txt, TextColumnType columnType) => str.InvariantEndsWith(txt); +#pragma warning restore IDE0060 // Remove unused parameter } } diff --git a/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseExtensions.cs b/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseExtensions.cs index f2cc2031d5..86ffc1b128 100644 --- a/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseExtensions.cs +++ b/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseExtensions.cs @@ -1,6 +1,8 @@ -using System; +using System; +using System.Collections.Generic; using System.Linq; using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Persistence @@ -9,24 +11,36 @@ namespace Umbraco.Cms.Infrastructure.Persistence { public static UmbracoDatabase AsUmbracoDatabase(this IUmbracoDatabase database) { - var asDatabase = database as UmbracoDatabase; - if (asDatabase == null) throw new Exception("oops: database."); + if (database is not UmbracoDatabase asDatabase) + { + throw new Exception("oops: database."); + } + return asDatabase; } /// - /// Gets a key/value directly from the database, no scope, nothing. + /// Gets a dictionary of key/values directly from the database, no scope, nothing. /// /// Used by to determine the runtime state. - public static string GetFromKeyValueTable(this IUmbracoDatabase database, string key) + public static IReadOnlyDictionary GetFromKeyValueTable(this IUmbracoDatabase database, string keyPrefix) { if (database is null) return null; + // create the wildcard where clause + ISqlSyntaxProvider sqlSyntax = database.SqlContext.SqlSyntax; + var whereParam = sqlSyntax.GetStringColumnWildcardComparison( + sqlSyntax.GetQuotedColumnName("key"), + 0, + Querying.TextColumnType.NVarchar); + var sql = database.SqlContext.Sql() .Select() .From() - .Where(x => x.Key == key); - return database.FirstOrDefault(sql)?.Value; + .Where(whereParam, keyPrefix + sqlSyntax.GetWildcardPlaceholder()); + + return database.Fetch(sql) + .ToDictionary(x => x.Key, x => x.Value); } diff --git a/src/Umbraco.Infrastructure/RuntimeState.cs b/src/Umbraco.Infrastructure/RuntimeState.cs index bef9adb76d..6db80bbf34 100644 --- a/src/Umbraco.Infrastructure/RuntimeState.cs +++ b/src/Umbraco.Infrastructure/RuntimeState.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Threading; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -7,6 +9,7 @@ using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Exceptions; using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Packaging; using Umbraco.Cms.Core.Semver; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Migrations.Install; @@ -27,6 +30,7 @@ namespace Umbraco.Cms.Core private readonly ILogger _logger; private readonly DatabaseSchemaCreatorFactory _databaseSchemaCreatorFactory; private readonly IEventAggregator _eventAggregator; + private readonly PackageMigrationPlanCollection _packageMigrationPlans; /// /// The initial @@ -48,7 +52,8 @@ namespace Umbraco.Cms.Core IUmbracoDatabaseFactory databaseFactory, ILogger logger, DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory, - IEventAggregator eventAggregator) + IEventAggregator eventAggregator, + PackageMigrationPlanCollection packageMigrationPlans) { _globalSettings = globalSettings; _unattendedSettings = unattendedSettings; @@ -57,6 +62,7 @@ namespace Umbraco.Cms.Core _logger = logger; _databaseSchemaCreatorFactory = databaseSchemaCreatorFactory; _eventAggregator = eventAggregator; + _packageMigrationPlans = packageMigrationPlans; } @@ -101,55 +107,62 @@ namespace Umbraco.Cms.Core switch (GetUmbracoDatabaseState(_databaseFactory)) { - case UmbracoDatabaseState.CannotConnect: - { - // cannot connect to configured database, this is bad, fail - _logger.LogDebug("Could not connect to database."); + case UmbracoDatabaseState.CannotConnect: + { + // cannot connect to configured database, this is bad, fail + _logger.LogDebug("Could not connect to database."); - if (_globalSettings.Value.InstallMissingDatabase) - { - // ok to install on a configured but missing database - Level = RuntimeLevel.Install; - Reason = RuntimeLevelReason.InstallMissingDatabase; - return; - } - - // else it is bad enough that we want to throw - Reason = RuntimeLevelReason.BootFailedCannotConnectToDatabase; - BootFailedException =new BootFailedException("A connection string is configured but Umbraco could not connect to the database."); - throw BootFailedException; - } - case UmbracoDatabaseState.NotInstalled: + if (_globalSettings.Value.InstallMissingDatabase) { - // ok to install on an empty database + // ok to install on a configured but missing database Level = RuntimeLevel.Install; - Reason = RuntimeLevelReason.InstallEmptyDatabase; + Reason = RuntimeLevelReason.InstallMissingDatabase; return; } - 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 - // although the files version matches the code version, the database version does not - // which means the local files have been upgraded but not the database - need to upgrade - _logger.LogDebug("Has not reached the final upgrade step, need to upgrade Umbraco."); - Level = _unattendedSettings.Value.UpgradeUnattended ? RuntimeLevel.Run : RuntimeLevel.Upgrade; - Reason = RuntimeLevelReason.UpgradeMigrations; - } + // else it is bad enough that we want to throw + Reason = RuntimeLevelReason.BootFailedCannotConnectToDatabase; + BootFailedException = new BootFailedException("A connection string is configured but Umbraco could not connect to the database."); + throw BootFailedException; + } + case UmbracoDatabaseState.NotInstalled: + { + // ok to install on an empty database + Level = RuntimeLevel.Install; + Reason = RuntimeLevelReason.InstallEmptyDatabase; + return; + } + 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 + + // although the files version matches the code version, the database version does not + // which means the local files have been upgraded but not the database - need to upgrade + _logger.LogDebug("Has not reached the final upgrade step, need to upgrade Umbraco."); + Level = _unattendedSettings.Value.UpgradeUnattended ? RuntimeLevel.Run : RuntimeLevel.Upgrade; + Reason = RuntimeLevelReason.UpgradeMigrations; + } + break; + case UmbracoDatabaseState.NeedsPackageMigration: + + _logger.LogDebug("Package migrations need to execute."); + Level = _unattendedSettings.Value.PackageMigrationsUnattended ? RuntimeLevel.Run : RuntimeLevel.PackageMigrations; + Reason = RuntimeLevelReason.UpgradePackageMigrations; + break; case UmbracoDatabaseState.Ok: default: - { - // if we already know we want to upgrade, exit here - if (Level == RuntimeLevel.Upgrade) - return; + { + // if we already know we want to upgrade, exit here + if (Level == RuntimeLevel.Upgrade) + return; - // the database version matches the code & files version, all clear, can run - Level = RuntimeLevel.Run; - Reason = RuntimeLevelReason.Run; - } - break; + // the database version matches the code & files version, all clear, can run + Level = RuntimeLevel.Run; + Reason = RuntimeLevelReason.Run; + } + break; } } @@ -158,7 +171,8 @@ namespace Umbraco.Cms.Core Ok, CannotConnect, NotInstalled, - NeedsUpgrade + NeedsUpgrade, + NeedsPackageMigration } private UmbracoDatabaseState GetUmbracoDatabaseState(IUmbracoDatabaseFactory databaseFactory) @@ -178,10 +192,24 @@ namespace Umbraco.Cms.Core return UmbracoDatabaseState.NotInstalled; } - if (DoesUmbracoRequireUpgrade(database)) + // 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; } + + + // TODO: Can we save the result of this since we'll need to re-use it? + IReadOnlyList packagesRequiringMigration = DoesUmbracoRequirePackageMigrations(keyValues); + if (packagesRequiringMigration.Count > 0) + { + return UmbracoDatabaseState.NeedsPackageMigration; + } } return UmbracoDatabaseState.Ok; @@ -206,31 +234,36 @@ namespace Umbraco.Cms.Core public void DoUnattendedInstall() { - // unattended install is not enabled - if (_unattendedSettings.Value.InstallUnattended == false) return; + // unattended install is not enabled + if (_unattendedSettings.Value.InstallUnattended == false) + return; // no connection string set - if (_databaseFactory.Configured == false) return; + if (_databaseFactory.Configured == false) + return; var connect = false; var tries = _globalSettings.Value.InstallMissingDatabase ? 2 : 5; - for (var i = 0;;) + for (var i = 0; ;) { connect = _databaseFactory.CanConnect; - if (connect || ++i == tries) break; + if (connect || ++i == tries) + break; _logger.LogDebug("Could not immediately connect to database, trying again."); Thread.Sleep(1000); } // could not connect to the database - if (connect == false) return; + if (connect == false) + return; using (var database = _databaseFactory.CreateDatabase()) { var hasUmbracoTables = database.IsUmbracoInstalled(); // database has umbraco tables, assume Umbraco is already installed - if (hasUmbracoTables) return; + if (hasUmbracoTables) + return; // all conditions fulfilled, do the install _logger.LogInformation("Starting unattended install."); @@ -263,12 +296,14 @@ namespace Umbraco.Cms.Core } } - private bool DoesUmbracoRequireUpgrade(IUmbracoDatabase database) + private bool DoesUmbracoRequireUpgrade(IReadOnlyDictionary keyValues) { var upgrader = new Upgrader(new UmbracoPlan(_umbracoVersion)); var stateValueKey = upgrader.StateValueKey; - CurrentMigrationState = database.GetFromKeyValueTable(stateValueKey); + _ = keyValues.TryGetValue(stateValueKey, out var value); + + CurrentMigrationState = value; FinalMigrationState = upgrader.Plan.FinalState; _logger.LogDebug("Final upgrade state is {FinalMigrationState}, database contains {DatabaseState}", FinalMigrationState, CurrentMigrationState ?? ""); @@ -276,6 +311,41 @@ namespace Umbraco.Cms.Core return CurrentMigrationState != FinalMigrationState; } + private IReadOnlyList DoesUmbracoRequirePackageMigrations(IReadOnlyDictionary keyValues) + { + var packageMigrationPlans = _packageMigrationPlans.ToList(); + + var result = new List(packageMigrationPlans.Count); + + foreach(PackageMigrationPlan plan in packageMigrationPlans) + { + string currentMigrationState = null; + var planKeyValueKey = Constants.Conventions.Migrations.KeyValuePrefix + plan.Name; + if (keyValues.TryGetValue(planKeyValueKey, out var value)) + { + currentMigrationState = value; + + if (plan.FinalState != value) + { + // Not equal so we need to run + result.Add(plan.Name); + } + } + else + { + // If there is nothing in the DB then we need to run + result.Add(plan.Name); + } + + _logger.LogDebug("Final package migration for {PackagePlan} state is {FinalMigrationState}, database contains {DatabaseState}", + plan.Name, + plan.FinalState, + currentMigrationState ?? ""); + } + + return result; + } + private bool TryDbConnect(IUmbracoDatabaseFactory databaseFactory) { // anything other than install wants a database - see if we can connect @@ -285,7 +355,8 @@ namespace Umbraco.Cms.Core for (var i = 0; ;) { canConnect = databaseFactory.CanConnect; - if (canConnect || ++i == tries) break; + if (canConnect || ++i == tries) + break; _logger.LogDebug("Could not immediately connect to database, trying again."); Thread.Sleep(1000); } diff --git a/src/Umbraco.Tests.Integration/Umbraco.Core/RuntimeStateTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Core/RuntimeStateTests.cs new file mode 100644 index 0000000000..e8031d25b1 --- /dev/null +++ b/src/Umbraco.Tests.Integration/Umbraco.Core/RuntimeStateTests.cs @@ -0,0 +1,100 @@ +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.Packaging; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Migrations; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core +{ + [TestFixture] + [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] + public class RuntimeStateTests : UmbracoIntegrationTest + { + private protected IRuntimeState RuntimeState { get; private set; } + + public override void Configure(IApplicationBuilder app) + { + base.Configure(app); + + RuntimeState = Services.GetRequiredService(); + } + + protected override void CustomTestSetup(IUmbracoBuilder builder) + { + PackageMigrationPlanCollectionBuilder migrations = builder.PackageMigrationPlans(); + migrations.Clear(); + migrations.Add(); + } + + [Test] + public void GivenPackageMigrationsExist_WhenLatestStateIsRegistered_ThenLevelIsRun() + { + // Add the final state to the keyvalue storage + IKeyValueService keyValueService = Services.GetRequiredService(); + keyValueService.SetValue( + Constants.Conventions.Migrations.KeyValuePrefix + TestMigrationPlan.TestMigrationPlanName, + TestMigrationPlan.TestMigrationFinalState.ToString()); + + RuntimeState.DetermineRuntimeLevel(); + + Assert.AreEqual(RuntimeLevel.Run, RuntimeState.Level); + Assert.AreEqual(RuntimeLevelReason.Run, RuntimeState.Reason); + } + + [Test] + public void GivenPackageMigrationsExist_WhenUnattendedMigrations_ThenLevelIsRun() + { + RuntimeState.DetermineRuntimeLevel(); + + Assert.AreEqual(RuntimeLevel.Run, RuntimeState.Level); + Assert.AreEqual(RuntimeLevelReason.UpgradePackageMigrations, RuntimeState.Reason); + } + + [Test] + public void GivenPackageMigrationsExist_WhenNotUnattendedMigrations_ThenLevelIsPackageMigrations() + { + var unattendedOptions = Services.GetRequiredService>(); + unattendedOptions.Value.PackageMigrationsUnattended = false; + + RuntimeState.DetermineRuntimeLevel(); + + Assert.AreEqual(RuntimeLevel.PackageMigrations, RuntimeState.Level); + Assert.AreEqual(RuntimeLevelReason.UpgradePackageMigrations, RuntimeState.Reason); + } + + private class TestMigrationPlan : PackageMigrationPlan + { + public const string TestMigrationPlanName = "Test"; + public static Guid TestMigrationFinalState => new Guid("BB02C392-4007-4A6C-A550-28BA2FF7E43D"); + + public TestMigrationPlan() : base(TestMigrationPlanName) + { + } + + protected override void DefinePlan() + { + To(TestMigrationFinalState); + } + } + + private class TestMigration : MigrationBase + { + public TestMigration(IMigrationContext context) : base(context) + { + } + + public override void Migrate() + { + + } + } + } +} From 7d0572f2d156cab83eeae1dc6fa80295cade1460 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 8 Jun 2021 16:32:26 -0600 Subject: [PATCH 05/11] streamline runtime level checks --- .../Extensions/RuntimeStateExtensions.cs | 17 +++++++++++++++++ ...atabaseServerMessengerNotificationHandler.cs | 2 +- .../Examine/ExamineIndexRebuilder.cs | 2 +- .../Examine/RebuildOnStartupHandler.cs | 2 +- .../Persistence/UmbracoPocoDataBuilder.cs | 4 +++- .../Runtime/CoreRuntime.cs | 8 +++----- src/Umbraco.Infrastructure/RuntimeState.cs | 12 +++++++++++- .../Authorization/BackOfficeHandler.cs | 4 ++-- .../Install/InstallAreaRoutes.cs | 7 +++---- .../Install/InstallAuthorizeAttribute.cs | 6 +++--- .../Routing/BackOfficeAreaRoutes.cs | 1 + .../Routing/PreviewRoutes.cs | 1 + src/Umbraco.Web.Common/Profiler/WebProfiler.cs | 4 ++-- 13 files changed, 49 insertions(+), 21 deletions(-) diff --git a/src/Umbraco.Core/Extensions/RuntimeStateExtensions.cs b/src/Umbraco.Core/Extensions/RuntimeStateExtensions.cs index 4e45ea63d8..c3ef7af561 100644 --- a/src/Umbraco.Core/Extensions/RuntimeStateExtensions.cs +++ b/src/Umbraco.Core/Extensions/RuntimeStateExtensions.cs @@ -5,9 +5,26 @@ namespace Umbraco.Extensions { public static class RuntimeStateExtensions { + /// + /// Returns true if the installer is enabled based on the current runtime state + /// + /// + /// + public static bool EnableInstaller(this IRuntimeState state) + => state.Level == RuntimeLevel.Install || state.Level == RuntimeLevel.Upgrade || state.Level == RuntimeLevel.PackageMigrations; + /// /// Returns true if Umbraco is greater than /// public static bool UmbracoCanBoot(this IRuntimeState state) => state.Level > RuntimeLevel.BootFailed; + + /// + /// Returns true if the runtime state indicates that unattended boot logic should execute + /// + /// + /// + public static bool RunUnattendedBootLogic(this IRuntimeState state) + => (state.Reason == RuntimeLevelReason.UpgradeMigrations || state.Reason == RuntimeLevelReason.UpgradePackageMigrations) + && state.Level == RuntimeLevel.Run; } } diff --git a/src/Umbraco.Infrastructure/Cache/DatabaseServerMessengerNotificationHandler.cs b/src/Umbraco.Infrastructure/Cache/DatabaseServerMessengerNotificationHandler.cs index 092b4c6b99..fc59d06016 100644 --- a/src/Umbraco.Infrastructure/Cache/DatabaseServerMessengerNotificationHandler.cs +++ b/src/Umbraco.Infrastructure/Cache/DatabaseServerMessengerNotificationHandler.cs @@ -38,7 +38,7 @@ namespace Umbraco.Cms.Core.Cache /// public void Handle(UmbracoApplicationStartingNotification notification) { - if (_runtimeState.Level < RuntimeLevel.Run) + if (_runtimeState.Level != RuntimeLevel.Run) { return; } diff --git a/src/Umbraco.Infrastructure/Examine/ExamineIndexRebuilder.cs b/src/Umbraco.Infrastructure/Examine/ExamineIndexRebuilder.cs index 7d0a933027..472321ac9e 100644 --- a/src/Umbraco.Infrastructure/Examine/ExamineIndexRebuilder.cs +++ b/src/Umbraco.Infrastructure/Examine/ExamineIndexRebuilder.cs @@ -112,7 +112,7 @@ namespace Umbraco.Cms.Infrastructure.Examine } } - private bool CanRun() => _mainDom.IsMainDom && _runtimeState.Level >= RuntimeLevel.Run; + private bool CanRun() => _mainDom.IsMainDom && _runtimeState.Level == RuntimeLevel.Run; private void RebuildIndex(string indexName, TimeSpan delay, CancellationToken cancellationToken) { diff --git a/src/Umbraco.Infrastructure/Examine/RebuildOnStartupHandler.cs b/src/Umbraco.Infrastructure/Examine/RebuildOnStartupHandler.cs index 05b016dbb6..7ec59e1c2e 100644 --- a/src/Umbraco.Infrastructure/Examine/RebuildOnStartupHandler.cs +++ b/src/Umbraco.Infrastructure/Examine/RebuildOnStartupHandler.cs @@ -47,7 +47,7 @@ namespace Umbraco.Cms.Infrastructure.Examine /// public void Handle(UmbracoRequestBeginNotification notification) { - if (_runtimeState.Level < RuntimeLevel.Run) + if (_runtimeState.Level != RuntimeLevel.Run) { return; } diff --git a/src/Umbraco.Infrastructure/Persistence/UmbracoPocoDataBuilder.cs b/src/Umbraco.Infrastructure/Persistence/UmbracoPocoDataBuilder.cs index 3e34a77d7b..2e9fb6cebc 100644 --- a/src/Umbraco.Infrastructure/Persistence/UmbracoPocoDataBuilder.cs +++ b/src/Umbraco.Infrastructure/Persistence/UmbracoPocoDataBuilder.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Reflection; using NPoco; using Umbraco.Cms.Infrastructure.Persistence.Dtos; @@ -33,6 +33,8 @@ namespace Umbraco.Cms.Infrastructure.Persistence { var columnInfo = base.GetColumnInfo(mi, type); + // TODO: Is this upgrade flag still relevant? It's a lot of hacking to just set this value + // including the interface method ConfigureForUpgrade for this one circumstance. if (_upgrading) { if (type == typeof(UserDto) && mi.Name == "TourData") columnInfo.IgnoreColumn = true; diff --git a/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs b/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs index f7065fe0b5..715b569b96 100644 --- a/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs +++ b/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs @@ -15,6 +15,7 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Migrations.Install; using Umbraco.Cms.Infrastructure.Migrations.Upgrade; using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Runtime { @@ -106,7 +107,7 @@ namespace Umbraco.Cms.Infrastructure.Runtime DoUnattendedInstall(); DetermineRuntimeLevel(); - if (State.Level <= RuntimeLevel.BootFailed) + if (!State.UmbracoCanBoot()) { return; // The exception will be rethrown by BootFailedMiddelware } @@ -117,17 +118,14 @@ namespace Umbraco.Cms.Infrastructure.Runtime throw new InvalidOperationException($"An instance of {typeof(IApplicationShutdownRegistry)} could not be resolved from the container, ensure that one if registered in your runtime before calling {nameof(IRuntime)}.{nameof(StartAsync)}"); } - - // if level is Run and reason is UpgradeMigrations, that means we need to perform an unattended upgrade - if (State.Reason == RuntimeLevelReason.UpgradeMigrations && State.Level == RuntimeLevel.Run) + if (State.RunUnattendedBootLogic()) { // do the upgrade DoUnattendedUpgrade(); // upgrade is done, set reason to Run DetermineRuntimeLevel(); - } // create & initialize the components diff --git a/src/Umbraco.Infrastructure/RuntimeState.cs b/src/Umbraco.Infrastructure/RuntimeState.cs index 6db80bbf34..f362ba7ec3 100644 --- a/src/Umbraco.Infrastructure/RuntimeState.cs +++ b/src/Umbraco.Infrastructure/RuntimeState.cs @@ -236,26 +236,36 @@ namespace Umbraco.Cms.Core { // unattended install is not enabled if (_unattendedSettings.Value.InstallUnattended == false) + { return; + } // no connection string set if (_databaseFactory.Configured == false) + { return; + } - var connect = false; var tries = _globalSettings.Value.InstallMissingDatabase ? 2 : 5; + + bool connect; for (var i = 0; ;) { connect = _databaseFactory.CanConnect; if (connect || ++i == tries) + { break; + } + _logger.LogDebug("Could not immediately connect to database, trying again."); Thread.Sleep(1000); } // could not connect to the database if (connect == false) + { return; + } using (var database = _databaseFactory.CreateDatabase()) { diff --git a/src/Umbraco.Web.BackOffice/Authorization/BackOfficeHandler.cs b/src/Umbraco.Web.BackOffice/Authorization/BackOfficeHandler.cs index 97108f5460..fd7f8eb971 100644 --- a/src/Umbraco.Web.BackOffice/Authorization/BackOfficeHandler.cs +++ b/src/Umbraco.Web.BackOffice/Authorization/BackOfficeHandler.cs @@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Authorization; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; +using Umbraco.Extensions; namespace Umbraco.Cms.Web.BackOffice.Authorization { @@ -30,8 +31,7 @@ namespace Umbraco.Cms.Web.BackOffice.Authorization switch (_runtimeState.Level) { - case RuntimeLevel.Install: - case RuntimeLevel.Upgrade: + case var _ when _runtimeState.EnableInstaller(): return Task.FromResult(true); default: if (!_backOfficeSecurity.BackOfficeSecurity.IsAuthenticated()) diff --git a/src/Umbraco.Web.BackOffice/Install/InstallAreaRoutes.cs b/src/Umbraco.Web.BackOffice/Install/InstallAreaRoutes.cs index 28871db452..6280631c83 100644 --- a/src/Umbraco.Web.BackOffice/Install/InstallAreaRoutes.cs +++ b/src/Umbraco.Web.BackOffice/Install/InstallAreaRoutes.cs @@ -1,4 +1,4 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Routing; using Umbraco.Cms.Core; @@ -29,9 +29,8 @@ namespace Umbraco.Cms.Web.BackOffice.Install switch (_runtime.Level) { - case RuntimeLevel.Install: - case RuntimeLevel.Upgrade: - + case var _ when _runtime.EnableInstaller(): + endpoints.MapUmbracoRoute(installPathSegment, Cms.Core.Constants.Web.Mvc.InstallArea, "api", includeControllerNameInRoute: false); endpoints.MapUmbracoRoute(installPathSegment, Cms.Core.Constants.Web.Mvc.InstallArea, string.Empty, includeControllerNameInRoute: false); diff --git a/src/Umbraco.Web.BackOffice/Install/InstallAuthorizeAttribute.cs b/src/Umbraco.Web.BackOffice/Install/InstallAuthorizeAttribute.cs index ee55f22888..429b204ec6 100644 --- a/src/Umbraco.Web.BackOffice/Install/InstallAuthorizeAttribute.cs +++ b/src/Umbraco.Web.BackOffice/Install/InstallAuthorizeAttribute.cs @@ -1,9 +1,10 @@ -using System; +using System; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Services; +using Umbraco.Extensions; namespace Umbraco.Cms.Web.BackOffice.Install { @@ -44,8 +45,7 @@ namespace Umbraco.Cms.Web.BackOffice.Install { // if not configured (install or upgrade) then we can continue // otherwise we need to ensure that a user is logged in - return _runtimeState.Level == RuntimeLevel.Install - || _runtimeState.Level == RuntimeLevel.Upgrade + return _runtimeState.EnableInstaller() || (authorizationFilterContext.HttpContext.User?.Identity?.IsAuthenticated ?? false); } catch (Exception ex) diff --git a/src/Umbraco.Web.BackOffice/Routing/BackOfficeAreaRoutes.cs b/src/Umbraco.Web.BackOffice/Routing/BackOfficeAreaRoutes.cs index 13d57ccc43..3f171d6439 100644 --- a/src/Umbraco.Web.BackOffice/Routing/BackOfficeAreaRoutes.cs +++ b/src/Umbraco.Web.BackOffice/Routing/BackOfficeAreaRoutes.cs @@ -48,6 +48,7 @@ namespace Umbraco.Cms.Web.BackOffice.Routing { case RuntimeLevel.Install: case RuntimeLevel.Upgrade: + case RuntimeLevel.PackageMigrations: case RuntimeLevel.Run: MapMinimalBackOffice(endpoints); diff --git a/src/Umbraco.Web.BackOffice/Routing/PreviewRoutes.cs b/src/Umbraco.Web.BackOffice/Routing/PreviewRoutes.cs index bda08d0d87..15012728d9 100644 --- a/src/Umbraco.Web.BackOffice/Routing/PreviewRoutes.cs +++ b/src/Umbraco.Web.BackOffice/Routing/PreviewRoutes.cs @@ -36,6 +36,7 @@ namespace Umbraco.Cms.Web.BackOffice.Routing { case RuntimeLevel.Install: case RuntimeLevel.Upgrade: + case RuntimeLevel.PackageMigrations: case RuntimeLevel.Run: endpoints.MapHub(GetPreviewHubRoute()); endpoints.MapUmbracoRoute(_umbracoPathSegment, Constants.Web.Mvc.BackOfficeArea, null); diff --git a/src/Umbraco.Web.Common/Profiler/WebProfiler.cs b/src/Umbraco.Web.Common/Profiler/WebProfiler.cs index caf0b2fae0..7c5a89fa71 100644 --- a/src/Umbraco.Web.Common/Profiler/WebProfiler.cs +++ b/src/Umbraco.Web.Common/Profiler/WebProfiler.cs @@ -42,7 +42,7 @@ namespace Umbraco.Cms.Web.Common.Profiler public void UmbracoApplicationBeginRequest(HttpContext context, RuntimeLevel runtimeLevel) { - if (runtimeLevel < RuntimeLevel.Run) + if (runtimeLevel != RuntimeLevel.Run) { return; } @@ -55,7 +55,7 @@ namespace Umbraco.Cms.Web.Common.Profiler public void UmbracoApplicationEndRequest(HttpContext context, RuntimeLevel runtimeLevel) { - if (runtimeLevel < RuntimeLevel.Run) + if (runtimeLevel != RuntimeLevel.Run) { return; } From 26515117b0e281aa4d2c8ab2ee3125d8de8010c3 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Wed, 9 Jun 2021 08:48:32 +0200 Subject: [PATCH 06/11] Fix build issue after merge.. --- src/Umbraco.Infrastructure/RuntimeState.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Infrastructure/RuntimeState.cs b/src/Umbraco.Infrastructure/RuntimeState.cs index 504a7b946d..52313ed8a4 100644 --- a/src/Umbraco.Infrastructure/RuntimeState.cs +++ b/src/Umbraco.Infrastructure/RuntimeState.cs @@ -207,9 +207,9 @@ namespace Umbraco.Cms.Core // TODO: Can we save the result of this since we'll need to re-use it? IReadOnlyList packagesRequiringMigration = DoesUmbracoRequirePackageMigrations(keyValues); if (packagesRequiringMigration.Count > 0) - { + { return UmbracoDatabaseState.NeedsPackageMigration; - } + } } return UmbracoDatabaseState.Ok; @@ -321,7 +321,7 @@ namespace Umbraco.Cms.Core return CurrentMigrationState != FinalMigrationState; } - private IReadOnlyList DoesUmbracoRequirePackageMigrations(IReadOnlyDictionary keyValues)r + private IReadOnlyList DoesUmbracoRequirePackageMigrations(IReadOnlyDictionary keyValues) { var packageMigrationPlans = _packageMigrationPlans.ToList(); From 36baeb06159c0f087ad48b0cfbe36c9400ab39ea Mon Sep 17 00:00:00 2001 From: Shannon Deminick Date: Thu, 10 Jun 2021 00:53:11 +1000 Subject: [PATCH 07/11] Update src/Umbraco.Core/Persistence/Repositories/IKeyValueRepository.cs Co-authored-by: Bjarke Berg --- .../Persistence/Repositories/IKeyValueRepository.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/IKeyValueRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IKeyValueRepository.cs index 9d5119898c..0b0f9193fa 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IKeyValueRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IKeyValueRepository.cs @@ -10,6 +10,6 @@ namespace Umbraco.Cms.Core.Persistence.Repositories /// /// /// - IReadOnlyDictionary Find(string keyPrefix); + IReadOnlyDictionary FindByKeyPrefix(string keyPrefix); } } From c7c2b1dfd45c8c5a6d0a71cb025b828487f6f6b4 Mon Sep 17 00:00:00 2001 From: Shannon Deminick Date: Thu, 10 Jun 2021 00:53:17 +1000 Subject: [PATCH 08/11] Update src/Umbraco.Core/Services/IKeyValueService.cs Co-authored-by: Bjarke Berg --- src/Umbraco.Core/Services/IKeyValueService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Services/IKeyValueService.cs b/src/Umbraco.Core/Services/IKeyValueService.cs index 456733094c..82fddedad3 100644 --- a/src/Umbraco.Core/Services/IKeyValueService.cs +++ b/src/Umbraco.Core/Services/IKeyValueService.cs @@ -19,7 +19,7 @@ namespace Umbraco.Cms.Core.Services /// /// /// - IReadOnlyDictionary Find(string keyPrefix); + IReadOnlyDictionary FindByKeyPrefix(string keyPrefix); /// /// Sets a value. From 0dfbbedefa4fbdc41cdbc7a5c8c1bd0e4e549268 Mon Sep 17 00:00:00 2001 From: Shannon Deminick Date: Thu, 10 Jun 2021 00:53:35 +1000 Subject: [PATCH 09/11] Update src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/KeyValueServiceTests.cs Co-authored-by: Bjarke Berg --- .../Umbraco.Infrastructure/Services/KeyValueServiceTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/KeyValueServiceTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/KeyValueServiceTests.cs index 0aa1892390..28ccb53adf 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/KeyValueServiceTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/KeyValueServiceTests.cs @@ -28,7 +28,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services KeyValueService.SetValue("test2", "hello2"); KeyValueService.SetValue("test3", "hello3"); KeyValueService.SetValue("test4", "hello4"); - + KeyValueService.SetValue("someotherprefix1", "helloagain1"); // Act IReadOnlyDictionary value = KeyValueService.Find("test"); From 99ff3552c2453e2490c9d87398e081609895d9eb Mon Sep 17 00:00:00 2001 From: Shannon Deminick Date: Thu, 10 Jun 2021 00:55:03 +1000 Subject: [PATCH 10/11] Update src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/KeyValueServiceTests.cs --- .../Umbraco.Infrastructure/Services/KeyValueServiceTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/KeyValueServiceTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/KeyValueServiceTests.cs index 28ccb53adf..eed30c19cf 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/KeyValueServiceTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/KeyValueServiceTests.cs @@ -30,7 +30,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services KeyValueService.SetValue("test4", "hello4"); KeyValueService.SetValue("someotherprefix1", "helloagain1"); // Act - IReadOnlyDictionary value = KeyValueService.Find("test"); + IReadOnlyDictionary value = KeyValueService.FindByKeyPrefix("test"); // Assert From 436fe29bf81b0b70251dd1f76f34008c59bee81c Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 11 Jun 2021 11:14:43 -0600 Subject: [PATCH 11/11] fixing build --- .../Persistence/Repositories/Implement/KeyValueRepository.cs | 2 +- .../Services/Implement/KeyValueService.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/KeyValueRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/KeyValueRepository.cs index a4ffe7ee9a..288f480ed1 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/KeyValueRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/KeyValueRepository.cs @@ -21,7 +21,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement { } /// - public IReadOnlyDictionary Find(string keyPrefix) + public IReadOnlyDictionary FindByKeyPrefix(string keyPrefix) => Get(Query().Where(entity => entity.Identifier.StartsWith(keyPrefix))) .ToDictionary(x => x.Identifier, x => x.Value); diff --git a/src/Umbraco.Infrastructure/Services/Implement/KeyValueService.cs b/src/Umbraco.Infrastructure/Services/Implement/KeyValueService.cs index 73bdbdc7e8..7fda83a427 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/KeyValueService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/KeyValueService.cs @@ -27,11 +27,11 @@ namespace Umbraco.Cms.Core.Services.Implement } /// - public IReadOnlyDictionary Find(string keyPrefix) + public IReadOnlyDictionary FindByKeyPrefix(string keyPrefix) { using (var scope = _scopeProvider.CreateScope(autoComplete: true)) { - return _repository.Find(keyPrefix); + return _repository.FindByKeyPrefix(keyPrefix); } }