Skip to content

Commit 8e5ab3a

Browse files
authored
create a converter and use in EF to deserialize ChangeEntries which stored json in json (#1602)
1 parent 180079a commit 8e5ab3a

File tree

2 files changed

+84
-2
lines changed

2 files changed

+84
-2
lines changed

backend/LexData/Entities/CommitEntityConfiguration.cs

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ namespace LexData.Entities;
1111

1212
public class CommitEntityConfiguration : IEntityTypeConfiguration<ServerCommit>
1313
{
14+
private static JsonSerializerOptions Options = new JsonSerializerOptions()
15+
{
16+
Converters = { new ServerJsonChangeConverter() }
17+
};
1418
public void Configure(EntityTypeBuilder<ServerCommit> builder)
1519
{
1620
builder.ToTable("CrdtCommits");
@@ -24,12 +28,37 @@ public void Configure(EntityTypeBuilder<ServerCommit> builder)
2428
json => JsonSerializer.Deserialize<CommitMetadata>(json, (JsonSerializerOptions?)null) ?? new()
2529
);
2630
builder.Property(c => c.ChangeEntities).HasConversion(
27-
c => JsonSerializer.Serialize(c, (JsonSerializerOptions?)null),
28-
json => JsonSerializer.Deserialize<List<ChangeEntity<ServerJsonChange>>>(json, (JsonSerializerOptions?)null) ?? new()
31+
c => JsonSerializer.Serialize(c, Options),
32+
json => JsonSerializer.Deserialize<List<ChangeEntity<ServerJsonChange>>>(json, Options) ?? new()
2933
).HasColumnType("jsonb").IsRequired(false);
3034
}
3135

3236
private static ServerJsonChange Deserialize(string s) => JsonSerializer.Deserialize<ServerJsonChange>(s)!;
3337

3438
private static string Serialize(ServerJsonChange c) => JsonSerializer.Serialize(c);
39+
40+
private class ServerJsonChangeConverter: JsonConverter<ServerJsonChange>
41+
{
42+
public override ServerJsonChange? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions _)
43+
{
44+
//ignoring JsonSerializerOptions, otherwise we would stack overflow recursively using this converter
45+
if (typeToConvert != typeof(ServerJsonChange))
46+
throw new SerializationException("type not supported " + typeToConvert.FullName);
47+
//special case, this object type used to be encoded as a string in json like this: {"Change": "{\"Prop\": 1}"}
48+
if (reader.TokenType == JsonTokenType.String)
49+
{
50+
var json = reader.GetString();
51+
if (json is null) return null;
52+
return JsonSerializer.Deserialize<ServerJsonChange>(json);
53+
}
54+
55+
return JsonSerializer.Deserialize<ServerJsonChange>(ref reader);
56+
}
57+
58+
public override void Write(Utf8JsonWriter writer, ServerJsonChange value, JsonSerializerOptions options)
59+
{
60+
//not passing in options otherwise it would just use this converter again
61+
JsonSerializer.Serialize(writer, value);
62+
}
63+
}
3564
}

backend/Testing/LexCore/Services/CrdtCommitServiceTests.cs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using FluentAssertions.Equivalency;
44
using LexBoxApi.Services;
55
using LexData;
6+
using LinqToDB.EntityFrameworkCore;
67
using Microsoft.EntityFrameworkCore;
78
using Microsoft.Extensions.DependencyInjection;
89
using SIL.Harmony.Core;
@@ -61,6 +62,58 @@ private async IAsyncEnumerable<ServerCommit> AsAsync(IEnumerable<ServerCommit> c
6162
}
6263
}
6364

65+
//previously the value of the Change property was serialized twice, this test ensures that we can still
66+
//pull those commits out of the database
67+
[Fact]
68+
public async Task CanQueryOldCommits()
69+
{
70+
var projectId = await _lexBoxDbContext.Projects.Select(p => p.Id).FirstOrDefaultAsync();
71+
var context = _lexBoxDbContext.CreateLinqToDBContext();
72+
var table = LinqToDB.DataExtensions.GetTable<ServerCommit>(context);
73+
var commitId = Guid.NewGuid();
74+
var changeEntity = new ChangeEntity<ServerJsonChange>
75+
{
76+
Index = 0,
77+
CommitId = commitId,
78+
EntityId = Guid.NewGuid(),
79+
Change = new()
80+
{
81+
Type = "MyTestType",
82+
ExtensionData = new Dictionary<string, JsonElement>()
83+
{
84+
["MyTestProperty"] = JsonSerializer.SerializeToElement("MyTestValue")
85+
}
86+
}
87+
};
88+
var changeEntityJson = JsonSerializer.SerializeToNode(changeEntity);
89+
changeEntityJson.Should().NotBeNull();
90+
//the old format stored json in json, this is emulating that.
91+
changeEntityJson["Change"] = changeEntityJson["Change"]?.ToJsonString();
92+
var jsonPayload = changeEntityJson.ToJsonString();
93+
var inlineSql = $"'[{jsonPayload}]'::jsonb";
94+
//insert a new server commit, manually specifying the value for ChangeEntities so it will match the old format.
95+
await LinqToDB.LinqExtensions.InsertAsync(table, () => new ServerCommit(commitId)
96+
{
97+
Id = commitId,
98+
ClientId = Guid.NewGuid(),
99+
HybridDateTime = new HybridDateTime(DateTimeOffset.UtcNow, 0)
100+
{
101+
DateTime = DateTimeOffset.UtcNow,
102+
Counter = 0
103+
},
104+
ProjectId = projectId,
105+
Metadata = new CommitMetadata(),
106+
ChangeEntities = LinqToDB.Sql.Expr<List<ChangeEntity<ServerJsonChange>>>(inlineSql)
107+
});
108+
var commits = await _lexBoxDbContext.CrdtCommits(projectId).ToArrayAsync();
109+
var actualCommit = commits.Should().ContainSingle(c => c.Id == commitId).Subject;
110+
actualCommit.ChangeEntities.Should().BeEquivalentTo([changeEntity],
111+
options => options
112+
.Using<JsonElement>(ctx => ctx.Subject.ToString().Should().Be(ctx.Expectation.ToString()))
113+
.WhenTypeIs<JsonElement>()
114+
);
115+
}
116+
64117

65118
[Fact]
66119
public async Task CanAddCommits()

0 commit comments

Comments
 (0)