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 3b24d4b2c..ef7f1b10d 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 d2bb96372..609dedfad 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 535aca532..2cd92c596 100644
--- a/LiteDB/LiteDB.csproj
+++ b/LiteDB/LiteDB.csproj
@@ -28,7 +28,7 @@
true
LiteDB.snk
true
- 8.0
+ latest