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.
///