diff --git a/Lager.Tests/Lager.Tests.csproj b/Lager.Tests/Lager.Tests.csproj index 17245fa..13badb9 100644 --- a/Lager.Tests/Lager.Tests.csproj +++ b/Lager.Tests/Lager.Tests.csproj @@ -75,9 +75,13 @@ + + + + diff --git a/Lager.Tests/MigrationTest.cs b/Lager.Tests/MigrationTest.cs new file mode 100644 index 0000000..693a0c5 --- /dev/null +++ b/Lager.Tests/MigrationTest.cs @@ -0,0 +1,102 @@ +using Akavache; +using Moq; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reactive.Linq; +using System.Threading.Tasks; +using Xunit; + +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() + { + 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); + } + + [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); + } + + [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() + { + 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); + } + + [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/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.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.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.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/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..cabbff5 --- /dev/null +++ b/Lager/SettingsMigration.cs @@ -0,0 +1,63 @@ +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)); + } + + 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); + } + + 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.blobCache.InvalidateObject(key); + + await this.blobCache.InsertObject(key, after); + } + + 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..ef3da88 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,29 @@ protected SettingsStorage(string keyPrefix, IBlobCache cache) public event PropertyChangedEventHandler PropertyChanged; + public async Task MigrateAsync(IEnumerable migrations) + { + 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(); + } + + this.cache.Clear(); + } + /// /// Gets the value for the specified key, or, if the value doesn't exist, saves the and returns it. ///