From 95684ea3ca2109c5f07ab7a2e708f15747134521 Mon Sep 17 00:00:00 2001 From: Dennis Daume Date: Tue, 3 Dec 2013 22:49:07 +0100 Subject: [PATCH 1/5] Initial hack on migrations --- Lager.Tests/Lager.Tests.csproj | 2 ++ Lager.Tests/MigrationTest.cs | 21 ++++++++++++++ Lager.Tests/RemoveIntegerMigration.cs | 20 +++++++++++++ Lager/Lager.csproj | 1 + Lager/SettingsMigration.cs | 41 +++++++++++++++++++++++++++ Lager/SettingsStorage.cs | 13 +++++++++ 6 files changed, 98 insertions(+) create mode 100644 Lager.Tests/MigrationTest.cs create mode 100644 Lager.Tests/RemoveIntegerMigration.cs create mode 100644 Lager/SettingsMigration.cs diff --git a/Lager.Tests/Lager.Tests.csproj b/Lager.Tests/Lager.Tests.csproj index 17245fa..008a356 100644 --- a/Lager.Tests/Lager.Tests.csproj +++ b/Lager.Tests/Lager.Tests.csproj @@ -75,6 +75,8 @@ + + diff --git a/Lager.Tests/MigrationTest.cs b/Lager.Tests/MigrationTest.cs new file mode 100644 index 0000000..0578514 --- /dev/null +++ b/Lager.Tests/MigrationTest.cs @@ -0,0 +1,21 @@ +using System.Threading.Tasks; +using Xunit; + +namespace Lager.Tests +{ + public class MigrationTest + { + [Fact] + public async Task RemoveAsyncSmokeTest() + { + var storage = new SettingsStorageProxy(); + storage.SetOrCreateProxy(1, "Setting1"); + + await storage.MigrateAsync(new[] { new RemoveIntegerMigration("Setting1") }); + + int value = storage.GetOrCreateProxy(42, "Setting1"); + + Assert.Equal(42, value); + } + } +} \ No newline at end of file diff --git a/Lager.Tests/RemoveIntegerMigration.cs b/Lager.Tests/RemoveIntegerMigration.cs new file mode 100644 index 0000000..8b20280 --- /dev/null +++ b/Lager.Tests/RemoveIntegerMigration.cs @@ -0,0 +1,20 @@ +using System.Threading.Tasks; + +namespace Lager.Tests +{ + public class RemoveIntegerMigration : SettingsMigration + { + private readonly string key; + + public RemoveIntegerMigration(string key) + : base(1) + { + this.key = key; + } + + public override async Task MigrateAsync() + { + await this.RemoveAsync(key); + } + } +} \ No newline at end of file diff --git a/Lager/Lager.csproj b/Lager/Lager.csproj index d7ed058..c54e8df 100644 --- a/Lager/Lager.csproj +++ b/Lager/Lager.csproj @@ -37,6 +37,7 @@ + diff --git a/Lager/SettingsMigration.cs b/Lager/SettingsMigration.cs new file mode 100644 index 0000000..0d5062c --- /dev/null +++ b/Lager/SettingsMigration.cs @@ -0,0 +1,41 @@ +using Akavache; +using System; +using System.Reactive.Linq; +using System.Threading.Tasks; + +namespace Lager +{ + public abstract class SettingsMigration + { + private IBlobCache blobCache; + private string keyPrefix; + + protected SettingsMigration(int revision) + { + if (revision < 0) + throw new ArgumentOutOfRangeException("revision", "Revision has to be greater or equal 0"); + + this.Revision = revision; + } + + internal int Revision { get; private set; } + + public abstract Task MigrateAsync(); + + internal void Initialize(string keyPrefix, IBlobCache blobCache) + { + this.keyPrefix = keyPrefix; + this.blobCache = blobCache; + } + + protected async Task RemoveAsync(string key) + { + await this.blobCache.InvalidateObject(this.CreateKey(key)); + } + + private string CreateKey(string key) + { + return string.Format("{0}:{1}", this.keyPrefix, key); + } + } +} \ No newline at end of file diff --git a/Lager/SettingsStorage.cs b/Lager/SettingsStorage.cs index 8a86bea..4ccc5d3 100644 --- a/Lager/SettingsStorage.cs +++ b/Lager/SettingsStorage.cs @@ -2,9 +2,11 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Linq; using System.Reactive.Linq; using System.Runtime.CompilerServices; using System.Threading; +using System.Threading.Tasks; namespace Lager { @@ -38,6 +40,17 @@ protected SettingsStorage(string keyPrefix, IBlobCache cache) public event PropertyChangedEventHandler PropertyChanged; + public async Task MigrateAsync(IEnumerable migrations) + { + foreach (SettingsMigration migration in migrations.OrderBy(x => x.Revision)) + { + migration.Initialize(this.keyPrefix, this.blobCache); + await migration.MigrateAsync(); + } + + this.cache.Clear(); + } + /// /// Gets the value for the specified key, or, if the value doesn't exist, saves the and returns it. /// From 97abe9d08a82dc91d67499d4f45f381f5f224825 Mon Sep 17 00:00:00 2001 From: Dennis Daume Date: Tue, 3 Dec 2013 23:24:02 +0100 Subject: [PATCH 2/5] Add a rename migration method --- Lager.Tests/Lager.Tests.csproj | 1 + Lager.Tests/MigrationTest.cs | 13 +++++++++++++ Lager.Tests/RenameIntegerMigration.cs | 22 ++++++++++++++++++++++ Lager/SettingsMigration.cs | 9 +++++++++ 4 files changed, 45 insertions(+) create mode 100644 Lager.Tests/RenameIntegerMigration.cs diff --git a/Lager.Tests/Lager.Tests.csproj b/Lager.Tests/Lager.Tests.csproj index 008a356..3472c1b 100644 --- a/Lager.Tests/Lager.Tests.csproj +++ b/Lager.Tests/Lager.Tests.csproj @@ -78,6 +78,7 @@ + diff --git a/Lager.Tests/MigrationTest.cs b/Lager.Tests/MigrationTest.cs index 0578514..a5abc25 100644 --- a/Lager.Tests/MigrationTest.cs +++ b/Lager.Tests/MigrationTest.cs @@ -17,5 +17,18 @@ public async Task RemoveAsyncSmokeTest() Assert.Equal(42, value); } + + [Fact] + public async Task RenameSmokeTest() + { + var storage = new SettingsStorageProxy(); + storage.SetOrCreateProxy(42, "Setting1"); + + await storage.MigrateAsync(new[] { new RenameIntegerMigration(1, "Setting1", "Setting2") }); + + int value = storage.GetOrCreateProxy(1, "Setting2"); + + Assert.Equal(42, value); + } } } \ No newline at end of file diff --git a/Lager.Tests/RenameIntegerMigration.cs b/Lager.Tests/RenameIntegerMigration.cs new file mode 100644 index 0000000..bda900f --- /dev/null +++ b/Lager.Tests/RenameIntegerMigration.cs @@ -0,0 +1,22 @@ +using System.Threading.Tasks; + +namespace Lager.Tests +{ + public class RenameIntegerMigration : SettingsMigration + { + private readonly string newKey; + private readonly string previousKey; + + public RenameIntegerMigration(int revision, string previousKey, string newKey) + : base(revision) + { + this.previousKey = previousKey; + this.newKey = newKey; + } + + public override async Task MigrateAsync() + { + await this.RenameAsync(this.previousKey, this.newKey); + } + } +} \ No newline at end of file diff --git a/Lager/SettingsMigration.cs b/Lager/SettingsMigration.cs index 0d5062c..5b6b37b 100644 --- a/Lager/SettingsMigration.cs +++ b/Lager/SettingsMigration.cs @@ -33,6 +33,15 @@ protected async Task RemoveAsync(string key) await this.blobCache.InvalidateObject(this.CreateKey(key)); } + protected async Task RenameAsync(string previousKey, string newKey) + { + T value = await this.blobCache.GetObjectAsync(this.CreateKey(previousKey)); + + await this.blobCache.InvalidateObject(this.CreateKey(previousKey)); + + await this.blobCache.InsertObject(this.CreateKey(newKey), value); + } + private string CreateKey(string key) { return string.Format("{0}:{1}", this.keyPrefix, key); From 0aec00252abf67cb7575f694a2e37a0ea9ebdf9c Mon Sep 17 00:00:00 2001 From: Dennis Daume Date: Tue, 3 Dec 2013 23:40:22 +0100 Subject: [PATCH 3/5] Implement transformations --- Lager.Tests/Lager.Tests.csproj | 1 + Lager.Tests/MigrationTest.cs | 13 +++++++++++++ Lager.Tests/TransformationMigration.cs | 23 +++++++++++++++++++++++ Lager/SettingsMigration.cs | 13 +++++++++++++ 4 files changed, 50 insertions(+) create mode 100644 Lager.Tests/TransformationMigration.cs diff --git a/Lager.Tests/Lager.Tests.csproj b/Lager.Tests/Lager.Tests.csproj index 3472c1b..13badb9 100644 --- a/Lager.Tests/Lager.Tests.csproj +++ b/Lager.Tests/Lager.Tests.csproj @@ -81,6 +81,7 @@ + diff --git a/Lager.Tests/MigrationTest.cs b/Lager.Tests/MigrationTest.cs index a5abc25..a1e6f0e 100644 --- a/Lager.Tests/MigrationTest.cs +++ b/Lager.Tests/MigrationTest.cs @@ -30,5 +30,18 @@ public async Task RenameSmokeTest() Assert.Equal(42, value); } + + [Fact] + public async Task TransformationSmokeTest() + { + var storage = new SettingsStorageProxy(); + storage.SetOrCreateProxy(42, "Setting"); + + await storage.MigrateAsync(new[] { new TransformationMigration(1, "Setting", x => x.ToString()) }); + + string value = storage.GetOrCreateProxy("Bla", "Setting"); + + Assert.Equal("42", value); + } } } \ No newline at end of file diff --git a/Lager.Tests/TransformationMigration.cs b/Lager.Tests/TransformationMigration.cs new file mode 100644 index 0000000..5260ba3 --- /dev/null +++ b/Lager.Tests/TransformationMigration.cs @@ -0,0 +1,23 @@ +using System; +using System.Threading.Tasks; + +namespace Lager.Tests +{ + public class TransformationMigration : SettingsMigration + { + private readonly string key; + private readonly Func transformation; + + public TransformationMigration(int revision, string key, Func transformation) + : base(revision) + { + this.key = key; + this.transformation = transformation; + } + + public override async Task MigrateAsync() + { + await this.TransformAsync(key, transformation); + } + } +} \ No newline at end of file diff --git a/Lager/SettingsMigration.cs b/Lager/SettingsMigration.cs index 5b6b37b..6905587 100644 --- a/Lager/SettingsMigration.cs +++ b/Lager/SettingsMigration.cs @@ -42,6 +42,19 @@ protected async Task RenameAsync(string previousKey, string newKey) await this.blobCache.InsertObject(this.CreateKey(newKey), value); } + protected async Task TransformAsync(string key, Func transformation) + { + key = this.CreateKey(key); + + TBefore before = await this.blobCache.GetObjectAsync(key); + + TAfter after = transformation(before); + + await this.RemoveAsync(key); + + await this.blobCache.InsertObject(key, after); + } + private string CreateKey(string key) { return string.Format("{0}:{1}", this.keyPrefix, key); From 5bf5b088b508a589f9b9eae3073b3d34dc2ed901 Mon Sep 17 00:00:00 2001 From: Dennis Daume Date: Tue, 3 Dec 2013 23:49:42 +0100 Subject: [PATCH 4/5] Remove the old key properly --- Lager.Tests/MigrationTest.cs | 17 ++++++++++++++++- Lager/SettingsMigration.cs | 2 +- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/Lager.Tests/MigrationTest.cs b/Lager.Tests/MigrationTest.cs index a1e6f0e..94c3763 100644 --- a/Lager.Tests/MigrationTest.cs +++ b/Lager.Tests/MigrationTest.cs @@ -1,4 +1,7 @@ -using System.Threading.Tasks; +using Akavache; +using System.Collections.Generic; +using System.Reactive.Linq; +using System.Threading.Tasks; using Xunit; namespace Lager.Tests @@ -31,6 +34,18 @@ public async Task RenameSmokeTest() Assert.Equal(42, value); } + [Fact] + public async Task TransformationRemovesOldObject() + { + var blobCache = new TestBlobCache(); + var storage = new SettingsStorageProxy(blobCache); + storage.SetOrCreateProxy(42, "Setting"); + + await storage.MigrateAsync(new[] { new TransformationMigration(1, "Setting", x => x.ToString()) }); + + Assert.Throws(() => blobCache.GetObjectAsync("Setting").Wait()); + } + [Fact] public async Task TransformationSmokeTest() { diff --git a/Lager/SettingsMigration.cs b/Lager/SettingsMigration.cs index 6905587..cabbff5 100644 --- a/Lager/SettingsMigration.cs +++ b/Lager/SettingsMigration.cs @@ -50,7 +50,7 @@ protected async Task TransformAsync(string key, Func(key); + await this.blobCache.InvalidateObject(key); await this.blobCache.InsertObject(key, after); } From 0ab28196b9815f694bf0e928214a8aa3846b624a Mon Sep 17 00:00:00 2001 From: Dennis Daume Date: Wed, 4 Dec 2013 00:47:37 +0100 Subject: [PATCH 5/5] Add some validation --- Lager.Tests/MigrationTest.cs | 40 +++++++++++++++++++++++++++++ Lager.Tests/SettingsStorageProxy.cs | 1 - Lager/SettingsStorage.cs | 14 +++++++++- 3 files changed, 53 insertions(+), 2 deletions(-) diff --git a/Lager.Tests/MigrationTest.cs b/Lager.Tests/MigrationTest.cs index 94c3763..693a0c5 100644 --- a/Lager.Tests/MigrationTest.cs +++ b/Lager.Tests/MigrationTest.cs @@ -1,5 +1,8 @@ using Akavache; +using Moq; +using System; using System.Collections.Generic; +using System.Linq; using System.Reactive.Linq; using System.Threading.Tasks; using Xunit; @@ -8,6 +11,30 @@ namespace Lager.Tests { public class MigrationTest { + public async static Task ThrowsAsync(Func testCode) where T : Exception + { + try + { + await testCode(); + Assert.Throws(() => { }); // Use xUnit's default behavior. + } + + catch (T exception) + { + return exception; + } + + return null; + } + + [Fact] + public async Task MigrateAsyncThrowsOnEmptyEnumerable() + { + var storage = new SettingsStorageProxy(); + + await ThrowsAsync(() => storage.MigrateAsync(Enumerable.Empty())); + } + [Fact] public async Task RemoveAsyncSmokeTest() { @@ -58,5 +85,18 @@ public async Task TransformationSmokeTest() Assert.Equal("42", value); } + + [Fact] + public async Task TransformationsShouldHaveDistinctRevisions() + { + var storage = new SettingsStorageProxy(); + + var migration1 = new Mock(1); + var migration2 = new Mock(1); + + var migrations = new[] { migration1.Object, migration2.Object }; + + await ThrowsAsync(() => storage.MigrateAsync(migrations)); + } } } \ No newline at end of file diff --git a/Lager.Tests/SettingsStorageProxy.cs b/Lager.Tests/SettingsStorageProxy.cs index a37cdd5..0db4afb 100644 --- a/Lager.Tests/SettingsStorageProxy.cs +++ b/Lager.Tests/SettingsStorageProxy.cs @@ -1,5 +1,4 @@ using Akavache; -using Lager; namespace Lager.Tests { diff --git a/Lager/SettingsStorage.cs b/Lager/SettingsStorage.cs index 4ccc5d3..ef3da88 100644 --- a/Lager/SettingsStorage.cs +++ b/Lager/SettingsStorage.cs @@ -42,7 +42,19 @@ protected SettingsStorage(string keyPrefix, IBlobCache cache) public async Task MigrateAsync(IEnumerable migrations) { - foreach (SettingsMigration migration in migrations.OrderBy(x => x.Revision)) + List migrationsList = migrations.ToList(); + + if (!migrationsList.Any()) + throw new ArgumentException("Migration list is empty.", "migrations"); + + bool areRevisionsUnique = migrationsList + .GroupBy(x => x.Revision) + .All(x => x.Count() == 1); + + if (!areRevisionsUnique) + throw new ArgumentException("Migration revisions aren't unique.", "migrations"); + + foreach (SettingsMigration migration in migrationsList.OrderBy(x => x.Revision)) { migration.Initialize(this.keyPrefix, this.blobCache); await migration.MigrateAsync();