diff --git a/LiteDB.Tests/Database/DocumentUpgrade_Tests.cs b/LiteDB.Tests/Database/DocumentUpgrade_Tests.cs new file mode 100644 index 000000000..5bf3ad3a2 --- /dev/null +++ b/LiteDB.Tests/Database/DocumentUpgrade_Tests.cs @@ -0,0 +1,133 @@ +using FluentAssertions; + +using LiteDB.Engine; + +using System.IO; + +using Xunit; + +namespace LiteDB.Tests.Database; + +public class DocumentUpgrade_Tests +{ + [Fact] + public void DocumentUpgrade_Test() + { + var ms = new MemoryStream(); + using (var db = new LiteDatabase(ms)) + { + var col = db.GetCollection("col"); + + col.Insert(new BsonDocument { ["version"] = 1, ["_id"] = 1, ["name"] = "John" }); + } + + ms.Position = 0; + + using (var db = new LiteDatabase(ms)) + { + var col = db.GetCollection("col"); + + col.Count().Should().Be(1); + + var doc = col.FindById(1); + + doc["version"].AsInt32.Should().Be(1); + doc["name"].AsString.Should().Be("John"); + doc["age"].AsInt32.Should().Be(0); + } + + ms.Position = 0; + + using var engine = new LiteEngine(new EngineSettings + { + DataStream = ms, + ReadTransform = (collectionName, val) => + { + if (val is not BsonDocument doc) + { + return val; + } + + if (doc.TryGetValue("version", out var version) && version.AsInt32 == 1) + { + doc["version"] = 2; + doc["age"] = 30; + } + + return val; + } + }); + + using (var db = new LiteDatabase(engine)) + { + var col = db.GetCollection("col"); + + col.Count().Should().Be(1); + + var doc = col.FindById(1); + + doc["version"].AsInt32.Should().Be(2); + doc["name"].AsString.Should().Be("John"); + doc["age"].AsInt32.Should().Be(30); + } + } + + [Fact] + public void DocumentUpgrade_BsonMapper_Test() + { + var ms = new MemoryStream(); + using (var db = new LiteDatabase(ms)) + { + var col = db.GetCollection("col"); + + col.Insert(new BsonDocument { ["version"] = 1, ["_id"] = 1, ["name"] = "John" }); + } + + ms.Position = 0; + + using (var db = new LiteDatabase(ms)) + { + var col = db.GetCollection("col"); + + col.Count().Should().Be(1); + + var doc = col.FindById(1); + + doc["version"].AsInt32.Should().Be(1); + doc["name"].AsString.Should().Be("John"); + doc["age"].AsInt32.Should().Be(0); + } + + ms.Position = 0; + + var mapper = new BsonMapper(); + mapper.OnDeserialization = (sender, type, val) => + { + if (val is not BsonDocument doc) + { + return val; + } + + if (doc.TryGetValue("version", out var version) && version.AsInt32 == 1) + { + doc["version"] = 2; + doc["age"] = 30; + } + + return doc; + }; + + using (var db = new LiteDatabase(ms, mapper)) + { + var col = db.GetCollection("col"); + + col.Count().Should().Be(1); + + var doc = col.FindById(1); + + doc["version"].AsInt32.Should().Be(2); + doc["name"].AsString.Should().Be("John"); + doc["age"].AsInt32.Should().Be(30); + } + } +} \ No newline at end of file diff --git a/LiteDB/Client/Mapper/BsonMapper.Deserialize.cs b/LiteDB/Client/Mapper/BsonMapper.Deserialize.cs index a79eb32e2..67d93745c 100644 --- a/LiteDB/Client/Mapper/BsonMapper.Deserialize.cs +++ b/LiteDB/Client/Mapper/BsonMapper.Deserialize.cs @@ -9,6 +9,24 @@ namespace LiteDB { public partial class BsonMapper { + #region Deserialization Hooks + + /// + /// Delegate for deserialization callback. + /// + /// The BsonMapper instance that triggered the deserialization. + /// The target type for deserialization. + /// The BsonValue to be deserialized. + /// The deserialized BsonValue. + public delegate BsonValue DeserializationCallback(BsonMapper sender, Type target, BsonValue value); + + /// + /// Gets called before deserialization of a value + /// + public DeserializationCallback? OnDeserialization { get; set; } + + #endregion Deserialization Hooks + #region Basic direct .NET convert types // direct bson types @@ -78,6 +96,15 @@ public T Deserialize(BsonValue value) /// public object Deserialize(Type type, BsonValue value) { + if (OnDeserialization is not null) + { + var result = OnDeserialization(this, type, value); + if (result is not null) + { + value = result; + } + } + // null value - null returns if (value.IsNull) return null; diff --git a/LiteDB/Client/Structures/ConnectionString.cs b/LiteDB/Client/Structures/ConnectionString.cs index 3f44acd76..945649764 100644 --- a/LiteDB/Client/Structures/ConnectionString.cs +++ b/LiteDB/Client/Structures/ConnectionString.cs @@ -1,4 +1,4 @@ -using LiteDB.Engine; +using LiteDB.Engine; using System; using System.Collections.Generic; using System.Globalization; @@ -107,7 +107,7 @@ public ConnectionString(string connectionString) /// /// Create ILiteEngine instance according string connection parameters. For now, only Local/Shared are supported /// - internal ILiteEngine CreateEngine() + internal ILiteEngine CreateEngine(Action engineSettingsAction = null) { var settings = new EngineSettings { @@ -120,6 +120,8 @@ internal ILiteEngine CreateEngine() AutoRebuild = this.AutoRebuild, }; + engineSettingsAction?.Invoke(settings); + // create engine implementation as Connection Type if (this.Connection == ConnectionType.Direct) { diff --git a/LiteDB/Document/DataReader/BsonDataReader.cs b/LiteDB/Document/DataReader/BsonDataReader.cs index c76125085..33131362a 100644 --- a/LiteDB/Document/DataReader/BsonDataReader.cs +++ b/LiteDB/Document/DataReader/BsonDataReader.cs @@ -1,4 +1,4 @@ -using LiteDB.Engine; +using LiteDB.Engine; using System; using System.Collections; using System.Collections.Generic; @@ -56,10 +56,10 @@ internal BsonDataReader(IEnumerable values, string collection, Engine if (_source.MoveNext()) { _hasValues = _isFirst = true; - _current = _source.Current; + _current = _state.ReadTransform(_collection, _source.Current); } } - catch(Exception ex) + catch (Exception ex) { _state.Handle(ex); throw; @@ -102,10 +102,10 @@ public bool Read() try { var read = _source.MoveNext(); // can throw any error here - _current = _source.Current; + _current = _state.ReadTransform(_collection, _source.Current); return read; } - catch(Exception ex) + catch (Exception ex) { _state.Handle(ex); throw ex; @@ -117,7 +117,7 @@ public bool Read() } } } - + public BsonValue this[string field] { get diff --git a/LiteDB/Engine/EngineSettings.cs b/LiteDB/Engine/EngineSettings.cs index dea2cd7bb..e78034ab2 100644 --- a/LiteDB/Engine/EngineSettings.cs +++ b/LiteDB/Engine/EngineSettings.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.ComponentModel; @@ -7,6 +7,7 @@ using System.Reflection; using System.Text; using System.Text.RegularExpressions; + using static LiteDB.Constants; namespace LiteDB.Engine @@ -66,6 +67,11 @@ public class EngineSettings /// public bool Upgrade { get; set; } = false; + /// + /// Is used to transform a from the database on read. This can be used to upgrade data from older versions. + /// + public Func ReadTransform { get; set; } + /// /// Create new IStreamFactory for datafile /// diff --git a/LiteDB/Engine/EngineState.cs b/LiteDB/Engine/EngineState.cs index cfdad8a25..bd3dafac7 100644 --- a/LiteDB/Engine/EngineState.cs +++ b/LiteDB/Engine/EngineState.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; @@ -9,7 +9,6 @@ using static LiteDB.Constants; - namespace LiteDB.Engine { internal class EngineState @@ -25,7 +24,7 @@ internal class EngineState #endif public EngineState(LiteEngine engine, EngineSettings settings) - { + { _engine = engine; _settings = settings; } @@ -39,7 +38,7 @@ public bool Handle(Exception ex) { LOG(ex.Message, "ERROR"); - if (ex is IOException || + if (ex is IOException || (ex is LiteException lex && lex.ErrorCode == LiteException.INVALID_DATAFILE_STATE)) { _exception = ex; @@ -51,5 +50,12 @@ public bool Handle(Exception ex) return true; } + + public BsonValue ReadTransform(string collection, BsonValue value) + { + if (_settings?.ReadTransform is null) return value; + + return _settings.ReadTransform(collection, value); + } } -} +} \ No newline at end of file diff --git a/LiteDB/LiteDB.csproj b/LiteDB/LiteDB.csproj index 86ec3afdd..f889ceeeb 100644 --- a/LiteDB/LiteDB.csproj +++ b/LiteDB/LiteDB.csproj @@ -28,7 +28,7 @@ true LiteDB.snk true - 8.0 + latest