diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e655ac..459f6e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog (Sorted by Date in Descending Order) +## 1.2.0.0 + +* An overload to `Upsert` without `updateCondition` was added and would now act as default path in case `updateCondition` wasn't specified, this should further optimize such cases by removing condition checks and another reference from the stack during runtime. +* Internal methods which are rather small and frequently invoked will now be prioritized for inlining by JIT, this should slightly improve perf, especially in NativeAot. +* Added a new factory initializer `CreateFromFileWithAes` that received an `Aes` instance as parameter. It will then use it to encrypt and decrypt the output and input during serialization and deserialization respectively. + ## 1.1.0.0 * Fixed issue with `FileSerializer` where serialization would write over existing file data which could create invalid tokens, causing deserialization to fail. diff --git a/README.md b/README.md index ed0bb73..b21ac87 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ [![NuGet Downloads](https://img.shields.io/nuget/dt/ArrowDb?style=flat&label=Nuget%20-%20ArrowDb)](https://www.nuget.org/packages/ArrowDb) [![Unit Tests](https://github.com/dusrdev/ArrowDb/actions/workflows/unit-tests.yaml/badge.svg)](https://github.com/dusrdev/ArrowDb/actions/workflows/unit-tests.yaml) [![Integrity Tests](https://github.com/dusrdev/ArrowDb/actions/workflows/integrity-tests.yaml/badge.svg)](https://github.com/dusrdev/ArrowDb/actions/workflows/integrity-tests.yaml) - + ArrowDb is a fast, lightweight, and type-safe key-value database designed for .NET. @@ -97,10 +97,11 @@ bool db.TryGetValue(ReadOnlySpan key, JsonTypeInfo jsonTyp Notice that all APIs accept keys as `ReadOnlySpan` to avoid unnecessary allocations. This means that if you check for a key by some slice of a string, there is no need to allocate a string just for the lookup. -Upserting (adding or updating) is done via a single method: +Upserting (adding or updating) is done via a 2 overloads: ```csharp -bool db.Upsert(ReadOnlySpan key, TValue value, JsonTypeInfo jsonTypeInfo, Func? updateCondition = null); // upserts a value into the ArrowDb instance +bool db.Upsert(ReadOnlySpan key, TValue value, JsonTypeInfo jsonTypeInfo); +bool db.Upsert(ReadOnlySpan key, TValue value, JsonTypeInfo jsonTypeInfo, Func updateCondition = null); // upserts a value into the ArrowDb instance ``` And removal: @@ -112,21 +113,18 @@ void Clear(); // removes all entries from the Arrow ## Optimistic Concurrency Control -`ArrowDb` uses optimistic concurrency control as a way to resolve write conflicts, similar to MongoDb. This is done via the optional `updateCondition` parameter of the `Upsert` method. +`ArrowDb` uses optimistic concurrency control as a way to resolve write conflicts, similar to MongoDb. This is done via the overload of `Upsert` with the `updateCondition` parameter. -The updateCondition is a predicate that is invoked on the reference value that is currently stored in the db under the same key. +The `updateCondition` is a predicate that is invoked on the reference value that is currently stored in the db under the same key. -For ArrowDb to reject the update (and return `false`), ALL of the following 3 conditions must be met: +For ArrowDb to reject the update (and return `false`), ALL of the following 2 conditions must be met: -1. The `updateCondition` predicate must not be null. -2. An entry with the same key must exist in the db and be successfully parsed into the specified type. -3. The `updateCondition` predicate returns `false` when invoked on the reference value. +1. An entry with the same key must exist in the db and be successfully parsed into the specified type. +2. The `updateCondition` predicate returns `false` when invoked on the reference value. This means that all other cases would allow addition/update: -* If `updateCondition` is null, no check is performed on the same key before adding/updating. - * This also means that the type will not be validated to be the same as the existing value, and will just overwrite it. -* If `updateCondition` is not null, but the key does not exist, the update is allowed - and regarded as an addition. If a strict "update or nothing" behavior is desired, combine `ContainsKey` into the workflow before calling `Upsert`. +* If `updateCondition` is used and the key does not exist, the update is allowed - and regarded as an addition. If a strict "update or nothing" behavior is desired, combine `ContainsKey` into the workflow before calling `Upsert`. To illustrate this, Let’s look at an example of a timestamped `Note` entity: @@ -196,6 +194,19 @@ builder.Services.AddSingleton(() => ArrowDb.CreateInMemory().GetAwaiter().GetRes // Since this isn’t persisted, you may also use it as a Transient or Scoped service (whatever fits your needs). ``` +## Encryption + +As seen earlier, the default recommended serializer is `FileSerializer`, which serializes the db to a file on disk. In addition to it, `ArrowDb` also features a similar serializer that encrypts the db to a file on disk (the `AesFileSerializer`), for that the serializer requires an `Aes` instance to be passed along with the path to the file. + +```csharp +string path = "store.db"; +using var aes = Aes.Create(); +var db = await ArrowDb.CreateFromFileWithAes(path, aes); +// or with dependency injection +builder.Services.AddSingleton(_ => Aes.Create()); +builder.Services.AddSingleton(services => ArrowDb.CreateFromFileWithAes(path, services.GetRequiredService()).GetAwaiter().GetResult()); +``` + ## Serialization To enhance the use cases of `ArrowDb` it was designed to allow for custom serialization (the methods of persisting the db). diff --git a/src/ArrowDbCore/ArrowDb.Factory.cs b/src/ArrowDbCore/ArrowDb.Factory.cs index 2bb2739..ae48ccf 100644 --- a/src/ArrowDbCore/ArrowDb.Factory.cs +++ b/src/ArrowDbCore/ArrowDb.Factory.cs @@ -1,4 +1,8 @@ -namespace ArrowDbCore; +using System.Security.Cryptography; + +using ArrowDbCore.Serializers; + +namespace ArrowDbCore; public partial class ArrowDb { /// @@ -12,6 +16,18 @@ public static async ValueTask CreateFromFile(string path) { return new ArrowDb(data, serializer); } + /// + /// Initializes an managed file/disk backed database at the specified path + /// + /// The path that the file that backs the database + /// The instance to use + /// A database instance + public static async ValueTask CreateFromFileWithAes(string path, Aes aes) { + var serializer = new AesFileSerializer(path, aes, ArrowDbJsonContext.Default.ConcurrentDictionaryStringByteArray); + var data = await serializer.DeserializeAsync(); + return new ArrowDb(data, serializer); + } + /// /// Initializes an in-memory database /// diff --git a/src/ArrowDbCore/ArrowDb.Serialization.cs b/src/ArrowDbCore/ArrowDb.Serialization.cs index 988601c..7ccb432 100644 --- a/src/ArrowDbCore/ArrowDb.Serialization.cs +++ b/src/ArrowDbCore/ArrowDb.Serialization.cs @@ -1,4 +1,6 @@ -namespace ArrowDbCore; +using System.Runtime.CompilerServices; + +namespace ArrowDbCore; public partial class ArrowDb { /// @@ -23,6 +25,7 @@ public async Task SerializeAsync() { /// /// Waits for the semaphore if the database is currently serializing /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] private void WaitIfSerializing() { if (Semaphore.CurrentCount == 0) { Semaphore.Wait(); diff --git a/src/ArrowDbCore/ArrowDb.Upsert.cs b/src/ArrowDbCore/ArrowDb.Upsert.cs index c9ab00b..995ec83 100644 --- a/src/ArrowDbCore/ArrowDb.Upsert.cs +++ b/src/ArrowDbCore/ArrowDb.Upsert.cs @@ -4,6 +4,22 @@ namespace ArrowDbCore; public partial class ArrowDb { + /// + /// Upsert the specified key with the specified value into the database + /// + /// The type of the value to upsert + /// The key at which to upsert the value + /// The value to upsert + /// The json type info for the value type + /// True + public bool Upsert(ReadOnlySpan key, TValue value, JsonTypeInfo jsonTypeInfo) { + WaitIfSerializing(); // block if the database is currently serializing + byte[] utf8value = JsonSerializer.SerializeToUtf8Bytes(value, jsonTypeInfo); + Lookup[key] = utf8value; + OnChangeInternal(ArrowDbChangeEventArgs.Upsert); // trigger change event + return true; + } + /// /// Tries to upsert the specified key with the specified value into the database /// @@ -21,16 +37,11 @@ public partial class ArrowDb { /// 2. A value for the specified key exists and successfully deserialized to /// 3. on the reference value returns false /// - public bool Upsert(ReadOnlySpan key, TValue value, JsonTypeInfo jsonTypeInfo, Func? updateCondition = null) { - if (updateCondition is not null && - TryGetValue(key, jsonTypeInfo, out TValue existingReference) && + public bool Upsert(ReadOnlySpan key, TValue value, JsonTypeInfo jsonTypeInfo, Func updateCondition) { + if (TryGetValue(key, jsonTypeInfo, out TValue existingReference) && !updateCondition(existingReference)) { return false; } - WaitIfSerializing(); // block if the database is currently serializing - byte[] utf8value = JsonSerializer.SerializeToUtf8Bytes(value, jsonTypeInfo); - Lookup[key] = utf8value; - OnChangeInternal(ArrowDbChangeEventArgs.Upsert); // trigger change event - return true; + return Upsert(key, value, jsonTypeInfo); } } diff --git a/src/ArrowDbCore/ArrowDbCore.csproj b/src/ArrowDbCore/ArrowDbCore.csproj index 07c1da9..2e9e0b3 100644 --- a/src/ArrowDbCore/ArrowDbCore.csproj +++ b/src/ArrowDbCore/ArrowDbCore.csproj @@ -4,14 +4,14 @@ net9.0 enable enable - 1.1.0.0 + 1.2.0.0 true David Shnayder David Shnayder - README.md + Readme.Nuget.md LICENSE.txt True ArrowDb @@ -28,7 +28,7 @@ - + diff --git a/src/ArrowDbCore/README.md b/src/ArrowDbCore/README.md deleted file mode 120000 index fe84005..0000000 --- a/src/ArrowDbCore/README.md +++ /dev/null @@ -1 +0,0 @@ -../../README.md \ No newline at end of file diff --git a/src/ArrowDbCore/Readme.Nuget.md b/src/ArrowDbCore/Readme.Nuget.md new file mode 100644 index 0000000..aba95d2 --- /dev/null +++ b/src/ArrowDbCore/Readme.Nuget.md @@ -0,0 +1,14 @@ +# ArrowDb + +A fast, lightweight, and type-safe key-value database designed for .NET. + +* Super-Lightweight (dll size is <= 20KB - approximately 9X smaller than [UltraLiteDb](https://github.com/rejemy/UltraLiteDB)) +* Ultra-Fast (1,000,000 random operations / ~100ms on M2 MacBook Pro) +* Minimal-Allocation (~2KB for serialization of 1,000,000 items) +* Thread-Safe and Concurrent +* ACID compliant on transaction level +* Type-Safe (no reflection - compile-time enforced via source-generated `JsonSerializerContext`) +* Cross-Platform and Fully AOT-compatible +* Super-Easy API near mirroring of `Dictionary` + +Information on usage can be found in the [README](https://github.com/dusrdev/ArrowDb/blob/stable/README.md). diff --git a/src/ArrowDbCore/Serializers/AesFileSerializer.cs b/src/ArrowDbCore/Serializers/AesFileSerializer.cs new file mode 100644 index 0000000..3cfdc8a --- /dev/null +++ b/src/ArrowDbCore/Serializers/AesFileSerializer.cs @@ -0,0 +1,59 @@ +using System.Collections.Concurrent; +using System.Security.Cryptography; +using System.Text.Json; +using System.Text.Json.Serialization.Metadata; + +namespace ArrowDbCore.Serializers; + +/// +/// An managed file/disk backed serializer +/// +public sealed class AesFileSerializer : IDbSerializer { + /// + /// The path to the file + /// + private readonly string _path; + + /// + /// The Aes instance + /// + private readonly Aes _aes; + + /// + /// The json type info for the dictionary + /// + private readonly JsonTypeInfo> _jsonTypeInfo; + + /// + /// Initializes a new instance of the class. + /// + /// The path to the file + /// The instance to use + /// The json type info for the dictionary + public AesFileSerializer(string path, Aes aes, JsonTypeInfo> jsonTypeInfo) { + _path = path; + _aes = aes; + _jsonTypeInfo = jsonTypeInfo; + } + + /// + public ValueTask> DeserializeAsync() { + if (!File.Exists(_path) || new FileInfo(_path).Length == 0) { + return ValueTask.FromResult(new ConcurrentDictionary()); + } + using var fileStream = File.OpenRead(_path); + using var decryptor = _aes.CreateDecryptor(); + using var cryptoStream = new CryptoStream(fileStream, decryptor, CryptoStreamMode.Read); + var res = JsonSerializer.Deserialize(cryptoStream, _jsonTypeInfo); + return ValueTask.FromResult(res ?? new ConcurrentDictionary()); + } + + /// + public ValueTask SerializeAsync(ConcurrentDictionary data) { + using var fileStream = File.Create(_path); + using var encryptor = _aes.CreateEncryptor(); + using var cryptoStream = new CryptoStream(fileStream, encryptor, CryptoStreamMode.Write); + JsonSerializer.Serialize(cryptoStream, data, _jsonTypeInfo); + return ValueTask.CompletedTask; + } +} \ No newline at end of file diff --git a/src/ArrowDbCore/FileSerializer.cs b/src/ArrowDbCore/Serializers/FileSerializer.cs similarity index 97% rename from src/ArrowDbCore/FileSerializer.cs rename to src/ArrowDbCore/Serializers/FileSerializer.cs index f424fca..16cf141 100644 --- a/src/ArrowDbCore/FileSerializer.cs +++ b/src/ArrowDbCore/Serializers/FileSerializer.cs @@ -2,7 +2,8 @@ using System.Text.Json; using System.Text.Json.Serialization.Metadata; -namespace ArrowDbCore; + +namespace ArrowDbCore.Serializers; /// /// A file/disk backed serializer diff --git a/src/ArrowDbCore/InMemorySerializer.cs b/src/ArrowDbCore/Serializers/InMemorySerializer.cs similarity index 93% rename from src/ArrowDbCore/InMemorySerializer.cs rename to src/ArrowDbCore/Serializers/InMemorySerializer.cs index ca16f6d..da46964 100644 --- a/src/ArrowDbCore/InMemorySerializer.cs +++ b/src/ArrowDbCore/Serializers/InMemorySerializer.cs @@ -1,6 +1,7 @@ using System.Collections.Concurrent; -namespace ArrowDbCore; + +namespace ArrowDbCore.Serializers; /// /// An in-memory serializer (does nothing) diff --git a/tests/ArrowDbCore.Tests.Integrity/LargeFile.cs b/tests/ArrowDbCore.Tests.Integrity/LargeFile.cs index 32e58c9..76636ac 100644 --- a/tests/ArrowDbCore.Tests.Integrity/LargeFile.cs +++ b/tests/ArrowDbCore.Tests.Integrity/LargeFile.cs @@ -1,10 +1,11 @@ -using Bogus; +using System.Security.Cryptography; + +using Bogus; namespace ArrowDbCore.Tests.Integrity; public class LargeFile { - [Fact] - public async Task LargeFile_Passes_OneReadWriteCycle() { + private static async Task LargeFile_Passes_OneReadWriteCycle(string path, Func> factory) { const int itemCount = 500_000; var faker = new Faker(); @@ -15,11 +16,9 @@ public async Task LargeFile_Passes_OneReadWriteCycle() { faker.RuleFor(p => p.IsMarried, (f, _) => f.Random.Bool()); var buffer = new char[256]; - - var path = Sharpify.Utils.Env.PathInBaseDirectory("long-test.db"); try { // load the db - var db = await ArrowDb.CreateFromFile(path); + var db = await factory(); // clear db.Clear(); // add items @@ -32,7 +31,7 @@ public async Task LargeFile_Passes_OneReadWriteCycle() { await db.SerializeAsync(); var actualCount = db.Count; // try to load again - var db2 = await ArrowDb.CreateFromFile(path); + var db2 = await factory(); Assert.Equal(actualCount, db2.Count); } finally { if (File.Exists(path)) { @@ -42,4 +41,19 @@ public async Task LargeFile_Passes_OneReadWriteCycle() { // this test fails if an exception is thrown } + + [Fact] + public async Task LargeFile_Passes_OneReadWriteCycle_FileSerializer() { + var path = Sharpify.Utils.Env.PathInBaseDirectory("long-test-file-serializer.db"); + await LargeFile_Passes_OneReadWriteCycle(path, () => ArrowDb.CreateFromFile(path)); + } + + [Fact] + public async Task LargeFile_Passes_OneReadWriteCycle_AesFileSerializer() { + var path = Sharpify.Utils.Env.PathInBaseDirectory("long-test-aes-file-serializer.db"); + using var aes = Aes.Create(); + aes.GenerateKey(); + aes.GenerateIV(); + await LargeFile_Passes_OneReadWriteCycle(path, () => ArrowDb.CreateFromFileWithAes(path, aes)); + } } \ No newline at end of file diff --git a/tests/ArrowDbCore.Tests.Integrity/OverwriteForceClear.cs b/tests/ArrowDbCore.Tests.Integrity/OverwriteForceClear.cs index 50092df..1946b5b 100644 --- a/tests/ArrowDbCore.Tests.Integrity/OverwriteForceClear.cs +++ b/tests/ArrowDbCore.Tests.Integrity/OverwriteForceClear.cs @@ -1,10 +1,11 @@ -using Bogus; +using System.Security.Cryptography; + +using Bogus; namespace ArrowDbCore.Tests.Integrity; public class OverwriteForceClear { - [Fact] - public async Task SerializeOverwritesExistingFile() { + private static async Task SerializeOverwritesExistingFile(string path, Func> factory) { const int itemCount = 1_000; var faker = new Faker(); @@ -15,11 +16,9 @@ public async Task SerializeOverwritesExistingFile() { faker.RuleFor(p => p.IsMarried, (f, _) => f.Random.Bool()); var buffer = new char[256]; - - var path = Sharpify.Utils.Env.PathInBaseDirectory("overwrite-test.db"); try { // load the db - var db = await ArrowDb.CreateFromFile(path); + var db = await factory(); // clear db.Clear(); // add items @@ -47,4 +46,19 @@ public async Task SerializeOverwritesExistingFile() { // this test fails if an exception is thrown or the file is not overwritten } + + [Fact] + public async Task SerializeOverwritesExistingFile_FileSerializer() { + var path = Sharpify.Utils.Env.PathInBaseDirectory("overwrite-test-file-serializer.db"); + await SerializeOverwritesExistingFile(path, () => ArrowDb.CreateFromFile(path)); + } + + [Fact] + public async Task SerializeOverwritesExistingFile_AesFileSerializer() { + var path = Sharpify.Utils.Env.PathInBaseDirectory("overwrite-test-aes-file-serializer.db"); + using var aes = Aes.Create(); + aes.GenerateKey(); + aes.GenerateIV(); + await SerializeOverwritesExistingFile(path, () => ArrowDb.CreateFromFileWithAes(path, aes)); + } } \ No newline at end of file diff --git a/tests/ArrowDbCore.Tests.Integrity/ReadWriteCycles.cs b/tests/ArrowDbCore.Tests.Integrity/ReadWriteCycles.cs index f7f5e88..1fcc358 100644 --- a/tests/ArrowDbCore.Tests.Integrity/ReadWriteCycles.cs +++ b/tests/ArrowDbCore.Tests.Integrity/ReadWriteCycles.cs @@ -1,12 +1,11 @@ -using Bogus; +using System.Security.Cryptography; + +using Bogus; namespace ArrowDbCore.Tests.Integrity; -public class ReadWriteCycles -{ - [Fact] - public async Task FileIO_Passes_ReadWriteCycles() - { +public class ReadWriteCycles { + private static async Task FileIO_Passes_ReadWriteCycles(string path, Func> factory) { const int iterations = 200; const int itemCount = 100; @@ -18,12 +17,10 @@ public async Task FileIO_Passes_ReadWriteCycles() faker.RuleFor(p => p.IsMarried, (f, _) => f.Random.Bool()); var buffer = new char[256]; - - var path = Sharpify.Utils.Env.PathInBaseDirectory("rdc-test.db"); try { for (var i = 0; i < iterations; i++) { // load the db - var db = await ArrowDb.CreateFromFile(path); + var db = await factory(); // clear db.Clear(); // add items @@ -43,4 +40,19 @@ public async Task FileIO_Passes_ReadWriteCycles() // this test fails if an exception is thrown } + + [Fact] + public async Task FileIO_Passes_ReadWriteCycles_FileSerializer() { + var path = Sharpify.Utils.Env.PathInBaseDirectory("rdc-test-file-serializer.db"); + await FileIO_Passes_ReadWriteCycles(path, () => ArrowDb.CreateFromFile(path)); + } + + [Fact] + public async Task FileIO_Passes_ReadWriteCycles_AesFileSerializer() { + var path = Sharpify.Utils.Env.PathInBaseDirectory("rdc-test-aes-file-serializer.db"); + using var aes = Aes.Create(); + aes.GenerateKey(); + aes.GenerateIV(); + await FileIO_Passes_ReadWriteCycles(path, () => ArrowDb.CreateFromFileWithAes(path, aes)); + } } \ No newline at end of file diff --git a/tests/ArrowDbCore.Tests.Unit/Serialization.cs b/tests/ArrowDbCore.Tests.Unit/Serialization.cs index 7d81a6b..99e1ffa 100644 --- a/tests/ArrowDbCore.Tests.Unit/Serialization.cs +++ b/tests/ArrowDbCore.Tests.Unit/Serialization.cs @@ -1,4 +1,6 @@ -namespace ArrowDbCore.Tests.Unit; +using System.Security.Cryptography; + +namespace ArrowDbCore.Tests.Unit; public class Serialization { [Fact] @@ -56,31 +58,42 @@ public async Task DeferredSerializationScope_Serialize_After_Dispose() { Assert.Equal(0, db.PendingChanges); } - [Fact] - public async Task SerializedToFile_Serializes_And_Deserializes_As_Expected() { - var file = Path.GetTempFileName(); + private static async Task File_Serializes_And_Deserializes_As_Expected(string path, Func> factory) { try { - var db = await ArrowDb.CreateFromFile(file); + var db = await factory(); db.Upsert("1", 1, JContext.Default.Int32); Assert.True(db.ContainsKey("1")); Assert.Equal(1, db.Count); Assert.Equal(1, db.PendingChanges); await db.SerializeAsync(); - var db2 = await ArrowDb.CreateFromFile(file); + var db2 = await factory(); Assert.Equal(db2.Source, db.Source); } finally { // cleanup - if (File.Exists(file)) { - File.Delete(file); + if (File.Exists(path)) { + File.Delete(path); } } } [Fact] - public async Task SerializedToFile_Serializes_And_Rollback_As_Expected() { - var file = Path.GetTempFileName(); + public async Task FileSerializer_Serializes_And_Deserializes_As_Expected() { + var path = Path.GetTempFileName(); + await File_Serializes_And_Deserializes_As_Expected(path, () => ArrowDb.CreateFromFile(path)); + } + + [Fact] + public async Task AesFileSerializer_Serializes_And_Deserializes_As_Expected() { + var path = Path.GetTempFileName(); + using var aes = Aes.Create(); + aes.GenerateKey(); + aes.GenerateIV(); + await File_Serializes_And_Deserializes_As_Expected(path, () => ArrowDb.CreateFromFileWithAes(path, aes)); + } + + private static async Task File_Serializes_And_Rollback_As_Expected(string path, Func> factory) { try { - var db = await ArrowDb.CreateFromFile(file); + var db = await factory(); db.Upsert("1", 1, JContext.Default.Int32); Assert.True(db.ContainsKey("1")); Assert.Equal(1, db.Count); @@ -101,9 +114,24 @@ public async Task SerializedToFile_Serializes_And_Rollback_As_Expected() { Assert.Equal(1, value); } finally { // cleanup - if (File.Exists(file)) { - File.Delete(file); + if (File.Exists(path)) { + File.Delete(path); } } } + + [Fact] + public async Task FileSerializer_Serializes_And_Rollback_As_Expected() { + var path = Path.GetTempFileName(); + await File_Serializes_And_Rollback_As_Expected(path, () => ArrowDb.CreateFromFile(path)); + } + + [Fact] + public async Task AesFileSerializer_Serializes_And_Rollback_As_Expected() { + var path = Path.GetTempFileName(); + using var aes = Aes.Create(); + aes.GenerateKey(); + aes.GenerateIV(); + await File_Serializes_And_Rollback_As_Expected(path, () => ArrowDb.CreateFromFileWithAes(path, aes)); + } } \ No newline at end of file