diff --git a/LocalStorage.Tests/Helpers.cs b/LocalStorage.Tests/Helpers.cs new file mode 100644 index 0000000..9409bf6 --- /dev/null +++ b/LocalStorage.Tests/Helpers.cs @@ -0,0 +1,19 @@ +using System; +using Hanssens.Net; + +namespace LocalStorageTests +{ + internal static class TestHelpers + { + /// + /// Configuration that can be used for initializing a unique LocalStorage instance. + /// + internal static ILocalStorageConfiguration UniqueInstance() + { + return new LocalStorageConfiguration() + { + Filename = Guid.NewGuid().ToString() + }; + } + } +} \ No newline at end of file diff --git a/LocalStorage.Tests/LocalStorage.Tests.csproj b/LocalStorage.Tests/LocalStorage.Tests.csproj index e924767..579929e 100644 --- a/LocalStorage.Tests/LocalStorage.Tests.csproj +++ b/LocalStorage.Tests/LocalStorage.Tests.csproj @@ -1,13 +1,13 @@  - netcoreapp1.0 + netcoreapp2.2 LocalStorageTests - - - - + + + + diff --git a/LocalStorage.Tests/LocalStorageTests.cs b/LocalStorage.Tests/LocalStorageTests.cs index 1c4d169..58c087f 100644 --- a/LocalStorage.Tests/LocalStorageTests.cs +++ b/LocalStorage.Tests/LocalStorageTests.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.IO; using System.Linq; +using FluentAssertions.Common; using Hanssens.Net.Helpers; using Xunit; using LocalStorageTests.Stubs; @@ -126,7 +127,7 @@ public void LocalStorage_Store_Should_Overwrite_Existing_Key() // assert - last stored value should be the truth target.Should().NotBeNull(); - target.ShouldBeEquivalentTo(expected_value); + target.IsSameOrEqualTo(expected_value); } [Fact(DisplayName = "LocalStorage.Clear() should clear all in-memory content")] @@ -236,6 +237,84 @@ public void LocalStorage_Should_Perform_Decently_With_Large_Collections() // assert - make sure the entire operation is done in < 1sec. (psychological boundry, if you will) stopwatch.ElapsedMilliseconds.Should().BeLessOrEqualTo(1000); } + + [Fact(DisplayName = "LocalStorage should perform decently with many iterations collections")] + public void LocalStorage_Should_Perform_Decently_With_Many_Opens_And_Writes() + { + // arrange - iterate a lot of times through open/persist/close + for (var i = 0; i < 1000; i++) + { + var storage = new LocalStorage(); + // storage.Clear(); + storage.Store(Guid.NewGuid().ToString(), i); + storage.Persist(); + } + + // cleanup + var store = new LocalStorage(); + store.Destroy(); + } + + [Fact(DisplayName = "LocalStorage.Exists() should locate existing key")] + public void LocalStorage_Exists_Should_Locate_Existing_Key() + { + // arrange + var storage = new LocalStorage(); + var expected_key = Guid.NewGuid().ToString(); + storage.Store(expected_key, Guid.NewGuid().ToString()); + + // act + var target = storage.Exists(expected_key); + + // assert + target.Should().BeTrue(); + } + + [Fact(DisplayName = "LocalStorage.Exists() should ignore non-existing key")] + public void LocalStorage_Exists_Should_Ignore_NonExisting_Key() + { + // arrange + var storage = new LocalStorage(); + var nonexisting_key = Guid.NewGuid().ToString(); + + // act + var target = storage.Exists(nonexisting_key); + + // assert + target.Should().BeFalse(); + } + + [Fact(DisplayName = "LocalStorage.Keys() should return collection of all keys")] + public void LocalStorage_Keys_Should_Return_Collection_Of_Keys() + { + // arrange + var storage = new LocalStorage(TestHelpers.UniqueInstance()); + for (var i = 0; i < 10; i++) + storage.Store(Guid.NewGuid().ToString(), i); + var expected_keycount = storage.Count; + + // act + var target = storage.Keys(); + + // assert + target.Should().NotBeNullOrEmpty(); + target.Count.Should().Be(expected_keycount); + } + + [Fact(DisplayName = "LocalStorage.Keys() should return 0 on empty collection")] + public void LocalStorage_Keys_Should_Return_Zero_On_Empty_Collection() + { + // arrange + var storage = new LocalStorage(TestHelpers.UniqueInstance()); + + // act + var target = storage.Keys(); + + // assert + target.Should().NotBeNull(); + target.Should().BeEmpty(); + target.Count.Should().Be(0, because: "nothing is added to the LocalStorage"); + } [Fact(DisplayName = "LocalStorage.Query() should cast to a collection")] public void LocalStorage_Query_Should_Cast_Response_To_Collection() diff --git a/LocalStorage/ILocalStorageConfiguration.cs b/LocalStorage/ILocalStorageConfiguration.cs index 05b5b7b..4c9ca8a 100644 --- a/LocalStorage/ILocalStorageConfiguration.cs +++ b/LocalStorage/ILocalStorageConfiguration.cs @@ -23,6 +23,11 @@ public interface ILocalStorageConfiguration /// bool EnableEncryption { get; set; } + /// + /// [Optional] Add a custom salt to encryption, when EnableEncryption is enabled. + /// + string EncryptionSalt { get; set; } + /// /// Filename for the persisted state on disk (defaults to ".localstorage"). /// diff --git a/LocalStorage/LocalStorage.cs b/LocalStorage/LocalStorage.cs index 68f6e8a..0cfef8b 100644 --- a/LocalStorage/LocalStorage.cs +++ b/LocalStorage/LocalStorage.cs @@ -20,7 +20,7 @@ public class LocalStorage : IDisposable /// /// Configurable behaviour for this LocalStorage instance. /// - private readonly LocalStorageConfiguration _config; + private readonly ILocalStorageConfiguration _config; /// /// User-provided encryption key, used for encrypting/decrypting values. @@ -31,16 +31,16 @@ public class LocalStorage : IDisposable /// Most current actual, in-memory state representation of the LocalStorage. /// private Dictionary Storage { get; set; } = new Dictionary(); + + private object writeLock = new object(); - public LocalStorage() : this(new LocalStorageConfiguration()) { } + public LocalStorage() : this(new LocalStorageConfiguration(), string.Empty) { } - public LocalStorage(LocalStorageConfiguration configuration) : this(configuration, string.Empty) { } + public LocalStorage(ILocalStorageConfiguration configuration) : this(configuration, string.Empty) { } - public LocalStorage(LocalStorageConfiguration configuration, string encryptionKey) + public LocalStorage(ILocalStorageConfiguration configuration, string encryptionKey) { - if (configuration == null) throw new ArgumentNullException(nameof(configuration)); - - _config = configuration; + _config = configuration ?? throw new ArgumentNullException(nameof(configuration)); if (_config.EnableEncryption) { if (string.IsNullOrEmpty(encryptionKey)) throw new ArgumentNullException(nameof(encryptionKey), "When EnableEncryption is enabled, an encryptionKey is required when initializing the LocalStorage."); @@ -75,6 +75,16 @@ public void Destroy() File.Delete(FileHelpers.GetLocalStoreFilePath(_config.Filename)); } + /// + /// Determines whether this LocalStorage instance contains the specified key. + /// + /// + /// + public bool Exists(string key) + { + return Storage.ContainsKey(key: key); + } + /// /// Gets an object from the LocalStorage, without knowing its type. /// @@ -99,6 +109,14 @@ public T Get(string key) return JsonConvert.DeserializeObject(raw); } + /// + /// Gets a collection containing all the keys in the LocalStorage. + /// + public IReadOnlyCollection Keys() + { + return Storage.Keys.OrderBy(x => x).ToList(); + } + /// /// Loads the persisted state from disk into memory, overriding the current memory instance. /// @@ -152,13 +170,22 @@ public IEnumerable Query(string key, Func predicate = null) /// public void Persist() { - var serialized = JsonConvert.SerializeObject(Storage); + var serialized = JsonConvert.SerializeObject(Storage, Formatting.Indented); - using (var fileStream = new FileStream(FileHelpers.GetLocalStoreFilePath(_config.Filename), FileMode.OpenOrCreate, FileAccess.Write)) + var writemode = File.Exists(FileHelpers.GetLocalStoreFilePath(_config.Filename)) + ? FileMode.Truncate + : FileMode.Create; + + lock (writeLock) { - using (var writer = new StreamWriter(fileStream)) + using (var fileStream = new FileStream(FileHelpers.GetLocalStoreFilePath(_config.Filename), + mode: writemode, + access: FileAccess.Write)) { - writer.Write(serialized); + using (var writer = new StreamWriter(fileStream)) + { + writer.Write(serialized); + } } } } diff --git a/LocalStorage/LocalStorage.csproj b/LocalStorage/LocalStorage.csproj index 9e88800..1ae30a9 100644 --- a/LocalStorage/LocalStorage.csproj +++ b/LocalStorage/LocalStorage.csproj @@ -1,22 +1,25 @@  - netcoreapp1.0;netstandard1.3;net46 + netstandard2.0 True Juliën Hanssens https://github.com/hanssens/localstorage-for-dotnet https://github.com/hanssens/localstorage-for-dotnet - c#, dotnet, local, storage + c#, dotnet, storage, cache, nosql, lightweight A simple and lightweight tool for persisting data in dotnet (core) apps. git https://d17oy1vhnax1f7.cloudfront.net/items/1x2w321G3u3x2z0g120Q/hanssens-beer.png - https://github.com/hanssens/localstorage-for-dotnet/blob/master/LICENSE - 1.1.0 + LocalStorage + 2.0.0 Hanssens.Net See the releases page on GitHub for release notes: https://github.com/hanssens/localstorage-for-dotnet/releases - + + + + \ No newline at end of file diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..e3a09a2 --- /dev/null +++ b/build.sh @@ -0,0 +1 @@ +dotnet pack ./LocalStorage/LocalStorage.csproj -c Release --include-symbols