diff --git a/src/Core/Grand.Infrastructure/Migrations/IMigrationVersionStamp.cs b/src/Core/Grand.Infrastructure/Migrations/IMigrationVersionStamp.cs
new file mode 100644
index 000000000..7be0ccfa6
--- /dev/null
+++ b/src/Core/Grand.Infrastructure/Migrations/IMigrationVersionStamp.cs
@@ -0,0 +1,12 @@
+namespace Grand.Infrastructure.Migrations;
+
+///
+/// Marks the migration that records a database version as reached.
+///
+///
+/// A version stamp must be the last migration to run for its , and it must only
+/// run when every other migration of that version succeeded.
+/// filters by version, so a version recorded before its own migrations completed makes them unreachable forever —
+/// they compare equal to the installed version and are never selected again.
+///
+public interface IMigrationVersionStamp : IMigration;
diff --git a/src/Core/Grand.Infrastructure/Migrations/MigrationManager.cs b/src/Core/Grand.Infrastructure/Migrations/MigrationManager.cs
index 5c4c9c59f..03b29024e 100644
--- a/src/Core/Grand.Infrastructure/Migrations/MigrationManager.cs
+++ b/src/Core/Grand.Infrastructure/Migrations/MigrationManager.cs
@@ -33,7 +33,10 @@ public IEnumerable GetCurrentMigrations(DbVersion installedVersion)
{
return GetAllMigrations()
.Where(x => x.Version.CompareTo(installedVersion) > 0)
- .OrderBy(mg => mg.Version.ToString())
+ //DbVersion, not its string form - "2.10" sorts before "2.2" as text
+ .OrderBy(mg => mg.Version)
+ //the version stamp closes its version, whatever priority it declares
+ .ThenBy(mg => mg is IMigrationVersionStamp ? 1 : 0)
.ThenBy(mg => mg.Priority)
.ToList();
}
diff --git a/src/Modules/Grand.Module.Migration/Migrations/1.1/MigrationUpgradeDbVersion_11.cs b/src/Modules/Grand.Module.Migration/Migrations/1.1/MigrationUpgradeDbVersion_11.cs
index 64881c80b..7af20c346 100644
--- a/src/Modules/Grand.Module.Migration/Migrations/1.1/MigrationUpgradeDbVersion_11.cs
+++ b/src/Modules/Grand.Module.Migration/Migrations/1.1/MigrationUpgradeDbVersion_11.cs
@@ -1,35 +1,10 @@
-using Grand.Data;
-using Grand.Domain.Common;
-using Grand.Infrastructure;
using Grand.Infrastructure.Migrations;
-using Microsoft.Extensions.DependencyInjection;
namespace Grand.Module.Migration.Migrations._1._1;
-public class MigrationUpgradeDbVersion_11 : IMigration
+public class MigrationUpgradeDbVersion_11 : MigrationUpgradeDbVersionBase
{
- public int Priority => 0;
+ public override DbVersion Version => new(1, 1);
- public DbVersion Version => new(1, 1);
-
- public Guid Identity => new("6BDB7093-4C31-4D78-9604-58188DF728D3");
-
- public string Name => "Upgrade version of the database to 1.1";
-
- ///
- /// Upgrade process
- ///
- ///
- ///
- ///
- public bool UpgradeProcess(IServiceProvider serviceProvider)
- {
- var repository = serviceProvider.GetRequiredService>();
-
- var dbversion = repository.Table.FirstOrDefault();
- dbversion!.DataBaseVersion = $"{GrandVersion.SupportedDBVersion}";
- repository.Update(dbversion);
-
- return true;
- }
-}
\ No newline at end of file
+ public override Guid Identity => new("6BDB7093-4C31-4D78-9604-58188DF728D3");
+}
diff --git a/src/Modules/Grand.Module.Migration/Migrations/2.0/MigrationUpgradeDbVersion_20.cs b/src/Modules/Grand.Module.Migration/Migrations/2.0/MigrationUpgradeDbVersion_20.cs
index 90cca5034..5ffd22f5c 100644
--- a/src/Modules/Grand.Module.Migration/Migrations/2.0/MigrationUpgradeDbVersion_20.cs
+++ b/src/Modules/Grand.Module.Migration/Migrations/2.0/MigrationUpgradeDbVersion_20.cs
@@ -1,35 +1,10 @@
-using Grand.Data;
-using Grand.Domain.Common;
-using Grand.Infrastructure;
using Grand.Infrastructure.Migrations;
-using Microsoft.Extensions.DependencyInjection;
namespace Grand.Module.Migration.Migrations._2._0;
-public class MigrationUpgradeDbVersion_20 : IMigration
+public class MigrationUpgradeDbVersion_20 : MigrationUpgradeDbVersionBase
{
- public int Priority => 0;
+ public override DbVersion Version => new(2, 0);
- public DbVersion Version => new(2, 0);
-
- public Guid Identity => new("AEC3CF1F-4443-474A-B932-4F91D08C8F61");
-
- public string Name => "Upgrade version of the database to 2.0";
-
- ///
- /// Upgrade process
- ///
- ///
- ///
- ///
- public bool UpgradeProcess(IServiceProvider serviceProvider)
- {
- var repository = serviceProvider.GetRequiredService>();
-
- var dbversion = repository.Table.FirstOrDefault();
- dbversion!.DataBaseVersion = $"{GrandVersion.SupportedDBVersion}";
- repository.Update(dbversion);
-
- return true;
- }
-}
\ No newline at end of file
+ public override Guid Identity => new("AEC3CF1F-4443-474A-B932-4F91D08C8F61");
+}
diff --git a/src/Modules/Grand.Module.Migration/Migrations/2.1/MigrationUpgradeDbVersion_21.cs b/src/Modules/Grand.Module.Migration/Migrations/2.1/MigrationUpgradeDbVersion_21.cs
index f17ef9e4a..e67fb3a23 100644
--- a/src/Modules/Grand.Module.Migration/Migrations/2.1/MigrationUpgradeDbVersion_21.cs
+++ b/src/Modules/Grand.Module.Migration/Migrations/2.1/MigrationUpgradeDbVersion_21.cs
@@ -1,35 +1,10 @@
-using Grand.Data;
-using Grand.Domain.Common;
-using Grand.Infrastructure;
using Grand.Infrastructure.Migrations;
-using Microsoft.Extensions.DependencyInjection;
namespace Grand.Module.Migration.Migrations._2._1;
-public class MigrationUpgradeDbVersion_21 : IMigration
+public class MigrationUpgradeDbVersion_21 : MigrationUpgradeDbVersionBase
{
- public int Priority => 0;
+ public override DbVersion Version => new(2, 1);
- public DbVersion Version => new(2, 1);
-
- public Guid Identity => new("EA674DAA-66B2-4F21-9C68-008ACE752FBD");
-
- public string Name => "Upgrade version of the database to 2.1";
-
- ///
- /// Upgrade process
- ///
- ///
- ///
- ///
- public bool UpgradeProcess(IServiceProvider serviceProvider)
- {
- var repository = serviceProvider.GetRequiredService>();
-
- var dbversion = repository.Table.FirstOrDefault();
- dbversion!.DataBaseVersion = $"{GrandVersion.SupportedDBVersion}";
- repository.Update(dbversion);
-
- return true;
- }
-}
\ No newline at end of file
+ public override Guid Identity => new("EA674DAA-66B2-4F21-9C68-008ACE752FBD");
+}
diff --git a/src/Modules/Grand.Module.Migration/Migrations/2.2/MigrationUpgradeDbVersion_22.cs b/src/Modules/Grand.Module.Migration/Migrations/2.2/MigrationUpgradeDbVersion_22.cs
index 03a3c60a5..458a556d1 100644
--- a/src/Modules/Grand.Module.Migration/Migrations/2.2/MigrationUpgradeDbVersion_22.cs
+++ b/src/Modules/Grand.Module.Migration/Migrations/2.2/MigrationUpgradeDbVersion_22.cs
@@ -1,35 +1,10 @@
-using Grand.Data;
-using Grand.Domain.Common;
-using Grand.Infrastructure;
using Grand.Infrastructure.Migrations;
-using Microsoft.Extensions.DependencyInjection;
namespace Grand.Module.Migration.Migrations._2._2;
-public class MigrationUpgradeDbVersion_22 : IMigration
+public class MigrationUpgradeDbVersion_22 : MigrationUpgradeDbVersionBase
{
- public int Priority => 0;
+ public override DbVersion Version => new(2, 2);
- public DbVersion Version => new(2, 2);
-
- public Guid Identity => new("9B9FD138-7E67-44AA-913B-273F3D5B5DE9");
-
- public string Name => "Upgrade version of the database to 2.2";
-
- ///
- /// Upgrade process
- ///
- ///
- ///
- ///
- public bool UpgradeProcess(IServiceProvider serviceProvider)
- {
- var repository = serviceProvider.GetRequiredService>();
-
- var dbversion = repository.Table.FirstOrDefault();
- dbversion!.DataBaseVersion = $"{GrandVersion.SupportedDBVersion}";
- repository.Update(dbversion);
-
- return true;
- }
-}
\ No newline at end of file
+ public override Guid Identity => new("9B9FD138-7E67-44AA-913B-273F3D5B5DE9");
+}
diff --git a/src/Modules/Grand.Module.Migration/Migrations/2.3/MigrationUpgradeDbVersion_23.cs b/src/Modules/Grand.Module.Migration/Migrations/2.3/MigrationUpgradeDbVersion_23.cs
index d9ddae27e..072f8d51e 100644
--- a/src/Modules/Grand.Module.Migration/Migrations/2.3/MigrationUpgradeDbVersion_23.cs
+++ b/src/Modules/Grand.Module.Migration/Migrations/2.3/MigrationUpgradeDbVersion_23.cs
@@ -1,36 +1,10 @@
-using Grand.Data;
-using Grand.Domain.Common;
-using Grand.Infrastructure;
using Grand.Infrastructure.Migrations;
-using Microsoft.Extensions.DependencyInjection;
namespace Grand.Module.Migration.Migrations._2._3;
-public class MigrationUpgradeDbVersion_23 : IMigration
+public class MigrationUpgradeDbVersion_23 : MigrationUpgradeDbVersionBase
{
- public int Priority => 0;
+ public override DbVersion Version => new(2, 3);
- public DbVersion Version => new(2, 3);
-
- public Guid Identity => new("689E5BFA-7229-41A5-AF48-07CB58C0D608");
-
- public string Name => "Upgrade version of the database to 2.3";
-
- ///
- /// Upgrade process
- ///
- ///
- ///
- ///
- public bool UpgradeProcess(IServiceProvider serviceProvider)
- {
- var repository = serviceProvider.GetRequiredService>();
-
- var dbversion = repository.Table.FirstOrDefault();
- dbversion!.InstalledVersion = $"{GrandVersion.SupportedDBVersion}";
- dbversion!.DataBaseVersion = $"{GrandVersion.SupportedDBVersion}";
- repository.Update(dbversion);
-
- return true;
- }
-}
\ No newline at end of file
+ public override Guid Identity => new("689E5BFA-7229-41A5-AF48-07CB58C0D608");
+}
diff --git a/src/Modules/Grand.Module.Migration/Migrations/2.4/MigrationUpgradeDbVersion_24.cs b/src/Modules/Grand.Module.Migration/Migrations/2.4/MigrationUpgradeDbVersion_24.cs
index e4a86d7d1..bfc0a41ff 100644
--- a/src/Modules/Grand.Module.Migration/Migrations/2.4/MigrationUpgradeDbVersion_24.cs
+++ b/src/Modules/Grand.Module.Migration/Migrations/2.4/MigrationUpgradeDbVersion_24.cs
@@ -1,35 +1,10 @@
-using Grand.Data;
-using Grand.Domain.Common;
-using Grand.Infrastructure;
using Grand.Infrastructure.Migrations;
-using Microsoft.Extensions.DependencyInjection;
namespace Grand.Module.Migration.Migrations._2._4;
-public class MigrationUpgradeDbVersion_24 : IMigration
+public class MigrationUpgradeDbVersion_24 : MigrationUpgradeDbVersionBase
{
- public int Priority => 0;
+ public override DbVersion Version => new(2, 4);
- public DbVersion Version => new(2, 4);
-
- public Guid Identity => new("A1B2C3D4-E5F6-7890-ABCD-EF1234567890");
-
- public string Name => "Upgrade version of the database to 2.4";
-
- ///
- /// Upgrade process
- ///
- ///
- ///
- public bool UpgradeProcess(IServiceProvider serviceProvider)
- {
- var repository = serviceProvider.GetRequiredService>();
-
- var dbversion = repository.Table.FirstOrDefault();
- dbversion!.InstalledVersion = $"{GrandVersion.SupportedDBVersion}";
- dbversion!.DataBaseVersion = $"{GrandVersion.SupportedDBVersion}";
- repository.Update(dbversion);
-
- return true;
- }
+ public override Guid Identity => new("A1B2C3D4-E5F6-7890-ABCD-EF1234567890");
}
diff --git a/src/Modules/Grand.Module.Migration/Migrations/MigrationProcess.cs b/src/Modules/Grand.Module.Migration/Migrations/MigrationProcess.cs
index dfe9c79bd..9237b0047 100644
--- a/src/Modules/Grand.Module.Migration/Migrations/MigrationProcess.cs
+++ b/src/Modules/Grand.Module.Migration/Migrations/MigrationProcess.cs
@@ -57,14 +57,45 @@ public virtual void RunMigrationProcess()
{
var migrationsDb = GetMigrationDb();
var version = _repositoryVersion.Table.FirstOrDefault();
- if(version == null)
+ if (version == null)
return;
- var majorVersion = string.IsNullOrEmpty(version?.InstalledVersion) ? int.Parse(version?.DataBaseVersion.Split('.')[0]!) : int.Parse(version?.InstalledVersion.Split('.')[0]!);
- var minorVersion = string.IsNullOrEmpty(version?.InstalledVersion) ? int.Parse(version?.DataBaseVersion.Split('.')[1]!) : int.Parse(version?.InstalledVersion.Split('.')[1]!);
+
+ var installedVersion = ParseDbVersion(string.IsNullOrEmpty(version.InstalledVersion)
+ ? version.DataBaseVersion
+ : version.InstalledVersion);
+
+ if (installedVersion == null)
+ {
+ _logger.LogError("Cannot read the installed database version - migration process skipped");
+ return;
+ }
+
var migrationManager = new MigrationManager();
- foreach (var item in migrationManager.GetCurrentMigrations(new DbVersion(majorVersion, minorVersion)))
- if (migrationsDb.FirstOrDefault(x => x.Identity == item.Identity) == null)
- RunProcess(item);
+ foreach (var item in migrationManager.GetCurrentMigrations(installedVersion))
+ {
+ if (migrationsDb.Any(x => x.Identity == item.Identity))
+ continue;
+
+ if (RunProcess(item).Success) continue;
+
+ //stop before the version stamp of this version runs - recording a version whose
+ //migrations did not all succeed puts them below the installed version, and
+ //GetCurrentMigrations never selects them again
+ _logger.LogError("Migration process stopped at {MigrationName} ({Version}) - it will be retried on the next start",
+ item.Name, item.Version);
+ return;
+ }
+ }
+
+ private static DbVersion? ParseDbVersion(string? version)
+ {
+ var parts = version?.Split('.');
+ if (parts is not { Length: >= 2 })
+ return null;
+
+ return int.TryParse(parts[0], out var major) && int.TryParse(parts[1], out var minor)
+ ? new DbVersion(major, minor)
+ : null;
}
private MigrationResult RunProcessInternal(IMigration migration)
diff --git a/src/Modules/Grand.Module.Migration/Migrations/MigrationUpgradeDbVersionBase.cs b/src/Modules/Grand.Module.Migration/Migrations/MigrationUpgradeDbVersionBase.cs
new file mode 100644
index 000000000..ba1a6a83d
--- /dev/null
+++ b/src/Modules/Grand.Module.Migration/Migrations/MigrationUpgradeDbVersionBase.cs
@@ -0,0 +1,45 @@
+using Grand.Data;
+using Grand.Domain.Common;
+using Grand.Infrastructure.Migrations;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Grand.Module.Migration.Migrations;
+
+///
+/// Records as the version the database has reached.
+///
+///
+/// Runs last within its version - see .
+///
+public abstract class MigrationUpgradeDbVersionBase : IMigrationVersionStamp
+{
+ public int Priority => 0;
+
+ public abstract DbVersion Version { get; }
+
+ public abstract Guid Identity { get; }
+
+ public string Name => $"Upgrade version of the database to {Version}";
+
+ ///
+ /// Upgrade process
+ ///
+ ///
+ ///
+ public bool UpgradeProcess(IServiceProvider serviceProvider)
+ {
+ var repository = serviceProvider.GetRequiredService>();
+
+ var dbversion = repository.Table.FirstOrDefault();
+ if (dbversion == null)
+ return false;
+
+ //stamp the version this migration closes, not the version the build supports - stamping
+ //the latest supported version here makes every migration in between unreachable
+ dbversion.InstalledVersion = Version.ToString();
+ dbversion.DataBaseVersion = Version.ToString();
+ repository.Update(dbversion);
+
+ return true;
+ }
+}
diff --git a/src/Tests/Grand.Modules.Tests/Services/Migrations/MigrationManagerTests.cs b/src/Tests/Grand.Modules.Tests/Services/Migrations/MigrationManagerTests.cs
index a0139e364..9352077e7 100644
--- a/src/Tests/Grand.Modules.Tests/Services/Migrations/MigrationManagerTests.cs
+++ b/src/Tests/Grand.Modules.Tests/Services/Migrations/MigrationManagerTests.cs
@@ -1,16 +1,37 @@
-using Grand.Infrastructure.Migrations;
+using Grand.Data;
+using Grand.Domain.Common;
+using Grand.Infrastructure.Migrations;
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
namespace Grand.Modules.Tests.Services.Migrations;
[TestClass]
public class MigrationManagerTests
{
+ ///
+ /// MigrationManager scans loaded assemblies, and a project reference alone does not load one.
+ /// Holding the assembly in a field keeps it loaded before the first scan.
+ ///
+ private static readonly System.Reflection.Assembly MigrationsAssembly =
+ typeof(Grand.Module.Migration.Migrations.MigrationUpgradeDbVersionBase).Assembly;
+
private MigrationManager _migrationManager;
+ ///
+ /// Every migration that ships, from the oldest supported database version upwards.
+ ///
+ private IList AllShippedMigrations =>
+ _migrationManager.GetCurrentMigrations(new DbVersion(1, 0))
+ //test fixtures in this assembly also implement IMigration - keep them out
+ .Where(x => x.GetType().Assembly == MigrationsAssembly)
+ .ToList();
+
[TestInitialize]
public void Init()
{
+ Assert.IsNotNull(MigrationsAssembly);
_migrationManager = new MigrationManager();
}
@@ -20,4 +41,66 @@ public void GetCurrentMigrations_Exists()
var migrations = _migrationManager.GetCurrentMigrations(new DbVersion(2, 2));
Assert.IsNotEmpty(migrations);
}
-}
\ No newline at end of file
+
+ [TestMethod]
+ public void GetCurrentMigrations_OrderedByAscendingVersion()
+ {
+ var migrations = AllShippedMigrations;
+
+ for (var i = 1; i < migrations.Count; i++)
+ Assert.IsTrue(migrations[i].Version.CompareTo(migrations[i - 1].Version) >= 0,
+ $"{migrations[i].Name} ({migrations[i].Version}) runs after {migrations[i - 1].Name} ({migrations[i - 1].Version})");
+ }
+
+ [TestMethod]
+ public void GetCurrentMigrations_VersionStampIsLastWithinItsVersion()
+ {
+ var migrations = AllShippedMigrations;
+
+ foreach (var group in migrations.GroupBy(x => x.Version.ToString()))
+ {
+ var stamp = group.OfType().SingleOrDefault();
+ if (stamp == null) continue;
+
+ Assert.AreSame(stamp, group.Last(),
+ $"The version stamp for {group.Key} must run last, otherwise the remaining migrations of that version are skipped permanently");
+ }
+ }
+
+ [TestMethod]
+ public void AllMigrations_HaveUniqueIdentity()
+ {
+ var duplicates = AllShippedMigrations
+ .GroupBy(x => x.Identity)
+ .Where(x => x.Count() > 1)
+ .Select(x => x.Key)
+ .ToList();
+
+ Assert.IsEmpty(duplicates, $"Duplicated migration identity: {string.Join(", ", duplicates)}");
+ }
+
+ [TestMethod]
+ public void VersionStamp_RecordsItsOwnVersion()
+ {
+ var stamps = AllShippedMigrations.OfType().ToList();
+ Assert.IsNotEmpty(stamps);
+
+ foreach (var stamp in stamps)
+ {
+ var dbVersion = new GrandNodeVersion { DataBaseVersion = "1.0", InstalledVersion = "1.0" };
+
+ var repository = new Mock>();
+ repository.Setup(x => x.Table).Returns(new List { dbVersion }.AsQueryable());
+
+ var services = new ServiceCollection();
+ services.AddSingleton(repository.Object);
+
+ Assert.IsTrue(stamp.UpgradeProcess(services.BuildServiceProvider()), $"{stamp.Name} failed");
+
+ Assert.AreEqual(stamp.Version.ToString(), dbVersion.DataBaseVersion,
+ $"{stamp.Name} must record its own version, not the version the build supports");
+ Assert.AreEqual(stamp.Version.ToString(), dbVersion.InstalledVersion,
+ $"{stamp.Name} must record its own version in InstalledVersion too - the migration process reads it first");
+ }
+ }
+}