diff --git a/src/Umbraco.Core/Migrations/MigrationPlan.cs b/src/Umbraco.Core/Migrations/MigrationPlan.cs
index 68402de85f..49edf6cc05 100644
--- a/src/Umbraco.Core/Migrations/MigrationPlan.cs
+++ b/src/Umbraco.Core/Migrations/MigrationPlan.cs
@@ -4,6 +4,7 @@ using System.Linq;
using Umbraco.Core.Exceptions;
using Umbraco.Core.Logging;
using Umbraco.Core.Scoping;
+using Type = System.Type;
namespace Umbraco.Core.Migrations
{
@@ -31,6 +32,11 @@ namespace Umbraco.Core.Migrations
DefinePlan();
}
+ ///
+ /// Gets the transitions.
+ ///
+ public IReadOnlyDictionary Transitions => _transitions;
+
///
/// Defines the plan.
///
@@ -238,8 +244,8 @@ namespace Umbraco.Core.Migrations
{
Validate();
- if (migrationBuilder == null || logger == null)
- throw new InvalidOperationException("Cannot execute a non-executable plan.");
+ if (migrationBuilder == null) throw new ArgumentNullException(nameof(migrationBuilder));
+ if (logger == null) throw new ArgumentNullException(nameof(logger));
logger.Info("Starting '{MigrationName}'...", Name);
@@ -275,10 +281,47 @@ namespace Umbraco.Core.Migrations
return origState;
}
+ ///
+ /// Follows a path (for tests and debugging).
+ ///
+ /// Does the same thing Execute does, but does not actually execute migrations.
+ internal string FollowPath(string fromState, string toState = null)
+ {
+ toState = toState.NullOrWhiteSpaceAsNull();
+
+ Validate();
+
+ var origState = fromState ?? string.Empty;
+
+ if (!_transitions.TryGetValue(origState, out var transition))
+ throw new Exception($"Unknown state \"{origState}\".");
+
+ while (transition != null)
+ {
+ var nextState = transition.TargetState;
+ origState = nextState;
+
+ if (nextState == toState)
+ {
+ transition = null;
+ continue;
+ }
+
+ if (!_transitions.TryGetValue(origState, out transition))
+ throw new Exception($"Unknown state \"{origState}\".");
+ }
+
+ // safety check
+ if (origState != (toState ?? _finalState))
+ throw new Exception($"Internal error, reached state {origState} which is not state {toState ?? _finalState}");
+
+ return origState;
+ }
+
///
/// Represents a plan transition.
///
- private class Transition
+ public class Transition
{
///
/// Initializes a new instance of the class.
diff --git a/src/Umbraco.Core/Migrations/Upgrade/Upgrader.cs b/src/Umbraco.Core/Migrations/Upgrade/Upgrader.cs
index 4b966a5d80..3795ed79af 100644
--- a/src/Umbraco.Core/Migrations/Upgrade/Upgrader.cs
+++ b/src/Umbraco.Core/Migrations/Upgrade/Upgrader.cs
@@ -41,6 +41,11 @@ namespace Umbraco.Core.Migrations.Upgrade
/// A logger.
public void Execute(IScopeProvider scopeProvider, IMigrationBuilder migrationBuilder, IKeyValueService keyValueService, ILogger logger)
{
+ 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())
diff --git a/src/Umbraco.Tests/Migrations/MigrationPlanTests.cs b/src/Umbraco.Tests/Migrations/MigrationPlanTests.cs
index d6ac7abff4..9aec42c252 100644
--- a/src/Umbraco.Tests/Migrations/MigrationPlanTests.cs
+++ b/src/Umbraco.Tests/Migrations/MigrationPlanTests.cs
@@ -1,7 +1,9 @@
using System;
+using System.Linq;
using Moq;
using NPoco;
using NUnit.Framework;
+using Umbraco.Core;
using Umbraco.Core.Logging;
using Umbraco.Core.Migrations;
using Umbraco.Core.Migrations.Upgrade;
@@ -141,6 +143,40 @@ namespace Umbraco.Tests.Migrations
Assert.IsFalse(string.IsNullOrWhiteSpace(plan.FinalState));
}
+ [Test]
+ public void CanCopyChain()
+ {
+ var plan = new MigrationPlan("default");
+ plan
+ .From(string.Empty)
+ .To("aaa")
+ .To("bbb")
+ .To("ccc")
+ .To("ddd")
+ .To("eee");
+
+ plan
+ .From("xxx")
+ .To("yyy", "bbb", "ddd")
+ .To("eee");
+
+ WritePlanToConsole(plan);
+
+ plan.Validate();
+ Assert.AreEqual("eee", plan.FollowPath("xxx"));
+ Assert.AreEqual("yyy", plan.FollowPath("xxx", "yyy"));
+ }
+
+ private void WritePlanToConsole(MigrationPlan plan)
+ {
+ var final = plan.Transitions.First(x => x.Value == null).Key;
+
+ Console.WriteLine("plan \"{0}\" to final state \"{1}\":", plan.Name, final);
+ foreach (var (_, transition) in plan.Transitions)
+ if (transition != null)
+ Console.WriteLine(transition);
+ }
+
public class DeleteRedirectUrlTable : MigrationBase
{
public DeleteRedirectUrlTable(IMigrationContext context)