diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonDefaultConventions.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonDefaultConventions.cs index efb01a5c47..c7d6fb0901 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonDefaultConventions.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonDefaultConventions.cs @@ -18,7 +18,6 @@ public static void Register() { try { - if (isRegistered) { return; diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonInstantSerializer.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonInstantSerializer.cs index e23cd2d4a6..24bbae3b8f 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonInstantSerializer.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonInstantSerializer.cs @@ -34,9 +34,14 @@ public bool IsDiscriminatorCompatibleWithObjectSerializer public BsonType Representation { get; } - public BsonInstantSerializer(BsonType representation = BsonType.DateTime) + public BsonInstantSerializer() + : this(BsonType.DateTime) { - if (representation != BsonType.DateTime && representation != BsonType.Int64 && representation != BsonType.String) + } + + public BsonInstantSerializer(BsonType representation) + { + if (representation is not BsonType.DateTime and not BsonType.Int64 and not BsonType.String) { throw new ArgumentException("Unsupported representation.", nameof(representation)); } diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConvention.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConvention.cs index 35205a1c5b..febbd8f361 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConvention.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConvention.cs @@ -6,6 +6,7 @@ // ========================================================================== using System.Reflection; +using System.Reflection.Metadata; using System.Text.Json; using MongoDB.Bson; using MongoDB.Bson.Serialization; @@ -17,9 +18,11 @@ public static class BsonJsonConvention { private static bool isRegistered; - public static JsonSerializerOptions Options { get; set; } = new JsonSerializerOptions(JsonSerializerDefaults.Web); + public static JsonSerializerOptions Options { get; private set; } = new JsonSerializerOptions(JsonSerializerDefaults.Web); - public static void Register(JsonSerializerOptions? options = null) + public static BsonType Representation { get; private set; } = BsonType.Document; + + public static void Register(JsonSerializerOptions? options = null, BsonType? representation = null) { try { @@ -28,6 +31,11 @@ public static void Register(JsonSerializerOptions? options = null) Options = options; } + if (representation != null) + { + Representation = representation.Value; + } + if (isRegistered) { return; diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonSerializer.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonSerializer.cs index cd45cb91b6..7dd7fbc6a3 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonSerializer.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonSerializer.cs @@ -14,28 +14,63 @@ namespace Squidex.Infrastructure.MongoDb { - public sealed class BsonJsonSerializer : ClassSerializerBase where T : class + public sealed class BsonJsonSerializer : ClassSerializerBase, IRepresentationConfigurable> where T : class { - public override T? Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) + public BsonType Representation { get; } + + public BsonType ActualRepresentation { - var bsonReader = context.Reader; + get => Representation == BsonType.Undefined ? BsonJsonConvention.Representation : Representation; + } - if (bsonReader.GetCurrentBsonType() == BsonType.Null) + public JsonSerializerOptions Options + { + get => BsonJsonConvention.Options; + } + + public BsonJsonSerializer() + : this(BsonType.Undefined) + { + } + + public BsonJsonSerializer(BsonType representation) + { + if (representation is not BsonType.Undefined and not BsonType.String and not BsonType.Binary) { - bsonReader.ReadNull(); - return null; + throw new ArgumentException("Unsupported representation.", nameof(representation)); } - using var stream = DefaultPools.MemoryStream.GetStream(); + Representation = representation; + } - using (var writer = new Utf8JsonWriter(stream)) + public override T? Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) + { + var reader = context.Reader; + + switch (reader.GetCurrentBsonType()) { - FromBson(bsonReader, writer); - } + case BsonType.Null: + reader.ReadNull(); + return null; + case BsonType.String: + var valueString = reader.ReadString(); + return JsonSerializer.Deserialize(valueString, Options); + case BsonType.Binary: + var valueBinary = reader.ReadBytes(); + return JsonSerializer.Deserialize(valueBinary, Options); + default: + using (var stream = DefaultPools.MemoryStream.GetStream()) + { + using (var writer = new Utf8JsonWriter(stream)) + { + FromBson(reader, writer); + } - stream.Position = 0; + stream.Position = 0; - return JsonSerializer.Deserialize(stream, BsonJsonConvention.Options); + return JsonSerializer.Deserialize(stream, Options); + } + } } private static void FromBson(IBsonReader reader, Utf8JsonWriter writer) @@ -43,28 +78,34 @@ private static void FromBson(IBsonReader reader, Utf8JsonWriter writer) void ReadDocument() { reader.ReadStartDocument(); - writer.WriteStartObject(); - - while (reader.ReadBsonType() != BsonType.EndOfDocument) { - Read(); + writer.WriteStartObject(); + + while (reader.ReadBsonType() != BsonType.EndOfDocument) + { + Read(); + } + + writer.WriteEndObject(); } - writer.WriteEndObject(); reader.ReadEndDocument(); } void ReadArray() { reader.ReadStartArray(); - writer.WriteStartArray(); - - while (reader.ReadBsonType() != BsonType.EndOfDocument) { - Read(); + writer.WriteStartArray(); + + while (reader.ReadBsonType() != BsonType.EndOfDocument) + { + Read(); + } + + writer.WriteEndArray(); } - writer.WriteEndArray(); reader.ReadEndArray(); } @@ -135,11 +176,25 @@ void Read() public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, T? value) { - var bsonWriter = context.Writer; + var writer = context.Writer; - using (var jsonDocument = JsonSerializer.SerializeToDocument(value, BsonJsonConvention.Options)) + switch (ActualRepresentation) { - WriteElement(bsonWriter, jsonDocument.RootElement); + case BsonType.String: + var jsonString = JsonSerializer.Serialize(value, args.NominalType, Options); + writer.WriteString(jsonString); + break; + case BsonType.Binary: + var jsonBytes = JsonSerializer.SerializeToUtf8Bytes(value, args.NominalType, Options); + writer.WriteBytes(jsonBytes); + break; + default: + using (var jsonDocument = JsonSerializer.SerializeToDocument(value, args.NominalType, Options)) + { + WriteElement(writer, jsonDocument.RootElement); + } + + break; } } @@ -178,6 +233,7 @@ private static void WriteElement(IBsonWriter writer, JsonElement element) foreach (var property in element.EnumerateObject()) { writer.WriteName(property.Name.EscapeJson()); + WriteElement(writer, property.Value); } @@ -188,5 +244,15 @@ private static void WriteElement(IBsonWriter writer, JsonElement element) break; } } + + public BsonJsonSerializer WithRepresentation(BsonType representation) + { + return Representation == representation ? this : new BsonJsonSerializer(representation); + } + + IBsonSerializer IRepresentationConfigurable.WithRepresentation(BsonType representation) + { + return WithRepresentation(representation); + } } } diff --git a/backend/src/Squidex/Config/Domain/StoreServices.cs b/backend/src/Squidex/Config/Domain/StoreServices.cs index ba1408dd02..6794af5e01 100644 --- a/backend/src/Squidex/Config/Domain/StoreServices.cs +++ b/backend/src/Squidex/Config/Domain/StoreServices.cs @@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Caching.Distributed; using Migrations.Migrations.MongoDb; +using MongoDB.Bson; using MongoDB.Driver; using MongoDB.Driver.Core.Extensions.DiagnosticSources; using Squidex.Assets; @@ -176,7 +177,9 @@ public static void AddSquidexStoreServices(this IServiceCollection services, ICo services.AddInitializer("Serializer (BSON)", jsonSerializerOptions => { - BsonJsonConvention.Options = jsonSerializerOptions; + var representation = config.GetValue("store:mongoDB:valueRepresentation"); + + BsonJsonConvention.Register(jsonSerializerOptions, representation); }, int.MinValue); } }); diff --git a/backend/src/Squidex/appsettings.json b/backend/src/Squidex/appsettings.json index 39a74d2e4d..fc6a95de8b 100644 --- a/backend/src/Squidex/appsettings.json +++ b/backend/src/Squidex/appsettings.json @@ -485,6 +485,11 @@ // The database for all your other read collections. "database": "Squidex", + // Defines how key-value-store values are represented in MongoDB (e.g. app, rule, schema). + // + // SUPPORTED: Undefined (Objects), String, Binary (from slow to fast). + "valueRepresentation": "Undefined", + "atlas": { // The organization id. "groupId": "", diff --git a/backend/tests/Squidex.Infrastructure.Tests/MongoDb/BsonJsonSerializerTests.cs b/backend/tests/Squidex.Infrastructure.Tests/MongoDb/BsonJsonSerializerTests.cs index 3e6f6fd072..08efc8f912 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/MongoDb/BsonJsonSerializerTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/MongoDb/BsonJsonSerializerTests.cs @@ -6,8 +6,9 @@ // ========================================================================== using FluentAssertions; -using MongoDB.Bson.IO; -using MongoDB.Bson.Serialization; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using Squidex.Infrastructure.TestHelpers; using Xunit; namespace Squidex.Infrastructure.MongoDb @@ -20,6 +21,20 @@ public class TestWrapper public T Value { get; set; } } + public class TestWrapperString + { + [BsonJson] + [BsonRepresentation(BsonType.String)] + public T Value { get; set; } + } + + public class TestWrapperBinary + { + [BsonJson] + [BsonRepresentation(BsonType.Binary)] + public T Value { get; set; } + } + public class TestObject { public bool Bool { get; set; } @@ -97,69 +112,90 @@ public static TestObject CreateWithValues(bool nested = true) } [Fact] - public void Should_serialize_with_reader_and_writer() + public void Should_serialize_and_deserialize() { - var source = TestObject.CreateWithValues(); + var source = new TestWrapper + { + Value = TestObject.CreateWithValues() + }; - var deserialized = SerializeAndDeserialize(source); + var deserialized = source.SerializeAndDeserializeBson(); deserialized.Should().BeEquivalentTo(source); } [Fact] - public void Should_deserialize_property_with_dollar() + public void Should_serialize_and_deserialize_as_string() { - var source = new Dictionary + var source = new TestWrapperString { - ["$key"] = 12 + Value = TestObject.CreateWithValues() }; - var deserialized = SerializeAndDeserialize(source); + var deserialized = source.SerializeAndDeserializeBson(); deserialized.Should().BeEquivalentTo(source); } [Fact] - public void Should_deserialize_property_with_dot() + public void Should_serialize_and_deserialize_as_binary() { - var source = new Dictionary + var source = new TestWrapperBinary { - ["type.of.value"] = 12 + Value = TestObject.CreateWithValues() }; - var deserialized = SerializeAndDeserialize(source); + var deserialized = source.SerializeAndDeserializeBson(); deserialized.Should().BeEquivalentTo(source); } [Fact] - public void Should_deserialize_property_as_empty_string() + public void Should_serialize_and_deserialize_property_with_dollar() { - var source = new Dictionary + var source = new TestWrapper> { - [string.Empty] = 12 + Value = new Dictionary + { + ["$key"] = 12 + } }; - var deserialized = SerializeAndDeserialize(source); + var deserialized = source.SerializeAndDeserializeBson(); deserialized.Should().BeEquivalentTo(source); } - private static T SerializeAndDeserialize(T value) + [Fact] + public void Should_serialize_and_deserialize_property_with_dot() { - var stream = new MemoryStream(); - - using (var writer = new BsonBinaryWriter(stream)) + var source = new TestWrapper> { - BsonSerializer.Serialize(writer, new TestWrapper { Value = value }); - } + Value = new Dictionary + { + ["type.of.value"] = 12 + } + }; - stream.Position = 0; + var deserialized = source.SerializeAndDeserializeBson(); - using (var reader = new BsonBinaryReader(stream)) + deserialized.Should().BeEquivalentTo(source); + } + + [Fact] + public void Should_serialize_and_deserialize_property_as_empty_string() + { + var source = new TestWrapper> { - return BsonSerializer.Deserialize>(reader)!.Value; - } + Value = new Dictionary + { + [string.Empty] = 12 + } + }; + + var deserialized = source.SerializeAndDeserializeBson(); + + deserialized.Should().BeEquivalentTo(source); } } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/MongoDb/InstantSerializerTests.cs b/backend/tests/Squidex.Infrastructure.Tests/MongoDb/InstantSerializerTests.cs index 84a738da2e..c1a3befb07 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/MongoDb/InstantSerializerTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/MongoDb/InstantSerializerTests.cs @@ -5,9 +5,8 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using MongoDB.Bson.IO; -using MongoDB.Bson.Serialization; using NodaTime; +using Squidex.Infrastructure.TestHelpers; using Xunit; namespace Squidex.Infrastructure.MongoDb @@ -20,74 +19,60 @@ public InstantSerializerTests() } [Fact] - public void Should_serialize_as_default() + public void Should_serialize_and_deserialize() { - var source = new Entities.DefaultEntity { Value = GetTime() }; + var source = new Entities.DefaultEntity + { + Value = GetTime() + }; - var result1 = SerializeAndDeserializeBson(source); + var deserialized = source.SerializeAndDeserializeBson(); - Assert.Equal(source.Value, result1.Value); + Assert.Equal(source.Value, deserialized.Value); } [Fact] - public void Should_serialize_as_string() + public void Should_serialize_and_deserialize_as_string() { - var source = new Entities.StringEntity { Value = GetTime() }; + var source = new Entities.StringEntity + { + Value = GetTime() + }; - var result1 = SerializeAndDeserializeBson(source); + var deserialized = source.SerializeAndDeserializeBson(); - Assert.Equal(source.Value, result1.Value); + Assert.Equal(source.Value, deserialized.Value); } [Fact] - public void Should_serialize_as_int64() + public void Should_serialize_and_deserialize_as_int64() { - var source = new Entities.Int64Entity { Value = GetTime() }; + var source = new Entities.Int64Entity + { + Value = GetTime() + }; - var result1 = SerializeAndDeserializeBson(source); + var deserialized = source.SerializeAndDeserializeBson(); - Assert.Equal(source.Value, result1.Value); + Assert.Equal(source.Value, deserialized.Value); } [Fact] - public void Should_serialize_as_datetime() + public void Should_serialize_and_deserialize_as_datetime() { - var source = new Entities.DateTimeEntity { Value = GetTime() }; + var source = new Entities.DateTimeEntity + { + Value = GetTime() + }; - var result1 = SerializeAndDeserializeBson(source); + var deserialized = source.SerializeAndDeserializeBson(); - Assert.Equal(source.Value, result1.Value); + Assert.Equal(source.Value, deserialized.Value); } private static Instant GetTime() { return SystemClock.Instance.GetCurrentInstant().WithoutNs(); } - - private static T SerializeAndDeserializeBson(T source) - { - return SerializeAndDeserializeBson(source); - } - - private static TOut SerializeAndDeserializeBson(TIn source) - { - var stream = new MemoryStream(); - - using (var writer = new BsonBinaryWriter(stream)) - { - BsonSerializer.Serialize(writer, source); - - writer.Flush(); - } - - stream.Position = 0; - - using (var reader = new BsonBinaryReader(stream)) - { - var target = BsonSerializer.Deserialize(reader); - - return target; - } - } } }