From c4db9a8c63d3bab7d4c42cd60a608f18faa46b31 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sun, 24 Jul 2022 17:02:42 +0200 Subject: [PATCH] System text json (#902) * Started conversion. * Made progress. * More fixes. * Fix tests * Fix null writing. * More fixes * Fix rule formatting. --- .../Actions/Algolia/AlgoliaActionHandler.cs | 45 ++- .../ElasticSearchActionHandler.cs | 31 +- .../Squidex.Extensions.csproj | 1 - .../Text/Azure/CommandFactory.cs | 6 +- .../Text/ElasticSearch/CommandFactory.cs | 6 +- .../ElasticSearch/ElasticSearchTextIndex.cs | 8 +- .../ElasticSearch/ElasticSearchTextPlugin.cs | 3 +- .../Migrations/ConvertEventStore.cs | 17 +- .../Migrations/ConvertEventStoreAppId.cs | 23 +- .../MongoDb/AddAppIdToEventStream.cs | 12 +- .../Migrations/MongoDb/ConvertDocumentIds.cs | 9 +- .../MongoDb/ConvertOldSnapshotStores.cs | 15 +- .../MongoDb/ConvertRuleEventsJson.cs | 7 +- .../Migrations/MongoDb/RenameAssetMetadata.cs | 70 ++-- .../MongoDb/RenameAssetSlugField.cs | 6 +- .../MongoDb/RestructureContentCollection.cs | 5 +- .../Contents/GeoJsonValue.cs | 11 +- .../Json/ContentFieldDataConverter.cs | 48 +-- .../Contents/Json/WorkflowStepSurrogate.cs | 6 +- .../EnrichedEvents/EnrichedCommentEvent.cs | 6 +- .../EnrichedEvents/EnrichedUserEventBase.cs | 4 +- .../Squidex.Domain.Apps.Core.Model.csproj | 1 + .../HandleRules/RuleEventFormatter.cs | 20 +- .../Extensions/StringFluidExtension.cs | 8 +- .../Apps/MongoAppRepository.cs | 5 +- .../Assets/MongoAssetEntity.cs | 1 - ...ongoAssetFolderRepository_SnapshotStore.cs | 2 +- .../MongoAssetRepository_SnapshotStore.cs | 2 +- .../Contents/MongoContentCollection.cs | 2 +- .../Contents/MongoContentEntity.cs | 2 - .../Contents/MongoContentRepository.cs | 2 +- .../Contents/Operations/Adapt.cs | 2 +- .../MongoCountCollection.cs | 4 +- .../Rules/MongoRuleRepository.cs | 5 +- .../Schemas/MongoSchemaRepository.cs | 5 +- .../Schemas/MongoSchemasHash.cs | 4 +- .../Text/AtlasTextIndex.cs | 4 +- .../Text/CommandFactory.cs | 8 +- .../Text/MongoTextIndex.cs | 4 +- .../Text/MongoTextIndexBase.cs | 25 +- .../Text/MongoTextIndexEntity.cs | 4 +- .../Text/MongoTextIndexerState.cs | 4 +- .../Apps/Commands/AppCommand.cs | 2 - .../Apps/Commands/AppUpdateCommand.cs | 2 - .../Apps/Commands/CreateApp.cs | 4 +- .../DomainObject/AppDomainObject.State.cs | 4 +- .../Apps/Plans/UsageNotifierWorker.cs | 2 +- .../Assets/Commands/AssetCommand.cs | 2 - .../Assets/Commands/AssetFolderCommand.cs | 2 - .../DomainObject/AssetDomainObject.State.cs | 6 +- .../AssetFolderDomainObject.State.cs | 4 +- .../Backup/Model/CompatibleStoredEvent.cs | 66 ++-- .../Backup/State/BackupState.cs | 2 +- .../Contents/Commands/ContentCommand.cs | 2 - .../DomainObject/ContentDomainObject.State.cs | 12 +- .../Contents/Text/Extensions.cs | 8 +- .../Contents/Text/UpsertIndexEntry.cs | 4 +- .../DomainObject/RuleDomainObject.State.cs | 4 +- .../Schemas/Commands/CreateSchema.cs | 1 - .../Schemas/Commands/SchemaUpdateCommand.cs | 1 - .../DomainObject/SchemaDomainObject.State.cs | 4 +- .../Caching/MongoDistributedCache.cs | 4 +- .../EventSourcing/MongoEvent.cs | 4 +- .../EventSourcing/MongoEventStore.cs | 4 - .../EventSourcing/MongoEventStore_Reader.cs | 20 +- .../EventSourcing/MongoEventStore_Writer.cs | 8 +- .../MongoDb/Batching.cs | 6 +- ...erializer.cs => BsonDomainIdSerializer.cs} | 6 +- ...Serializer.cs => BsonInstantSerializer.cs} | 26 +- .../MongoDb/BsonJsonConvention.cs | 15 +- .../MongoDb/BsonJsonReader.cs | 105 ------ .../MongoDb/BsonJsonSerializer.cs | 170 ++++++++- .../MongoDb/BsonJsonValueSerializer.cs | 91 +++++ .../MongoDb/BsonJsonWriter.cs | 190 ---------- ...gSerializer.cs => BsonStringSerializer.cs} | 4 +- .../MongoDb/FieldDefinitionBuilder.cs | 31 -- .../MongoDb/MongoBase.cs | 20 +- .../MongoDb/MongoRepositoryBase.cs | 28 +- .../States/MongoSnapshotStore.cs | 5 +- .../States/MongoSnapshotStoreBase.cs | 16 +- .../Collections/ReadonlyDictionary.cs | 2 - .../EventSourcing/DefaultEventFormatter.cs | 2 +- .../Json/IJsonSerializer.cs | 6 +- .../Newtonsoft/ConverterContractResolver.cs | 98 ------ .../Json/Newtonsoft/JsonClassConverter.cs | 49 --- .../Json/Newtonsoft/JsonValueConverter.cs | 207 ----------- .../Newtonsoft/NewtonsoftJsonSerializer.cs | 98 ------ .../Json/Newtonsoft/SurrogateConverter.cs | 30 -- .../Newtonsoft/TypeConverterJsonConverter.cs | 50 --- .../Newtonsoft/TypeNameSerializationBinder.cs | 45 --- .../Newtonsoft/WriteonlyGeoJsonConverter.cs | 16 - .../Json/System/InheritanceConverter.cs | 54 +++ .../Json/System/InheritanceConverterBase.cs | 95 +++++ .../Json/System/JsonValueConverter.cs | 72 ++++ .../ReadonlyDictionaryConverterFactory.cs | 69 ++++ .../System/ReadonlyListConverterFactory.cs | 65 ++++ .../Json/System/ReflectionHelper.cs | 46 +++ .../Json/System/StringConverter.cs | 62 ++++ .../Json/System/SurrogateJsonConverter.cs | 31 ++ .../Json/System/SystemJsonSerializer.cs | 108 ++++++ .../Json/System/UnsafeRawJsonConverter.cs | 27 ++ .../Squidex.Infrastructure.csproj | 2 - .../StringExtensions.cs | 22 +- .../ApiModelValidationAttribute.cs | 4 +- .../GraphQL/BufferingGraphQLSerializer.cs | 61 ---- ...nverter.cs => JsonInheritanceConverter.cs} | 43 ++- .../Json/JsonInheritanceConverterAttribute.cs | 22 ++ backend/src/Squidex.Web/Resource.cs | 4 +- backend/src/Squidex.Web/Squidex.Web.csproj | 2 +- .../Api/Config/OpenApi/OpenApiServices.cs | 13 +- .../Apps/Models/ContributorsDto.cs | 6 +- .../Api/Controllers/Assets/Models/AssetDto.cs | 6 +- .../Squidex/Areas/Api/Controllers/QueryDto.cs | 10 +- .../Controllers/Rules/Models/CreateRuleDto.cs | 2 +- .../Rules/Models/RuleActionConverter.cs | 2 +- .../Rules/Models/RuleActionProcessor.cs | 18 +- .../Api/Controllers/Rules/Models/RuleDto.cs | 2 +- .../Rules/Models/RuleTriggerDto.cs | 3 +- .../Controllers/Rules/Models/UpdateRuleDto.cs | 2 +- .../Schemas/Models/FieldPropertiesDto.cs | 3 +- .../Models/Fields/AssetsFieldPropertiesDto.cs | 2 +- .../Fields/BooleanFieldPropertiesDto.cs | 2 +- .../Fields/DateTimeFieldPropertiesDto.cs | 2 +- .../Models/Fields/NumberFieldPropertiesDto.cs | 2 +- .../Fields/ReferencesFieldPropertiesDto.cs | 2 +- .../Models/Fields/StringFieldPropertiesDto.cs | 2 +- .../Models/Fields/TagsFieldPropertiesDto.cs | 2 +- .../Areas/Api/Controllers/UI/MyUIOptions.cs | 25 +- .../Areas/Api/Controllers/UI/UIController.cs | 14 +- .../Frontend/Middlewares/IndexExtensions.cs | 39 +-- .../Config/IdentityServerServices.cs | 10 +- .../Config/Domain/SerializationServices.cs | 104 +++--- .../Squidex/Config/Domain/StoreServices.cs | 8 +- .../Config/Messaging/MessagingServices.cs | 4 +- backend/src/Squidex/Config/Web/WebServices.cs | 21 +- backend/src/Squidex/Squidex.csproj | 8 +- .../Model/Rules/RuleTests.cs | 6 - .../Model/Schemas/SchemaTests.cs | 3 +- .../GenerateFilters/FiltersTests.cs | 2 +- .../GenerateJsonSchema/JsonSchemaTests.cs | 6 +- .../Squidex.Domain.Apps.Core.Tests.csproj | 4 +- .../TestHelpers/TestSchema.cs | 105 ++++++ .../TestHelpers/TestUtils.cs | 249 +++++++------- .../Apps/DomainObject/AppState.json | 133 +++++++ .../Apps/DomainObject/AppStateTests.cs | 42 +++ .../Assets/MongoDb/AssetQueryTests.cs | 8 +- .../Assets/MongoDb/AssetsQueryFixture.cs | 9 +- .../Contents/GraphQL/GraphQLMutationTests.cs | 12 +- .../Contents/GraphQL/GraphQLQueriesTests.cs | 1 + .../Contents/GraphQL/GraphQLTestBase.cs | 12 +- .../Contents/MongoDb/ContentQueryTests.cs | 8 +- .../Contents/MongoDb/ContentsQueryFixture.cs | 9 +- .../Contents/MongoDb/StatusSerializerTests.cs | 2 +- .../Queries/ContentQueryParserTests.cs | 2 +- .../Contents/Text/AtlasTextIndexFixture.cs | 21 +- .../Contents/Text/AzureTextIndexFixture.cs | 14 +- .../Contents/Text/ElasticTextIndexFixture.cs | 18 +- .../Contents/Text/MongoTextIndexFixture.cs | 21 +- .../Schemas/DomainObject/SchemaState.json | 324 ++++++++++++++++++ .../Schemas/DomainObject/SchemaStateTests.cs | 42 +++ .../Schemas/MongoDb/SchemasHashFixture.cs | 2 +- .../Squidex.Domain.Apps.Entities.Tests.csproj | 8 +- .../Collections/ListDictionaryTests.cs | 16 + .../Collections/ReadonlyDictionaryTests.cs | 23 ++ .../Collections/ReadonlyListTests.cs | 30 +- .../Commands/CommandResultTests.cs | 12 +- .../DomainIdTests.cs | 17 +- .../DefaultEventFormatterTests.cs | 3 +- .../EventSourcing/GetEventStoreFixture.cs | 10 +- .../EventSourcing/MongoEventStoreFixture.cs | 17 +- .../EventSourcing/MongoParallelInsertTests.cs | 1 - .../ConverterContractResolverTests.cs | 104 ------ .../Objects/JsonValuesSerializationTests.cs | 87 +++-- .../JsonInheritanceConverterBaseTests.cs | 103 ++++++ .../System/JsonInheritanceConverterTests.cs | 90 +++++ .../ReadOnlyCollectionTests.cs | 26 +- .../MongoDb/BsonJsonSerializerTests.cs | 65 +--- .../MongoDb/DomainIdSerializerTests.cs | 2 +- .../MongoDb/InstantSerializerTests.cs | 2 +- .../MongoDb/MongoQueryTests.cs | 6 +- .../TypeConverterStringSerializerTests.cs | 4 +- .../Squidex.Infrastructure.Tests.csproj | 2 +- .../StringExtensionsTests.cs | 8 + .../TestHelpers/BinaryFormatterHelper.cs | 30 -- .../TestHelpers/TestUtils.cs | 131 +++++-- .../apps/pages/apps-page.component.ts | 2 +- .../editors/localized-input.component.html | 3 +- .../app/shared/components/notifo.component.ts | 2 +- .../internal/notifications-menu.component.ts | 2 +- .../pages/internal/profile-menu.component.ts | 2 +- 190 files changed, 2819 insertions(+), 2088 deletions(-) rename backend/src/Squidex.Infrastructure.MongoDb/MongoDb/{DomainIdSerializer.cs => BsonDomainIdSerializer.cs} (87%) rename backend/src/Squidex.Infrastructure.MongoDb/MongoDb/{InstantSerializer.cs => BsonInstantSerializer.cs} (77%) delete mode 100644 backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonReader.cs create mode 100644 backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonValueSerializer.cs delete mode 100644 backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonWriter.cs rename backend/src/Squidex.Infrastructure.MongoDb/MongoDb/{TypeConverterStringSerializer.cs => BsonStringSerializer.cs} (91%) delete mode 100644 backend/src/Squidex.Infrastructure.MongoDb/MongoDb/FieldDefinitionBuilder.cs delete mode 100644 backend/src/Squidex.Infrastructure/Json/Newtonsoft/ConverterContractResolver.cs delete mode 100644 backend/src/Squidex.Infrastructure/Json/Newtonsoft/JsonClassConverter.cs delete mode 100644 backend/src/Squidex.Infrastructure/Json/Newtonsoft/JsonValueConverter.cs delete mode 100644 backend/src/Squidex.Infrastructure/Json/Newtonsoft/NewtonsoftJsonSerializer.cs delete mode 100644 backend/src/Squidex.Infrastructure/Json/Newtonsoft/SurrogateConverter.cs delete mode 100644 backend/src/Squidex.Infrastructure/Json/Newtonsoft/TypeConverterJsonConverter.cs delete mode 100644 backend/src/Squidex.Infrastructure/Json/Newtonsoft/TypeNameSerializationBinder.cs delete mode 100644 backend/src/Squidex.Infrastructure/Json/Newtonsoft/WriteonlyGeoJsonConverter.cs create mode 100644 backend/src/Squidex.Infrastructure/Json/System/InheritanceConverter.cs create mode 100644 backend/src/Squidex.Infrastructure/Json/System/InheritanceConverterBase.cs create mode 100644 backend/src/Squidex.Infrastructure/Json/System/JsonValueConverter.cs create mode 100644 backend/src/Squidex.Infrastructure/Json/System/ReadonlyDictionaryConverterFactory.cs create mode 100644 backend/src/Squidex.Infrastructure/Json/System/ReadonlyListConverterFactory.cs create mode 100644 backend/src/Squidex.Infrastructure/Json/System/ReflectionHelper.cs create mode 100644 backend/src/Squidex.Infrastructure/Json/System/StringConverter.cs create mode 100644 backend/src/Squidex.Infrastructure/Json/System/SurrogateJsonConverter.cs create mode 100644 backend/src/Squidex.Infrastructure/Json/System/SystemJsonSerializer.cs create mode 100644 backend/src/Squidex.Infrastructure/Json/System/UnsafeRawJsonConverter.cs delete mode 100644 backend/src/Squidex.Web/GraphQL/BufferingGraphQLSerializer.cs rename backend/src/Squidex.Web/Json/{TypedJsonInheritanceConverter.cs => JsonInheritanceConverter.cs} (66%) create mode 100644 backend/src/Squidex.Web/Json/JsonInheritanceConverterAttribute.cs create mode 100644 backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/TestSchema.cs create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppState.json create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppStateTests.cs create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/SchemaState.json create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/SchemaStateTests.cs delete mode 100644 backend/tests/Squidex.Infrastructure.Tests/Json/Newtonsoft/ConverterContractResolverTests.cs create mode 100644 backend/tests/Squidex.Infrastructure.Tests/Json/System/JsonInheritanceConverterBaseTests.cs create mode 100644 backend/tests/Squidex.Infrastructure.Tests/Json/System/JsonInheritanceConverterTests.cs rename backend/tests/Squidex.Infrastructure.Tests/Json/{Newtonsoft => System}/ReadOnlyCollectionTests.cs (55%) delete mode 100644 backend/tests/Squidex.Infrastructure.Tests/TestHelpers/BinaryFormatterHelper.cs diff --git a/backend/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaActionHandler.cs index 48fa7720f..3a05869c3 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaActionHandler.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaActionHandler.cs @@ -5,12 +5,13 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Text.Json.Serialization; using Algolia.Search.Clients; -using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Infrastructure.Json; #pragma warning disable IDE0059 // Value assigned to symbol is never used #pragma warning disable MA0048 // File name must match type name @@ -21,8 +22,9 @@ namespace Squidex.Extensions.Actions.Algolia { private readonly ClientPool<(string AppId, string ApiKey, string IndexName), ISearchIndex> clients; private readonly IScriptEngine scriptEngine; + private readonly IJsonSerializer serializer; - public AlgoliaActionHandler(RuleEventFormatter formatter, IScriptEngine scriptEngine) + public AlgoliaActionHandler(RuleEventFormatter formatter, IScriptEngine scriptEngine, IJsonSerializer serializer) : base(formatter) { clients = new ClientPool<(string AppId, string ApiKey, string IndexName), ISearchIndex>(key => @@ -33,6 +35,7 @@ namespace Squidex.Extensions.Actions.Algolia }); this.scriptEngine = scriptEngine; + this.serializer = serializer; } protected override async Task<(string Description, AlgoliaJob Data)> CreateJobAsync(EnrichedEvent @event, AlgoliaAction action) @@ -43,7 +46,7 @@ namespace Squidex.Extensions.Actions.Algolia var ruleDescription = string.Empty; var contentId = entityEvent.Id.ToString(); - var content = (JObject)null; + var content = (AlgoliaContent)null; if (delete) { @@ -67,21 +70,27 @@ namespace Squidex.Extensions.Actions.Algolia jsonString = ToJson(@event); } - content = JObject.Parse(jsonString); + content = serializer.Deserialize(jsonString); } catch (Exception ex) { - content = new JObject(new JProperty("error", $"Invalid JSON: {ex.Message}")); + content = new AlgoliaContent + { + More = new Dictionary + { + ["error"] = $"Invalid JSON: {ex.Message}" + } + }; } - content["objectID"] = contentId; + content.ObjectID = contentId; } var ruleJob = new AlgoliaJob { AppId = action.AppId, ApiKey = action.ApiKey, - Content = content, + Content = serializer.Serialize(content, true), ContentId = contentId, IndexName = await FormatAsync(action.IndexName, @event) }; @@ -106,15 +115,20 @@ namespace Squidex.Extensions.Actions.Algolia { if (job.Content != null) { - var response = await index.SaveObjectAsync(job.Content, null, ct, true); + var raw = new[] + { + new JRaw(job.Content) + }; - return Result.Success(JsonConvert.SerializeObject(response, Formatting.Indented)); + var response = await index.SaveObjectsAsync(raw, null, ct, true); + + return Result.Success(serializer.Serialize(response, true)); } else { var response = await index.DeleteObjectAsync(job.ContentId, null, ct); - return Result.Success(JsonConvert.SerializeObject(response, Formatting.Indented)); + return Result.Success(serializer.Serialize(response, true)); } } catch (Exception ex) @@ -124,6 +138,15 @@ namespace Squidex.Extensions.Actions.Algolia } } + public sealed class AlgoliaContent + { + [JsonPropertyName("objectID")] + public string ObjectID { get; set; } + + [JsonExtensionData] + public Dictionary More { get; set; } = new Dictionary(); + } + public sealed class AlgoliaJob { public string AppId { get; set; } @@ -134,6 +157,6 @@ namespace Squidex.Extensions.Actions.Algolia public string IndexName { get; set; } - public JObject Content { get; set; } + public string Content { get; set; } } } diff --git a/backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchActionHandler.cs index 7cb25669b..40b716df6 100644 --- a/backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchActionHandler.cs +++ b/backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchActionHandler.cs @@ -5,12 +5,13 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Text.Json.Serialization; using Elasticsearch.Net; -using Newtonsoft.Json.Linq; using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; using Squidex.Domain.Apps.Core.Scripting; using Squidex.Infrastructure; +using Squidex.Infrastructure.Json; #pragma warning disable IDE0059 // Value assigned to symbol is never used #pragma warning disable MA0048 // File name must match type name @@ -21,8 +22,9 @@ namespace Squidex.Extensions.Actions.ElasticSearch { private readonly ClientPool<(Uri Host, string Username, string Password), ElasticLowLevelClient> clients; private readonly IScriptEngine scriptEngine; + private readonly IJsonSerializer serializer; - public ElasticSearchActionHandler(RuleEventFormatter formatter, IScriptEngine scriptEngine) + public ElasticSearchActionHandler(RuleEventFormatter formatter, IScriptEngine scriptEngine, IJsonSerializer serializer) : base(formatter) { clients = new ClientPool<(Uri Host, string Username, string Password), ElasticLowLevelClient>(key => @@ -38,6 +40,7 @@ namespace Squidex.Extensions.Actions.ElasticSearch }); this.scriptEngine = scriptEngine; + this.serializer = serializer; } protected override async Task<(string Description, ElasticSearchJob Data)> CreateJobAsync(EnrichedEvent @event, ElasticSearchAction action) @@ -73,7 +76,7 @@ namespace Squidex.Extensions.Actions.ElasticSearch { ruleDescription = $"Upsert to index: {action.IndexName}"; - JObject json; + ElasticContent content; try { string jsonString; @@ -88,16 +91,22 @@ namespace Squidex.Extensions.Actions.ElasticSearch jsonString = ToJson(@event); } - json = JObject.Parse(jsonString); + content = serializer.Deserialize(jsonString); } catch (Exception ex) { - json = new JObject(new JProperty("error", $"Invalid JSON: {ex.Message}")); + content = new ElasticContent + { + More = new Dictionary + { + ["error"] = $"Invalid JSON: {ex.Message}" + } + }; } - json.AddFirst(new JProperty("contentId", contentId)); + content.ContentId = contentId; - ruleJob.Content = json.ToString(); + ruleJob.Content = serializer.Serialize(content, true); } return (ruleDescription, ruleJob); @@ -135,6 +144,14 @@ namespace Squidex.Extensions.Actions.ElasticSearch } } + public sealed class ElasticContent + { + public string ContentId { get; set; } + + [JsonExtensionData] + public Dictionary More { get; set; } = new Dictionary(); + } + public sealed class ElasticSearchJob { public string ServerHost { get; set; } diff --git a/backend/extensions/Squidex.Extensions/Squidex.Extensions.csproj b/backend/extensions/Squidex.Extensions/Squidex.Extensions.csproj index 4492ff04e..dba7c09ab 100644 --- a/backend/extensions/Squidex.Extensions/Squidex.Extensions.csproj +++ b/backend/extensions/Squidex.Extensions/Squidex.Extensions.csproj @@ -28,7 +28,6 @@ - diff --git a/backend/extensions/Squidex.Extensions/Text/Azure/CommandFactory.cs b/backend/extensions/Squidex.Extensions/Text/Azure/CommandFactory.cs index 05fceff86..44416388b 100644 --- a/backend/extensions/Squidex.Extensions/Text/Azure/CommandFactory.cs +++ b/backend/extensions/Squidex.Extensions/Text/Azure/CommandFactory.cs @@ -7,7 +7,7 @@ using System.Text; using Azure.Search.Documents.Models; -using GeoJSON.Net.Geometry; +using NetTopologySuite.Geometries; using Squidex.Domain.Apps.Entities.Contents.Text; namespace Squidex.Extensions.Text.Azure @@ -47,8 +47,8 @@ namespace Squidex.Extensions.Text.Azure type = "Point", coordinates = new[] { - point.Coordinates.Longitude, - point.Coordinates.Latitude + point.Coordinate.X, + point.Coordinate.Y } }; break; diff --git a/backend/extensions/Squidex.Extensions/Text/ElasticSearch/CommandFactory.cs b/backend/extensions/Squidex.Extensions/Text/ElasticSearch/CommandFactory.cs index 706ca6010..3d929e1fb 100644 --- a/backend/extensions/Squidex.Extensions/Text/ElasticSearch/CommandFactory.cs +++ b/backend/extensions/Squidex.Extensions/Text/ElasticSearch/CommandFactory.cs @@ -5,7 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using GeoJSON.Net.Geometry; +using NetTopologySuite.Geometries; using Squidex.Domain.Apps.Entities.Contents.Text; namespace Squidex.Extensions.Text.ElasticSearch @@ -42,8 +42,8 @@ namespace Squidex.Extensions.Text.ElasticSearch geoField = key; geoObject = new { - lat = point.Coordinates.Latitude, - lon = point.Coordinates.Longitude + lat = point.Coordinate.X, + lon = point.Coordinate.Y }; break; } diff --git a/backend/extensions/Squidex.Extensions/Text/ElasticSearch/ElasticSearchTextIndex.cs b/backend/extensions/Squidex.Extensions/Text/ElasticSearch/ElasticSearchTextIndex.cs index b7ada2896..0ac7ff32c 100644 --- a/backend/extensions/Squidex.Extensions/Text/ElasticSearch/ElasticSearchTextIndex.cs +++ b/backend/extensions/Squidex.Extensions/Text/ElasticSearch/ElasticSearchTextIndex.cs @@ -7,12 +7,12 @@ using System.Text.RegularExpressions; using Elasticsearch.Net; -using Newtonsoft.Json; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Contents; using Squidex.Domain.Apps.Entities.Contents.Text; using Squidex.Hosting; using Squidex.Infrastructure; +using Squidex.Infrastructure.Json; namespace Squidex.Extensions.Text.ElasticSearch { @@ -23,14 +23,16 @@ namespace Squidex.Extensions.Text.ElasticSearch private readonly ElasticLowLevelClient client; private readonly QueryParser queryParser = new QueryParser(ElasticSearchIndexDefinition.GetFieldPath); private readonly string indexName; + private readonly IJsonSerializer jsonSerializer; - public ElasticSearchTextIndex(string configurationString, string indexName) + public ElasticSearchTextIndex(string configurationString, string indexName, IJsonSerializer jsonSerializer) { var config = new ConnectionConfiguration(new Uri(configurationString)); client = new ElasticLowLevelClient(config); this.indexName = indexName; + this.jsonSerializer = jsonSerializer; } public Task InitializeAsync( @@ -210,7 +212,7 @@ namespace Squidex.Extensions.Text.ElasticSearch elasticQuery.query.@bool.should.Add(bySchema); } - var json = JsonConvert.SerializeObject(elasticQuery, Formatting.Indented); + var json = jsonSerializer.Serialize(elasticQuery, true); return await SearchAsync(elasticQuery, ct); } diff --git a/backend/extensions/Squidex.Extensions/Text/ElasticSearch/ElasticSearchTextPlugin.cs b/backend/extensions/Squidex.Extensions/Text/ElasticSearch/ElasticSearchTextPlugin.cs index d5bcdc52d..04fcdd164 100644 --- a/backend/extensions/Squidex.Extensions/Text/ElasticSearch/ElasticSearchTextPlugin.cs +++ b/backend/extensions/Squidex.Extensions/Text/ElasticSearch/ElasticSearchTextPlugin.cs @@ -10,6 +10,7 @@ using Microsoft.Extensions.DependencyInjection; using Squidex.Domain.Apps.Entities.Contents.Text; using Squidex.Hosting; using Squidex.Hosting.Configuration; +using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Plugins; namespace Squidex.Extensions.Text.ElasticSearch @@ -39,7 +40,7 @@ namespace Squidex.Extensions.Text.ElasticSearch } services.AddSingleton( - c => new ElasticSearchTextIndex(elasticConfiguration, indexName)); + c => new ElasticSearchTextIndex(elasticConfiguration, indexName, c.GetRequiredService())); services.AddSingleton( c => c.GetRequiredService()); diff --git a/backend/src/Migrations/Migrations/ConvertEventStore.cs b/backend/src/Migrations/Migrations/ConvertEventStore.cs index 02f41301e..53a45bb83 100644 --- a/backend/src/Migrations/Migrations/ConvertEventStore.cs +++ b/backend/src/Migrations/Migrations/ConvertEventStore.cs @@ -9,10 +9,11 @@ using MongoDB.Bson; using MongoDB.Driver; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Migrations; +using Squidex.Infrastructure.MongoDb; namespace Migrations.Migrations { - public sealed class ConvertEventStore : IMigration + public sealed class ConvertEventStore : MongoBase, IMigration { private readonly IEventStore eventStore; @@ -26,15 +27,10 @@ namespace Migrations.Migrations { if (eventStore is MongoEventStore mongoEventStore) { + // Do not resolve in constructor, because most of the time it is not executed anyway. var collection = mongoEventStore.RawCollection; - var filter = Builders.Filter; - var writes = new List>(); - var writeOptions = new BulkWriteOptions - { - IsOrdered = false - }; async Task WriteAsync(WriteModel? model, bool force) { @@ -45,13 +41,12 @@ namespace Migrations.Migrations if (writes.Count == 1000 || (force && writes.Count > 0)) { - await collection.BulkWriteAsync(writes, writeOptions, ct); - + await collection.BulkWriteAsync(writes, BulkUnordered, ct); writes.Clear(); } } - await collection.Find(new BsonDocument()).ForEachAsync(async commit => + await collection.Find(FindAll).ForEachAsync(async commit => { foreach (BsonDocument @event in commit["Events"].AsBsonArray) { @@ -61,7 +56,7 @@ namespace Migrations.Migrations @event["Metadata"] = meta; } - await WriteAsync(new ReplaceOneModel(filter.Eq("_id", commit["_id"].AsString), commit), false); + await WriteAsync(new ReplaceOneModel(Filter.Eq("_id", commit["_id"].AsString), commit), false); }, ct); await WriteAsync(null, true); diff --git a/backend/src/Migrations/Migrations/ConvertEventStoreAppId.cs b/backend/src/Migrations/Migrations/ConvertEventStoreAppId.cs index 61f54ebce..46a6cf7ff 100644 --- a/backend/src/Migrations/Migrations/ConvertEventStoreAppId.cs +++ b/backend/src/Migrations/Migrations/ConvertEventStoreAppId.cs @@ -10,10 +10,11 @@ using MongoDB.Driver; using Squidex.Infrastructure; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Migrations; +using Squidex.Infrastructure.MongoDb; namespace Migrations.Migrations { - public sealed class ConvertEventStoreAppId : IMigration + public sealed class ConvertEventStoreAppId : MongoBase, IMigration { private readonly IEventStore eventStore; @@ -27,17 +28,10 @@ namespace Migrations.Migrations { if (eventStore is MongoEventStore mongoEventStore) { + // Do not resolve in constructor, because most of the time it is not executed anyway. var collection = mongoEventStore.RawCollection; - var filter = Builders.Filter; - - var updates = Builders.Update; - var writes = new List>(); - var writeOptions = new BulkWriteOptions - { - IsOrdered = false - }; async Task WriteAsync(WriteModel? model, bool force) { @@ -48,13 +42,12 @@ namespace Migrations.Migrations if (writes.Count == 1000 || (force && writes.Count > 0)) { - await collection.BulkWriteAsync(writes, writeOptions, ct); - + await collection.BulkWriteAsync(writes, BulkUnordered, ct); writes.Clear(); } } - await collection.Find(new BsonDocument()).ForEachAsync(async commit => + await collection.Find(FindAll).ForEachAsync(async commit => { UpdateDefinition? update = null; @@ -68,11 +61,11 @@ namespace Migrations.Migrations { var appId = NamedId.Parse(appIdValue.AsString, Guid.TryParse).Id.ToString(); - var eventUpdate = updates.Set($"Events.{index}.Metadata.AppId", appId); + var eventUpdate = Update.Set($"Events.{index}.Metadata.AppId", appId); if (update != null) { - update = updates.Combine(update, eventUpdate); + update = Update.Combine(update, eventUpdate); } else { @@ -85,7 +78,7 @@ namespace Migrations.Migrations if (update != null) { - var write = new UpdateOneModel(filter.Eq("_id", commit["_id"].AsString), update); + var write = new UpdateOneModel(Filter.Eq("_id", commit["_id"].AsString), update); await WriteAsync(write, false); } diff --git a/backend/src/Migrations/Migrations/MongoDb/AddAppIdToEventStream.cs b/backend/src/Migrations/Migrations/MongoDb/AddAppIdToEventStream.cs index ffb76b8c1..48efa4782 100644 --- a/backend/src/Migrations/Migrations/MongoDb/AddAppIdToEventStream.cs +++ b/backend/src/Migrations/Migrations/MongoDb/AddAppIdToEventStream.cs @@ -16,7 +16,7 @@ using Squidex.Infrastructure.Tasks; namespace Migrations.Migrations.MongoDb { - public sealed class AddAppIdToEventStream : IMigration + public sealed class AddAppIdToEventStream : MongoBase, IMigration { private readonly IMongoDatabase database; @@ -31,6 +31,7 @@ namespace Migrations.Migrations.MongoDb const int SizeOfBatch = 1000; const int SizeOfQueue = 20; + // Do not resolve in constructor, because most of the time it is not executed anyway. var collectionV1 = database.GetCollection("Events"); var collectionV2 = database.GetCollection("Events2"); @@ -39,11 +40,6 @@ namespace Migrations.Migrations.MongoDb BoundedCapacity = SizeOfQueue * SizeOfBatch }); - var writeOptions = new BulkWriteOptions - { - IsOrdered = false - }; - var actionBlock = new ActionBlock(async batch => { try @@ -102,7 +98,7 @@ namespace Migrations.Migrations.MongoDb if (writes.Count > 0) { - await collectionV2.BulkWriteAsync(writes, writeOptions, ct); + await collectionV2.BulkWriteAsync(writes, BulkUnordered, ct); } } catch (OperationCanceledException ex) @@ -119,7 +115,7 @@ namespace Migrations.Migrations.MongoDb batchBlock.BidirectionalLinkTo(actionBlock); - await foreach (var commit in collectionV1.Find(new BsonDocument()).ToAsyncEnumerable(ct: ct)) + await foreach (var commit in collectionV1.Find(FindAll).ToAsyncEnumerable(ct: ct)) { if (!await batchBlock.SendAsync(commit, ct)) { diff --git a/backend/src/Migrations/Migrations/MongoDb/ConvertDocumentIds.cs b/backend/src/Migrations/Migrations/MongoDb/ConvertDocumentIds.cs index 885713b1b..d1b572f63 100644 --- a/backend/src/Migrations/Migrations/MongoDb/ConvertDocumentIds.cs +++ b/backend/src/Migrations/Migrations/MongoDb/ConvertDocumentIds.cs @@ -15,7 +15,7 @@ using Squidex.Infrastructure.Tasks; namespace Migrations.Migrations.MongoDb { - public sealed class ConvertDocumentIds : IMigration + public sealed class ConvertDocumentIds : MongoBase, IMigration { private readonly IMongoDatabase database; private readonly IMongoDatabase databaseContent; @@ -80,6 +80,7 @@ namespace Migrations.Migrations.MongoDb collectionNameV2 = $"{collectionNameV1}2"; collectionNameV2 = collectionNameV2.Replace("State_", "States_", StringComparison.Ordinal); + // Do not resolve in constructor, because most of the time it is not executed anyway. var collectionV1 = database.GetCollection(collectionNameV1); var collectionV2 = database.GetCollection(collectionNameV2); @@ -88,7 +89,7 @@ namespace Migrations.Migrations.MongoDb return; } - await collectionV2.DeleteManyAsync(new BsonDocument(), ct); + await collectionV2.DeleteManyAsync(FindAll, ct); var batchBlock = new BatchBlock(SizeOfBatch, new GroupingDataflowBlockOptions { @@ -126,7 +127,7 @@ namespace Migrations.Migrations.MongoDb extraAction?.Invoke(document); - var filter = Builders.Filter.Eq("_id", documentIdNew); + var filter = Filter.Eq("_id", documentIdNew); writes.Add(new ReplaceOneModel(filter, document) { @@ -153,7 +154,7 @@ namespace Migrations.Migrations.MongoDb batchBlock.BidirectionalLinkTo(actionBlock); - await foreach (var document in collectionV1.Find(new BsonDocument()).ToAsyncEnumerable(ct: ct)) + await foreach (var document in collectionV1.Find(FindAll).ToAsyncEnumerable(ct: ct)) { if (!await batchBlock.SendAsync(document, ct)) { diff --git a/backend/src/Migrations/Migrations/MongoDb/ConvertOldSnapshotStores.cs b/backend/src/Migrations/Migrations/MongoDb/ConvertOldSnapshotStores.cs index 64ca6528c..012321a48 100644 --- a/backend/src/Migrations/Migrations/MongoDb/ConvertOldSnapshotStores.cs +++ b/backend/src/Migrations/Migrations/MongoDb/ConvertOldSnapshotStores.cs @@ -8,10 +8,11 @@ using MongoDB.Bson; using MongoDB.Driver; using Squidex.Infrastructure.Migrations; +using Squidex.Infrastructure.MongoDb; namespace Migrations.Migrations.MongoDb { - public sealed class ConvertOldSnapshotStores : IMigration + public sealed class ConvertOldSnapshotStores : MongoBase, IMigration { private readonly IMongoDatabase database; @@ -23,21 +24,17 @@ namespace Migrations.Migrations.MongoDb public Task UpdateAsync( CancellationToken ct) { + // Do not resolve in constructor, because most of the time it is not executed anyway. var collections = new[] { "States_Apps", "States_Rules", "States_Schemas" - }; + }.Select(x => database.GetCollection(x)); - var update = Builders.Update.Rename("State", "Doc"); + var update = Update.Rename("State", "Doc"); - var filter = new BsonDocument(); - - return Task.WhenAll( - collections - .Select(x => database.GetCollection(x)) - .Select(x => x.UpdateManyAsync(filter, update, cancellationToken: ct))); + return Task.WhenAll(collections.Select(x => x.UpdateManyAsync(FindAll, update, cancellationToken: ct))); } } } diff --git a/backend/src/Migrations/Migrations/MongoDb/ConvertRuleEventsJson.cs b/backend/src/Migrations/Migrations/MongoDb/ConvertRuleEventsJson.cs index 6156c1167..69c7f1a18 100644 --- a/backend/src/Migrations/Migrations/MongoDb/ConvertRuleEventsJson.cs +++ b/backend/src/Migrations/Migrations/MongoDb/ConvertRuleEventsJson.cs @@ -8,10 +8,11 @@ using MongoDB.Bson; using MongoDB.Driver; using Squidex.Infrastructure.Migrations; +using Squidex.Infrastructure.MongoDb; namespace Migrations.Migrations.MongoDb { - public sealed class ConvertRuleEventsJson : IMigration + public sealed class ConvertRuleEventsJson : MongoBase, IMigration { private readonly IMongoCollection collection; @@ -23,13 +24,13 @@ namespace Migrations.Migrations.MongoDb public async Task UpdateAsync( CancellationToken ct) { - foreach (var document in collection.Find(new BsonDocument()).ToEnumerable(ct)) + foreach (var document in collection.Find(FindAll).ToEnumerable(ct)) { try { document["Job"]["actionData"] = document["Job"]["actionData"].ToBsonDocument().ToJson(); - var filter = Builders.Filter.Eq("_id", document["_id"].ToString()); + var filter = Filter.Eq("_id", document["_id"].ToString()); await collection.ReplaceOneAsync(filter, document, cancellationToken: ct); } diff --git a/backend/src/Migrations/Migrations/MongoDb/RenameAssetMetadata.cs b/backend/src/Migrations/Migrations/MongoDb/RenameAssetMetadata.cs index b93178044..88d3990df 100644 --- a/backend/src/Migrations/Migrations/MongoDb/RenameAssetMetadata.cs +++ b/backend/src/Migrations/Migrations/MongoDb/RenameAssetMetadata.cs @@ -8,10 +8,11 @@ using MongoDB.Bson; using MongoDB.Driver; using Squidex.Infrastructure.Migrations; +using Squidex.Infrastructure.MongoDb; namespace Migrations.Migrations.MongoDb { - public sealed class RenameAssetMetadata : IMigration + public sealed class RenameAssetMetadata : MongoBase, IMigration { private readonly IMongoDatabase database; @@ -23,45 +24,38 @@ namespace Migrations.Migrations.MongoDb public async Task UpdateAsync( CancellationToken ct) { + // Do not resolve in constructor, because most of the time it is not executed anyway. var collection = database.GetCollection("States_Assets"); - var createMetadata = - Builders.Update - .Set("md", new BsonDocument()); - - await collection.UpdateManyAsync(new BsonDocument(), createMetadata, cancellationToken: ct); - - var removeNullPixelInfos = - Builders.Update - .Unset("ph") - .Unset("pw"); - - await collection.UpdateManyAsync(new BsonDocument("ph", BsonValue.Create(null)), removeNullPixelInfos, cancellationToken: ct); - - var setPixelDimensions = - Builders.Update - .Rename("ph", "md.pixelHeight") - .Rename("pw", "md.pixelWidth"); - - await collection.UpdateManyAsync(new BsonDocument(), setPixelDimensions, cancellationToken: ct); - - var setTypeToImage = - Builders.Update - .Set("at", "Image"); - - await collection.UpdateManyAsync(new BsonDocument("im", true), setTypeToImage, cancellationToken: ct); - - var setTypeToUnknown = - Builders.Update - .Set("at", "Unknown"); - - await collection.UpdateManyAsync(new BsonDocument("im", false), setTypeToUnknown, cancellationToken: ct); - - var removeIsImage = - Builders.Update - .Unset("im"); - - await collection.UpdateManyAsync(new BsonDocument(), removeIsImage, cancellationToken: ct); + // Create metadata. + await collection.UpdateManyAsync(FindAll, + Update.Set("md", new BsonDocument()), + cancellationToken: ct); + + // Remove null pixel infos. + await collection.UpdateManyAsync(new BsonDocument("ph", BsonValue.Create(null)), + Update.Unset("ph").Unset("pw"), + cancellationToken: ct); + + // Set pixel metadata. + await collection.UpdateManyAsync(FindAll, + Update.Rename("ph", "md.pixelHeight").Rename("pw", "md.pixelWidth"), + cancellationToken: ct); + + // Set type to image. + await collection.UpdateManyAsync(new BsonDocument("im", true), + Update.Set("at", "Image"), + cancellationToken: ct); + + // Set type to unknown. + await collection.UpdateManyAsync(new BsonDocument("im", false), + Update.Set("at", "Unknown"), + cancellationToken: ct); + + // Remove IsImage. + await collection.UpdateManyAsync(FindAll, + Update.Unset("im"), + cancellationToken: ct); } } } diff --git a/backend/src/Migrations/Migrations/MongoDb/RenameAssetSlugField.cs b/backend/src/Migrations/Migrations/MongoDb/RenameAssetSlugField.cs index e7cf56e55..e62104194 100644 --- a/backend/src/Migrations/Migrations/MongoDb/RenameAssetSlugField.cs +++ b/backend/src/Migrations/Migrations/MongoDb/RenameAssetSlugField.cs @@ -8,10 +8,11 @@ using MongoDB.Bson; using MongoDB.Driver; using Squidex.Infrastructure.Migrations; +using Squidex.Infrastructure.MongoDb; namespace Migrations.Migrations.MongoDb { - public sealed class RenameAssetSlugField : IMigration + public sealed class RenameAssetSlugField : MongoBase, IMigration { private readonly IMongoDatabase database; @@ -23,11 +24,12 @@ namespace Migrations.Migrations.MongoDb public Task UpdateAsync( CancellationToken ct) { + // Do not resolve in constructor, because most of the time it is not executed anyway. var collection = database.GetCollection("States_Assets"); var update = Builders.Update.Rename("FileNameSlug", "Slug"); - return collection.UpdateManyAsync(new BsonDocument(), update, cancellationToken: ct); + return collection.UpdateManyAsync(FindAll, update, cancellationToken: ct); } } } diff --git a/backend/src/Migrations/Migrations/MongoDb/RestructureContentCollection.cs b/backend/src/Migrations/Migrations/MongoDb/RestructureContentCollection.cs index fdedfde64..158801459 100644 --- a/backend/src/Migrations/Migrations/MongoDb/RestructureContentCollection.cs +++ b/backend/src/Migrations/Migrations/MongoDb/RestructureContentCollection.cs @@ -12,7 +12,7 @@ using Squidex.Infrastructure.MongoDb; namespace Migrations.Migrations.MongoDb { - public sealed class RestructureContentCollection : IMigration + public sealed class RestructureContentCollection : MongoBase, IMigration { private readonly IMongoDatabase contentDatabase; @@ -28,6 +28,7 @@ namespace Migrations.Migrations.MongoDb { await contentDatabase.DropCollectionAsync("State_Contents", ct); await contentDatabase.DropCollectionAsync("State_Content_Published", ct); + await contentDatabase.RenameCollectionAsync("State_Content_Draft", "State_Contents", cancellationToken: ct); } @@ -35,7 +36,7 @@ namespace Migrations.Migrations.MongoDb { var collection = contentDatabase.GetCollection("State_Contents"); - await collection.UpdateManyAsync(new BsonDocument(), Builders.Update.Unset("dt"), cancellationToken: ct); + await collection.UpdateManyAsync(FindAll, Update.Unset("dt"), cancellationToken: ct); } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/GeoJsonValue.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/GeoJsonValue.cs index f3e174205..662b94982 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/GeoJsonValue.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/GeoJsonValue.cs @@ -5,8 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using GeoJSON.Net; -using GeoJSON.Net.Geometry; +using NetTopologySuite.Geometries; using Squidex.Infrastructure; using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Json.Objects; @@ -17,7 +16,7 @@ namespace Squidex.Domain.Apps.Core.Contents { public static class GeoJsonValue { - public static GeoJsonParseResult TryParse(JsonValue value, IJsonSerializer serializer, out GeoJSONObject? geoJSON) + public static GeoJsonParseResult TryParse(JsonValue value, IJsonSerializer serializer, out Geometry? geoJSON) { Guard.NotNull(serializer); Guard.NotNull(value); @@ -41,7 +40,7 @@ namespace Squidex.Domain.Apps.Core.Contents return GeoJsonParseResult.InvalidLongitude; } - geoJSON = new Point(new Position(lat, lon)); + geoJSON = new Point(new Coordinate(lat, lon)); return GeoJsonParseResult.Success; } @@ -49,7 +48,7 @@ namespace Squidex.Domain.Apps.Core.Contents return GeoJsonParseResult.InvalidValue; } - private static bool TryParseGeoJson(JsonObject obj, IJsonSerializer serializer, out GeoJSONObject? geoJSON) + private static bool TryParseGeoJson(JsonObject obj, IJsonSerializer serializer, out Geometry? geoJSON) { geoJSON = null; @@ -66,7 +65,7 @@ namespace Squidex.Domain.Apps.Core.Contents stream.Position = 0; - geoJSON = serializer.Deserialize(stream, null, true); + geoJSON = serializer.Deserialize(stream, null, true); return true; } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/ContentFieldDataConverter.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/ContentFieldDataConverter.cs index dd259d6df..132fd60f5 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/ContentFieldDataConverter.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/ContentFieldDataConverter.cs @@ -5,30 +5,16 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using Newtonsoft.Json; +using System.Text.Json; +using System.Text.Json.Serialization; using Squidex.Infrastructure; -using Squidex.Infrastructure.Json.Newtonsoft; using Squidex.Infrastructure.Json.Objects; namespace Squidex.Domain.Apps.Core.Contents.Json { - public sealed class ContentFieldDataConverter : JsonClassConverter + public sealed class ContentFieldDataConverter : JsonConverter { - protected override void WriteValue(JsonWriter writer, ContentFieldData value, JsonSerializer serializer) - { - writer.WriteStartObject(); - - foreach (var (key, jsonValue) in value) - { - writer.WritePropertyName(key); - - serializer.Serialize(writer, jsonValue); - } - - writer.WriteEndObject(); - } - - protected override ContentFieldData ReadValue(JsonReader reader, Type objectType, JsonSerializer serializer) + public override ContentFieldData? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { var result = new ContentFieldData(); @@ -36,15 +22,15 @@ namespace Squidex.Domain.Apps.Core.Contents.Json { switch (reader.TokenType) { - case JsonToken.PropertyName: - var propertyName = reader.Value!.ToString()!; + case JsonTokenType.PropertyName: + var propertyName = reader.GetString()!; if (!reader.Read()) { - throw new JsonSerializationException("Unexpected end when reading Object."); + throw new JsonException("Unexpected end when reading Object."); } - var value = serializer.Deserialize(reader)!; + var value = JsonSerializer.Deserialize(ref reader, options)!; if (propertyName == InvariantPartitioning.Key) { @@ -57,12 +43,26 @@ namespace Squidex.Domain.Apps.Core.Contents.Json result[propertyName] = value; break; - case JsonToken.EndObject: + case JsonTokenType.EndObject: return result; } } - throw new JsonSerializationException("Unexpected end when reading Object."); + throw new JsonException("Unexpected end when reading Object."); + } + + public override void Write(Utf8JsonWriter writer, ContentFieldData value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + + foreach (var (key, jsonValue) in value) + { + writer.WritePropertyName(key); + + JsonSerializer.Serialize(writer, jsonValue, options); + } + + writer.WriteEndObject(); } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/WorkflowStepSurrogate.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/WorkflowStepSurrogate.cs index 57de31034..8fc27a389 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/WorkflowStepSurrogate.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/WorkflowStepSurrogate.cs @@ -5,7 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using Newtonsoft.Json; +using System.Text.Json.Serialization; using Squidex.Infrastructure; using Squidex.Infrastructure.Collections; using Squidex.Infrastructure.Reflection; @@ -16,10 +16,10 @@ namespace Squidex.Domain.Apps.Core.Contents.Json { public Dictionary Transitions { get; set; } - [JsonProperty("noUpdate")] + [JsonPropertyName("noUpdate")] public bool NoUpdateFlag { get; set; } - [JsonProperty("noUpdateRules")] + [JsonPropertyName("noUpdateRules")] public NoUpdate? NoUpdate { get; set; } public string? Color { get; set; } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedCommentEvent.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedCommentEvent.cs index 89665cd89..0e7abd25f 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedCommentEvent.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedCommentEvent.cs @@ -5,7 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System.Runtime.Serialization; +using System.Text.Json.Serialization; using Squidex.Shared.Users; #pragma warning disable CA1822 // Mark members as static @@ -21,10 +21,10 @@ namespace Squidex.Domain.Apps.Core.Rules.EnrichedEvents [FieldDescription(nameof(FieldDescriptions.CommentUrl))] public Uri? Url { get; set; } - [FieldDescription(nameof(FieldDescriptions.CommentMentionedUser)), IgnoreDataMember] + [FieldDescription(nameof(FieldDescriptions.CommentMentionedUser)), JsonIgnore] public IUser MentionedUser { get; set; } - [IgnoreDataMember] + [JsonIgnore] public override long Partition { get => MentionedUser?.Id.GetHashCode(StringComparison.Ordinal) ?? 0; diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedUserEventBase.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedUserEventBase.cs index 724d3b475..26652d1a8 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedUserEventBase.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedUserEventBase.cs @@ -5,7 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System.Runtime.Serialization; +using System.Text.Json.Serialization; using Squidex.Infrastructure; using Squidex.Shared.Users; @@ -19,7 +19,7 @@ namespace Squidex.Domain.Apps.Core.Rules.EnrichedEvents [FieldDescription(nameof(FieldDescriptions.Actor))] public RefToken Actor { get; set; } - [FieldDescription(nameof(FieldDescriptions.User)), IgnoreDataMember] + [FieldDescription(nameof(FieldDescriptions.User)), JsonIgnore] public IUser? User { get; set; } public bool ShouldSerializeUser() diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj b/backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj index ff6dd9636..efc400447 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj @@ -17,6 +17,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs index 0125a04aa..c7ad9c05b 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs @@ -9,11 +9,12 @@ using System.Globalization; using System.Security.Claims; using System.Text; using System.Text.RegularExpressions; -using Newtonsoft.Json; +using NodaTime; using NodaTime.Text; using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Core.Templates; +using Squidex.Infrastructure; using Squidex.Infrastructure.Json; using Squidex.Shared; using Squidex.Shared.Identity; @@ -79,16 +80,20 @@ namespace Squidex.Domain.Apps.Core.HandleRules public virtual string ToPayload(T @event) { - var payload = @event; - - return serializer.Serialize(payload); + // Just serialize the payload. + return serializer.Serialize(@event, true); } public virtual string ToEnvelope(EnrichedEvent @event) { - var payload = new { type = @event.Name, payload = @event, timestamp = @event.Timestamp }; + // Use the overloard with object to serialize a concrete type. + return ToEnvelope(@event.Name, @event, @event.Timestamp); + } - return serializer.Serialize(payload); + public virtual string ToEnvelope(string type, object payload, Instant timestamp) + { + // Provide this overload with object to serialize the derived type and not the static type. + return serializer.Serialize(new { type, payload, timestamp }, true); } public async ValueTask FormatAsync(string text, EnrichedEvent @event) @@ -288,8 +293,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules text = text.ToUpperInvariant(); break; case "escape": - text = JsonConvert.ToString(text); - text = text[1..^1]; + text = text.JsonEscape(); break; case "slugify": text = text.Slugify(); diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Templates/Extensions/StringFluidExtension.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Templates/Extensions/StringFluidExtension.cs index 47b32b446..e6cb0f0b6 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Templates/Extensions/StringFluidExtension.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Templates/Extensions/StringFluidExtension.cs @@ -7,7 +7,6 @@ using Fluid; using Fluid.Values; -using Newtonsoft.Json; using Squidex.Infrastructure; using Squidex.Text; @@ -29,12 +28,7 @@ namespace Squidex.Domain.Apps.Core.Templates.Extensions private static readonly FilterDelegate Escape = (input, arguments, context) => { - var result = input.ToStringValue(); - - result = JsonConvert.ToString(result); - result = result[1..^1]; - - return FluidValue.Create(result); + return FluidValue.Create(input.ToStringValue().JsonEscape()); }; private static readonly FilterDelegate Markdown2Text = (input, arguments, context) => diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs index 13f669d6d..5ed647b2e 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs @@ -6,7 +6,6 @@ // ========================================================================== using MongoDB.Driver; -using Newtonsoft.Json; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps.DomainObject; using Squidex.Domain.Apps.Entities.Apps.Repositories; @@ -18,8 +17,8 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Apps { public sealed class MongoAppRepository : MongoSnapshotStoreBase, IAppRepository, IDeleter { - public MongoAppRepository(IMongoDatabase database, JsonSerializer serializer) - : base(database, serializer) + public MongoAppRepository(IMongoDatabase database) + : base(database) { } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs index 04797ca5d..715e89059 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs @@ -103,7 +103,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets [BsonElement("dl")] public bool IsDeleted { get; set; } - [BsonJson] [BsonRequired] [BsonElement("md")] public AssetMetadata Metadata { get; set; } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderRepository_SnapshotStore.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderRepository_SnapshotStore.cs index fb25f4bc8..9319377f8 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderRepository_SnapshotStore.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderRepository_SnapshotStore.cs @@ -28,7 +28,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets IAsyncEnumerable> ISnapshotStore.ReadAllAsync( CancellationToken ct) { - return Collection.Find(new BsonDocument(), Batching.Options).ToAsyncEnumerable(ct) + return Collection.Find(FindAll, Batching.Options).ToAsyncEnumerable(ct) .Select(x => new SnapshotResult(x.DocumentId, x.ToState(), x.Version, true)); } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs index 44f355836..0127ed81b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs @@ -28,7 +28,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets IAsyncEnumerable> ISnapshotStore.ReadAllAsync( CancellationToken ct) { - return Collection.Find(new BsonDocument(), Batching.Options).ToAsyncEnumerable(ct) + return Collection.Find(FindAll, Batching.Options).ToAsyncEnumerable(ct) .Select(x => new SnapshotResult(x.DocumentId, x.ToState(), x.Version)); } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs index b56d0b0e0..4b6d2b551 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs @@ -248,7 +248,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents public IAsyncEnumerable StreamAll( CancellationToken ct) { - return Collection.Find(new BsonDocument()).ToAsyncEnumerable(ct); + return Collection.Find(FindAll).ToAsyncEnumerable(ct); } public async Task UpsertVersionedAsync(DomainId documentId, long oldVersion, MongoContentEntity value, diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs index e787cf867..60b9cb629 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs @@ -59,12 +59,10 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents [BsonIgnoreIfNull] [BsonElement("do")] - [BsonJson] public ContentData Data { get; set; } [BsonIgnoreIfNull] [BsonElement("dd")] - [BsonJson] public ContentData? DraftData { get; set; } [BsonIgnoreIfNull] diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs index 3dec39767..810b8f6a3 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs @@ -28,7 +28,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents static MongoContentRepository() { - TypeConverterStringSerializer.Register(); + BsonStringSerializer.Register(); } public MongoContentRepository(IMongoDatabase database, IAppProvider appProvider, diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/Adapt.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/Adapt.cs index 6c65c0e1d..ba633ab1b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/Adapt.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/Adapt.cs @@ -68,7 +68,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations for (var i = 1; i < path.Count; i++) { - result[i] = result[i].UnescapeEdmField().EscapeJson(); + result[i] = result[i].UnescapeEdmField().JsonEscape(); } return result; diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/MongoCountCollection.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/MongoCountCollection.cs index 07146b486..8d2d38100 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/MongoCountCollection.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/MongoCountCollection.cs @@ -20,8 +20,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb : base(database) { this.name = $"{name}_Count"; - - InitializeAsync(default).Wait(); } protected override string CollectionName() @@ -34,8 +32,10 @@ namespace Squidex.Domain.Apps.Entities.MongoDb { var (cachedTotal, isOutdated) = await CountAsync(key, ct); + // This is our hardcoded limit at the moment. Usually schemas are much smaller anyway. if (cachedTotal < 5_000) { + // We always want to have up to date collection sizes for smaller schemas. return await RefreshTotalAsync(key, cachedTotal, provider, ct); } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs index 2aaa55c7a..9fa28e72e 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs @@ -6,7 +6,6 @@ // ========================================================================== using MongoDB.Driver; -using Newtonsoft.Json; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Rules; using Squidex.Domain.Apps.Entities.Rules.DomainObject; @@ -18,8 +17,8 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Rules { public sealed class MongoRuleRepository : MongoSnapshotStoreBase, IRuleRepository, IDeleter { - public MongoRuleRepository(IMongoDatabase database, JsonSerializer serializer) - : base(database, serializer) + public MongoRuleRepository(IMongoDatabase database) + : base(database) { } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository.cs index 08c553d2b..8a217217b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository.cs @@ -6,7 +6,6 @@ // ========================================================================== using MongoDB.Driver; -using Newtonsoft.Json; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas.DomainObject; @@ -18,8 +17,8 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Schemas { public sealed class MongoSchemaRepository : MongoSnapshotStoreBase, ISchemaRepository, IDeleter { - public MongoSchemaRepository(IMongoDatabase database, JsonSerializer serializer) - : base(database, serializer) + public MongoSchemaRepository(IMongoDatabase database) + : base(database) { } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemasHash.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemasHash.cs index 409901e8e..3918c9241 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemasHash.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemasHash.cs @@ -39,8 +39,8 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Schemas get => "^schema-"; } - public MongoSchemasHash(IMongoDatabase database, bool setup = false) - : base(database, setup) + public MongoSchemasHash(IMongoDatabase database) + : base(database) { } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/AtlasTextIndex.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/AtlasTextIndex.cs index 0b42c259d..a54376561 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/AtlasTextIndex.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/AtlasTextIndex.cs @@ -28,8 +28,8 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Text private readonly AtlasOptions options; private string index; - public AtlasTextIndex(IMongoDatabase database, IOptions options, bool setup = false) - : base(database, setup) + public AtlasTextIndex(IMongoDatabase database, IOptions options) + : base(database) { this.options = options.Value; } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/CommandFactory.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/CommandFactory.cs index 8835f10eb..75456b126 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/CommandFactory.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/CommandFactory.cs @@ -7,16 +7,12 @@ using MongoDB.Driver; using Squidex.Domain.Apps.Entities.Contents.Text; +using Squidex.Infrastructure.MongoDb; namespace Squidex.Domain.Apps.Entities.MongoDb.Text { - public sealed class CommandFactory where T : class + public sealed class CommandFactory : MongoBase> where T : class { -#pragma warning disable RECS0108 // Warns about static fields in generic types - private static readonly FilterDefinitionBuilder> Filter = Builders>.Filter; - private static readonly UpdateDefinitionBuilder> Update = Builders>.Update; -#pragma warning restore RECS0108 // Warns about static fields in generic types - private readonly Func, T> textBuilder; public CommandFactory(Func, T> textBuilder) diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/MongoTextIndex.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/MongoTextIndex.cs index bbf50906c..aabe13c32 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/MongoTextIndex.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/MongoTextIndex.cs @@ -11,8 +11,8 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Text { public sealed class MongoTextIndex : MongoTextIndexBase> { - public MongoTextIndex(IMongoDatabase database, bool setup = false) - : base(database, setup) + public MongoTextIndex(IMongoDatabase database) + : base(database) { } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/MongoTextIndexBase.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/MongoTextIndexBase.cs index 9504580bf..a7b59d0f6 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/MongoTextIndexBase.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/MongoTextIndexBase.cs @@ -18,8 +18,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Text { public abstract class MongoTextIndexBase : MongoRepositoryBase>, ITextIndex, IDeleter where T : class { - private readonly ProjectionDefinition> searchTextProjection; - private readonly ProjectionDefinition> searchGeoProjection; private readonly CommandFactory commandFactory; protected sealed class MongoTextResult @@ -39,12 +37,9 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Text public double Score { get; set; } } - protected MongoTextIndexBase(IMongoDatabase database, bool setup = false) - : base(database, setup) + protected MongoTextIndexBase(IMongoDatabase database) + : base(database) { - searchGeoProjection = Projection.Include(x => x.ContentId); - searchTextProjection = Projection.Include(x => x.ContentId).MetaTextScore("score"); - #pragma warning disable MA0056 // Do not call overridable members in constructor commandFactory = new CommandFactory(BuildTexts); #pragma warning restore MA0056 // Do not call overridable members in constructor @@ -114,7 +109,8 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Text Filter.GeoWithinCenterSphere(x => x.GeoObject, query.Longitude, query.Latitude, query.Radius / 6378100)); var byGeo = - await GetCollection(scope).Find(findFilter).Limit(query.Take).Project(searchGeoProjection) + await GetCollection(scope).Find(findFilter).Limit(query.Take) + .Project(Projection.Include(x => x.ContentId)) .ToListAsync(ct); return byGeo.Select(x => x.ContentId).ToList(); @@ -185,15 +181,12 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Text private async Task SearchAsync(List<(DomainId, double)> result, FilterDefinition> filter, SearchScope scope, int take, double factor, CancellationToken ct = default) { - var collection = GetCollection(scope); - - var find = - collection.Find(filter).Limit(take) - .Project(searchTextProjection).Sort(Sort.MetaTextScore("score")); - - var documents = await find.ToListAsync(ct); + var byText = + await GetCollection(scope).Find(filter).Limit(take) + .Project(Projection.Include(x => x.ContentId).MetaTextScore("score")).Sort(Sort.MetaTextScore("score")) + .ToListAsync(ct); - result.AddRange(documents.Select(x => (x.ContentId, x.Score * factor))); + result.AddRange(byText.Select(x => (x.ContentId, x.Score * factor))); } private static FilterDefinition> Filter_ByScope(SearchScope scope) diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/MongoTextIndexEntity.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/MongoTextIndexEntity.cs index 6a9a07fef..d70010591 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/MongoTextIndexEntity.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/MongoTextIndexEntity.cs @@ -5,9 +5,9 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using GeoJSON.Net; using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; +using NetTopologySuite.Geometries; using Squidex.Infrastructure; using Squidex.Infrastructure.MongoDb; @@ -58,6 +58,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Text [BsonIgnoreIfNull] [BsonElement("go")] [BsonJson] - public GeoJSONObject GeoObject { get; set; } + public Geometry GeoObject { get; set; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/MongoTextIndexerState.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/MongoTextIndexerState.cs index b161069dc..0ae1e01e9 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/MongoTextIndexerState.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/MongoTextIndexerState.cs @@ -36,8 +36,8 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Text }); } - public MongoTextIndexerState(IMongoDatabase database, bool setup = false) - : base(database, setup) + public MongoTextIndexerState(IMongoDatabase database) + : base(database) { } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AppCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AppCommand.cs index 78613817a..09d401854 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AppCommand.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AppCommand.cs @@ -5,7 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System.Runtime.Serialization; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; @@ -13,7 +12,6 @@ namespace Squidex.Domain.Apps.Entities.Apps.Commands { public abstract class AppCommand : SquidexCommand, IAggregateCommand { - [IgnoreDataMember] public abstract DomainId AggregateId { get; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AppUpdateCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AppUpdateCommand.cs index b87970ac8..d384884bf 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AppUpdateCommand.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AppUpdateCommand.cs @@ -5,7 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System.Runtime.Serialization; using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Entities.Apps.Commands @@ -14,7 +13,6 @@ namespace Squidex.Domain.Apps.Entities.Apps.Commands { public NamedId AppId { get; set; } - [IgnoreDataMember] public override DomainId AggregateId { get => AppId.Id; diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/CreateApp.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/CreateApp.cs index eb1a30a53..d9fa314cc 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/CreateApp.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/CreateApp.cs @@ -5,7 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System.Runtime.Serialization; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; @@ -19,7 +18,6 @@ namespace Squidex.Domain.Apps.Entities.Apps.Commands public string? Template { get; set; } - [IgnoreDataMember] public override DomainId AggregateId { get => AppId; @@ -30,4 +28,4 @@ namespace Squidex.Domain.Apps.Entities.Apps.Commands AppId = DomainId.NewGuid(); } } -} \ No newline at end of file +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppDomainObject.State.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppDomainObject.State.cs index b4bb1c00a..d179abd12 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppDomainObject.State.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppDomainObject.State.cs @@ -5,7 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System.Runtime.Serialization; +using System.Text.Json.Serialization; using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Assets; using Squidex.Domain.Apps.Core.Contents; @@ -49,7 +49,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject public bool IsDeleted { get; set; } - [IgnoreDataMember] + [JsonIgnore] public DomainId UniqueId { get => Id; diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/UsageNotifierWorker.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/UsageNotifierWorker.cs index 348f587f1..8fb735879 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/UsageNotifierWorker.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/UsageNotifierWorker.cs @@ -25,7 +25,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans [CollectionName("UsageNotifications")] public sealed class State { - public Dictionary NotificationsSent { get; } = new Dictionary(); + public Dictionary NotificationsSent { get; set; } = new Dictionary(); } public IClock Clock { get; set; } = SystemClock.Instance; diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/AssetCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/AssetCommand.cs index b9a3388a2..095855ac5 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/AssetCommand.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/AssetCommand.cs @@ -5,7 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System.Runtime.Serialization; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; @@ -19,7 +18,6 @@ namespace Squidex.Domain.Apps.Entities.Assets.Commands public bool DoNotScript { get; set; } - [IgnoreDataMember] public DomainId AggregateId { get => DomainId.Combine(AppId, AssetId); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/AssetFolderCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/AssetFolderCommand.cs index 5cd3e2c51..f8a8d1f93 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/AssetFolderCommand.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/AssetFolderCommand.cs @@ -5,7 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System.Runtime.Serialization; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; @@ -17,7 +16,6 @@ namespace Squidex.Domain.Apps.Entities.Assets.Commands public DomainId AssetFolderId { get; set; } - [IgnoreDataMember] public DomainId AggregateId { get => DomainId.Combine(AppId, AssetFolderId); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetDomainObject.State.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetDomainObject.State.cs index 9d7424bf1..cb60ae55a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetDomainObject.State.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetDomainObject.State.cs @@ -5,7 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System.Runtime.Serialization; +using System.Text.Json.Serialization; using Squidex.Domain.Apps.Core.Assets; using Squidex.Domain.Apps.Events.Assets; using Squidex.Infrastructure; @@ -47,13 +47,13 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject public AssetType Type { get; set; } - [IgnoreDataMember] + [JsonIgnore] public DomainId AssetId { get => Id; } - [IgnoreDataMember] + [JsonIgnore] public DomainId UniqueId { get => DomainId.Combine(AppId, Id); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetFolderDomainObject.State.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetFolderDomainObject.State.cs index 186974063..2885e480a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetFolderDomainObject.State.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetFolderDomainObject.State.cs @@ -5,7 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System.Runtime.Serialization; +using System.Text.Json.Serialization; using Squidex.Domain.Apps.Events.Assets; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; @@ -26,7 +26,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject public bool IsDeleted { get; set; } - [IgnoreDataMember] + [JsonIgnore] public DomainId UniqueId { get => DomainId.Combine(AppId, Id); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/Model/CompatibleStoredEvent.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/Model/CompatibleStoredEvent.cs index 03a002aa9..a630c33cf 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/Model/CompatibleStoredEvent.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/Model/CompatibleStoredEvent.cs @@ -5,32 +5,30 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System.Globalization; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; +using System.Text.Json.Serialization; using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Json.System; -#pragma warning disable SA1401 // Fields must be private #pragma warning disable MA0048 // File name must match type name namespace Squidex.Domain.Apps.Entities.Backup.Model { public sealed class CompatibleStoredEvent { - [JsonProperty("n")] - public NewEvent NewEvent; + [JsonPropertyName("n")] + public NewEvent NewEvent { get; set; } - [JsonProperty] - public string StreamName; + [JsonPropertyName("streamName")] + public string StreamName { get; set; } - [JsonProperty] - public string EventPosition; + [JsonPropertyName("eventPosition")] + public string EventPosition { get; set; } - [JsonProperty] - public long EventStreamNumber; + [JsonPropertyName("eventStreamNumber")] + public long EventStreamNumber { get; set; } - [JsonProperty] - public CompatibleEventData Data; + [JsonPropertyName("data")] + public CompatibleEventData Data { get; set; } public static CompatibleStoredEvent V1(StoredEvent stored) { @@ -65,41 +63,45 @@ namespace Squidex.Domain.Apps.Entities.Backup.Model public sealed class CompatibleEventData { - [JsonProperty] - public string Type; + [JsonPropertyName("type")] + public string Type { get; set; } - [JsonProperty] - public JRaw Payload; + [JsonPropertyName("metadata")] + public EnvelopeHeaders EventHeaders { get; set; } - [JsonProperty] - public EnvelopeHeaders Metadata; + [JsonPropertyName("payload")] + [JsonConverter(typeof(UnsafeRawJsonConverter))] + public string EventPayload { get; set; } public static CompatibleEventData V1(EventData data) { - var payload = new JRaw(data.Payload); - - return new CompatibleEventData { Type = data.Type, Payload = payload, Metadata = data.Headers }; + return new CompatibleEventData + { + Type = data.Type, + EventPayload = data.Payload, + EventHeaders = data.Headers + }; } public EventData ToData() { - return new EventData(Type, Metadata, Payload.ToString(CultureInfo.InvariantCulture)); + return new EventData(Type, EventHeaders, EventPayload); } } public sealed class NewEvent { - [JsonProperty("t")] - public string EventType; + [JsonPropertyName("t")] + public string EventType { get; set; } - [JsonProperty("s")] - public string StreamName; + [JsonPropertyName("s")] + public string StreamName { get; set; } - [JsonProperty("p")] - public string EventPayload; + [JsonPropertyName("p")] + public string EventPayload { get; set; } - [JsonProperty("h")] - public EnvelopeHeaders EventHeaders; + [JsonPropertyName("h")] + public EnvelopeHeaders EventHeaders { get; set; } public static NewEvent V2(StoredEvent stored) { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/State/BackupState.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/State/BackupState.cs index 8da62b28b..fcb981912 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/State/BackupState.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/State/BackupState.cs @@ -12,7 +12,7 @@ namespace Squidex.Domain.Apps.Entities.Backup.State { public sealed class BackupState { - public List Jobs { get; } = new List(); + public List Jobs { get; set; } = new List(); public void EnsureCanStart() { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentCommand.cs index d9c6db1fb..89ba1fb61 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentCommand.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentCommand.cs @@ -5,7 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System.Runtime.Serialization; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; @@ -21,7 +20,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Commands public bool DoNotScript { get; set; } - [IgnoreDataMember] public DomainId AggregateId { get => DomainId.Combine(AppId, ContentId); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.State.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.State.cs index d680138b4..ad0f5fdce 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.State.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.State.cs @@ -5,7 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System.Runtime.Serialization; +using System.Text.Json.Serialization; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Events.Contents; using Squidex.Infrastructure; @@ -30,31 +30,31 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject public bool IsDeleted { get; set; } - [IgnoreDataMember] + [JsonIgnore] public DomainId UniqueId { get => DomainId.Combine(AppId, Id); } - [IgnoreDataMember] + [JsonIgnore] public ContentData Data { get => NewVersion?.Data ?? CurrentData; } - [IgnoreDataMember] + [JsonIgnore] public ContentData CurrentData { get => CurrentVersion.Data; } - [IgnoreDataMember] + [JsonIgnore] public Status? NewStatus { get => NewVersion?.Status; } - [IgnoreDataMember] + [JsonIgnore] public Status Status { get => CurrentVersion?.Status ?? default; diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/Extensions.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/Extensions.cs index 9f9d4f27d..df4939aea 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/Extensions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/Extensions.cs @@ -6,7 +6,7 @@ // ========================================================================== using System.Text; -using GeoJSON.Net; +using NetTopologySuite.Geometries; using Squidex.Domain.Apps.Core.Contents; using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Json.Objects; @@ -16,9 +16,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Text { public static class Extensions { - public static Dictionary? ToGeo(this ContentData data, IJsonSerializer serializer) + public static Dictionary? ToGeo(this ContentData data, IJsonSerializer serializer) { - Dictionary? result = null; + Dictionary? result = null; foreach (var (field, value) in data) { @@ -30,7 +30,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Text if (geoJson != null) { - result ??= new Dictionary(); + result ??= new Dictionary(); result[$"{field}.{key}"] = geoJson; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/UpsertIndexEntry.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/UpsertIndexEntry.cs index ec0aa214b..d4952cedb 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/UpsertIndexEntry.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/UpsertIndexEntry.cs @@ -5,14 +5,14 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using GeoJSON.Net; +using NetTopologySuite.Geometries; using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Entities.Contents.Text { public sealed class UpsertIndexEntry : IndexCommand { - public Dictionary? GeoObjects { get; set; } + public Dictionary? GeoObjects { get; set; } public Dictionary? Texts { get; set; } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/DomainObject/RuleDomainObject.State.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/DomainObject/RuleDomainObject.State.cs index 9daf73b3a..20a7db796 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/DomainObject/RuleDomainObject.State.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/DomainObject/RuleDomainObject.State.cs @@ -5,7 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System.Runtime.Serialization; +using System.Text.Json.Serialization; using Squidex.Domain.Apps.Core.Rules; using Squidex.Domain.Apps.Events.Rules; using Squidex.Infrastructure; @@ -25,7 +25,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.DomainObject public bool IsDeleted { get; set; } - [IgnoreDataMember] + [JsonIgnore] public DomainId UniqueId { get => DomainId.Combine(AppId, Id); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/CreateSchema.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/CreateSchema.cs index f534af351..8f8f1b5c2 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/CreateSchema.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/CreateSchema.cs @@ -40,7 +40,6 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Commands public ReadonlyDictionary? PreviewUrls { get; set; } - [IgnoreDataMember] public override DomainId AggregateId { get => DomainId.Combine(AppId, SchemaId); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/SchemaUpdateCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/SchemaUpdateCommand.cs index c5fa385a5..07750b37a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/SchemaUpdateCommand.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/SchemaUpdateCommand.cs @@ -14,7 +14,6 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Commands { public NamedId SchemaId { get; set; } - [IgnoreDataMember] public override DomainId AggregateId { get => DomainId.Combine(AppId, SchemaId.Id); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/SchemaDomainObject.State.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/SchemaDomainObject.State.cs index 3f94fcc7c..f2a7f1e79 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/SchemaDomainObject.State.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/SchemaDomainObject.State.cs @@ -5,7 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System.Runtime.Serialization; +using System.Text.Json.Serialization; using Squidex.Domain.Apps.Core; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Events.Schemas; @@ -28,7 +28,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject public bool IsDeleted { get; set; } - [IgnoreDataMember] + [JsonIgnore] public DomainId UniqueId { get => DomainId.Combine(AppId, Id); diff --git a/backend/src/Squidex.Infrastructure.MongoDb/Caching/MongoDistributedCache.cs b/backend/src/Squidex.Infrastructure.MongoDb/Caching/MongoDistributedCache.cs index 840cbfb35..b6253e572 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/Caching/MongoDistributedCache.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/Caching/MongoDistributedCache.cs @@ -13,8 +13,8 @@ namespace Squidex.Infrastructure.Caching { public sealed class MongoDistributedCache : MongoRepositoryBase, IDistributedCache { - public MongoDistributedCache(IMongoDatabase database, bool setup = false) - : base(database, setup) + public MongoDistributedCache(IMongoDatabase database) + : base(database) { } diff --git a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEvent.cs b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEvent.cs index 28d75a5f6..c75a6c8d0 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEvent.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEvent.cs @@ -16,13 +16,11 @@ namespace Squidex.Infrastructure.EventSourcing [BsonRequired] public string Type { get; set; } - [BsonJson] [BsonRequired] public string Payload { get; set; } [BsonElement("Metadata")] [BsonRequired] - [BsonJson] public EnvelopeHeaders Headers { get; set; } public static MongoEvent FromEventData(EventData data) @@ -35,4 +33,4 @@ namespace Squidex.Infrastructure.EventSourcing return new EventData(Type, Headers, Payload); } } -} \ No newline at end of file +} diff --git a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs index 7db325a12..bcc711188 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs @@ -14,10 +14,6 @@ namespace Squidex.Infrastructure.EventSourcing { public partial class MongoEventStore : MongoRepositoryBase, IEventStore { - private static readonly FieldDefinition TimestampField = FieldBuilder.Build(x => x.Timestamp); - private static readonly FieldDefinition EventsCountField = FieldBuilder.Build(x => x.EventsCount); - private static readonly FieldDefinition EventStreamOffsetField = FieldBuilder.Build(x => x.EventStreamOffset); - private static readonly FieldDefinition EventStreamField = FieldBuilder.Build(x => x.EventStream); private readonly IEventNotifier notifier; public IMongoCollection RawCollection diff --git a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Reader.cs b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Reader.cs index f703bf452..1c6904e17 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Reader.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Reader.cs @@ -47,10 +47,10 @@ namespace Squidex.Infrastructure.EventSourcing using (Telemetry.Activities.StartActivity("MongoEventStore/QueryLatestAsync")) { - var filter = Filter.Eq(EventStreamField, streamName); + var filter = Filter.Eq(x => x.EventStream, streamName); var commits = - await Collection.Find(filter).Sort(Sort.Descending(TimestampField)).Limit(count) + await Collection.Find(filter).Sort(Sort.Descending(x => x.Timestamp)).Limit(count) .ToListAsync(ct); var result = commits.Select(x => x.Filtered()).Reverse().SelectMany(x => x).TakeLast(count).ToList(); @@ -68,11 +68,11 @@ namespace Squidex.Infrastructure.EventSourcing { var filter = Filter.And( - Filter.Eq(EventStreamField, streamName), - Filter.Gte(EventStreamOffsetField, streamPosition - MaxCommitSize)); + Filter.Eq(x => x.EventStream, streamName), + Filter.Gte(x => x.EventStreamOffset, streamPosition - MaxCommitSize)); var commits = - await Collection.Find(filter).Sort(Sort.Ascending(TimestampField)) + await Collection.Find(filter).Sort(Sort.Ascending(x => x.Timestamp)) .ToListAsync(ct); var result = commits.SelectMany(x => x.Filtered(streamPosition)).ToList(); @@ -92,11 +92,11 @@ namespace Squidex.Infrastructure.EventSourcing var filter = Filter.And( - Filter.In(EventStreamField, streamNames), - Filter.Gte(EventStreamOffsetField, position)); + Filter.In(x => x.EventStream, streamNames), + Filter.Gte(x => x.EventStreamOffset, position)); var commits = - await Collection.Find(filter).Sort(Sort.Ascending(TimestampField)) + await Collection.Find(filter).Sort(Sort.Ascending(x => x.Timestamp)) .ToListAsync(ct); var result = commits.GroupBy(x => x.EventStream) @@ -122,7 +122,7 @@ namespace Squidex.Infrastructure.EventSourcing var find = Collection.Find(filterDefinition, Batching.Options) - .Limit(take).Sort(Sort.Descending(TimestampField).Ascending(EventStreamField)); + .Limit(take).Sort(Sort.Descending(x => x.Timestamp).Ascending(x => x.EventStream)); var taken = 0; @@ -162,7 +162,7 @@ namespace Squidex.Infrastructure.EventSourcing var find = Collection.Find(filterDefinition) - .Limit(take).Sort(Sort.Ascending(TimestampField).Ascending(EventStreamField)); + .Limit(take).Sort(Sort.Ascending(x => x.Timestamp).Ascending(x => x.EventStream)); var taken = 0; diff --git a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Writer.cs b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Writer.cs index 8bd3b1ee9..f58c2d2f8 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Writer.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Writer.cs @@ -129,11 +129,11 @@ namespace Squidex.Infrastructure.EventSourcing CancellationToken ct = default) { var document = - await Collection.Find(Filter.Eq(EventStreamField, streamName)) + await Collection.Find(Filter.Eq(x => x.EventStream, streamName)) .Project(Projection - .Include(EventStreamOffsetField) - .Include(EventsCountField)) - .Sort(Sort.Descending(EventStreamOffsetField)).Limit(1) + .Include(x => x.EventStreamOffset) + .Include(x => x.EventsCount)) + .Sort(Sort.Descending(x => x.EventStreamOffset)).Limit(1) .FirstOrDefaultAsync(ct); if (document != null) diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Batching.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Batching.cs index 0c224482a..8b34ffa40 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Batching.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Batching.cs @@ -11,13 +11,9 @@ namespace Squidex.Infrastructure.MongoDb { public static class Batching { - public const int BufferSize = 100; - - public const int Size = BufferSize * 2; - public static readonly FindOptions Options = new FindOptions { - BatchSize = Size + BatchSize = 200 }; } } diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/DomainIdSerializer.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonDomainIdSerializer.cs similarity index 87% rename from backend/src/Squidex.Infrastructure.MongoDb/MongoDb/DomainIdSerializer.cs rename to backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonDomainIdSerializer.cs index a7f44e04a..a10b118bb 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/DomainIdSerializer.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonDomainIdSerializer.cs @@ -11,13 +11,13 @@ using MongoDB.Bson.Serialization.Serializers; namespace Squidex.Infrastructure.MongoDb { - public sealed class DomainIdSerializer : SerializerBase, IBsonPolymorphicSerializer, IRepresentationConfigurable + public sealed class BsonDomainIdSerializer : SerializerBase, IBsonPolymorphicSerializer, IRepresentationConfigurable { public static void Register() { try { - BsonSerializer.RegisterSerializer(new DomainIdSerializer()); + BsonSerializer.RegisterSerializer(new BsonDomainIdSerializer()); } catch (BsonSerializationException) { @@ -58,7 +58,7 @@ namespace Squidex.Infrastructure.MongoDb context.Writer.WriteString(value.ToString()); } - public DomainIdSerializer WithRepresentation(BsonType representation) + public BsonDomainIdSerializer WithRepresentation(BsonType representation) { if (representation != BsonType.String) { diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/InstantSerializer.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonInstantSerializer.cs similarity index 77% rename from backend/src/Squidex.Infrastructure.MongoDb/MongoDb/InstantSerializer.cs rename to backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonInstantSerializer.cs index 92792cde5..e23cd2d4a 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/InstantSerializer.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonInstantSerializer.cs @@ -13,13 +13,13 @@ using NodaTime.Text; namespace Squidex.Infrastructure.MongoDb { - public sealed class InstantSerializer : SerializerBase, IBsonPolymorphicSerializer, IRepresentationConfigurable + public sealed class BsonInstantSerializer : SerializerBase, IBsonPolymorphicSerializer, IRepresentationConfigurable { public static void Register() { try { - BsonSerializer.RegisterSerializer(new InstantSerializer()); + BsonSerializer.RegisterSerializer(new BsonInstantSerializer()); } catch (BsonSerializationException) { @@ -34,7 +34,7 @@ namespace Squidex.Infrastructure.MongoDb public BsonType Representation { get; } - public InstantSerializer(BsonType representation = BsonType.DateTime) + public BsonInstantSerializer(BsonType representation = BsonType.DateTime) { if (representation != BsonType.DateTime && representation != BsonType.Int64 && representation != BsonType.String) { @@ -56,9 +56,10 @@ namespace Squidex.Infrastructure.MongoDb return Instant.FromUnixTimeMilliseconds(context.Reader.ReadInt64()); case BsonType.String: return InstantPattern.ExtendedIso.Parse(context.Reader.ReadString()).Value; + default: + ThrowHelper.NotSupportedException("Unsupported Representation."); + return default!; } - - throw new NotSupportedException("Unsupported Representation."); } public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, Instant value) @@ -67,21 +68,22 @@ namespace Squidex.Infrastructure.MongoDb { case BsonType.DateTime: context.Writer.WriteDateTime(value.ToUnixTimeMilliseconds()); - return; + break; case BsonType.Int64: context.Writer.WriteInt64(value.ToUnixTimeMilliseconds()); - return; + break; case BsonType.String: context.Writer.WriteString(InstantPattern.ExtendedIso.Format(value)); - return; + break; + default: + ThrowHelper.NotSupportedException("Unsupported Representation."); + break; } - - throw new NotSupportedException("Unsupported Representation."); } - public InstantSerializer WithRepresentation(BsonType representation) + public BsonInstantSerializer WithRepresentation(BsonType representation) { - return Representation == representation ? this : new InstantSerializer(representation); + return Representation == representation ? this : new BsonInstantSerializer(representation); } IBsonSerializer IRepresentationConfigurable.WithRepresentation(BsonType representation) diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConvention.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConvention.cs index c099fc00d..000aae341 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConvention.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConvention.cs @@ -6,19 +6,26 @@ // ========================================================================== using System.Reflection; +using System.Text.Json; using MongoDB.Bson; using MongoDB.Bson.Serialization; using MongoDB.Bson.Serialization.Conventions; -using Newtonsoft.Json; namespace Squidex.Infrastructure.MongoDb { public static class BsonJsonConvention { - public static void Register(JsonSerializer serializer) + public static JsonSerializerOptions Options { get; set; } = new JsonSerializerOptions(JsonSerializerDefaults.Web); + + public static void Register(JsonSerializerOptions? options = null) { try { + if (options != null) + { + Options = options; + } + var pack = new ConventionPack(); pack.AddMemberMapConvention("JsonBson", memberMap => @@ -28,7 +35,7 @@ namespace Squidex.Infrastructure.MongoDb if (attributes.OfType().Any()) { var bsonSerializerType = typeof(BsonJsonSerializer<>).MakeGenericType(memberMap.MemberType); - var bsonSerializer = Activator.CreateInstance(bsonSerializerType, serializer); + var bsonSerializer = Activator.CreateInstance(bsonSerializerType); memberMap.SetSerializer((IBsonSerializer)bsonSerializer!); } @@ -42,4 +49,4 @@ namespace Squidex.Infrastructure.MongoDb } } } -} \ No newline at end of file +} diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonReader.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonReader.cs deleted file mode 100644 index 7b035e827..000000000 --- a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonReader.cs +++ /dev/null @@ -1,105 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using MongoDB.Bson; -using MongoDB.Bson.IO; -using NewtonsoftJsonReader = Newtonsoft.Json.JsonReader; -using NewtonsoftJsonToken = Newtonsoft.Json.JsonToken; - -namespace Squidex.Infrastructure.MongoDb -{ - public sealed class BsonJsonReader : NewtonsoftJsonReader - { - private readonly IBsonReader bsonReader; - - public BsonJsonReader(IBsonReader bsonReader) - { - Guard.NotNull(bsonReader); - - this.bsonReader = bsonReader; - } - - public override bool Read() - { - if (bsonReader.State is BsonReaderState.Initial or BsonReaderState.ScopeDocument or BsonReaderState.Type) - { - bsonReader.ReadBsonType(); - } - - if (bsonReader.State == BsonReaderState.Name) - { - SetToken(NewtonsoftJsonToken.PropertyName, bsonReader.ReadName().UnescapeBson()); - } - else if (bsonReader.State == BsonReaderState.Value) - { - switch (bsonReader.CurrentBsonType) - { - case BsonType.Document: - SetToken(NewtonsoftJsonToken.StartObject); - bsonReader.ReadStartDocument(); - break; - case BsonType.Array: - SetToken(NewtonsoftJsonToken.StartArray); - bsonReader.ReadStartArray(); - break; - case BsonType.Undefined: - SetToken(NewtonsoftJsonToken.Undefined); - bsonReader.ReadUndefined(); - break; - case BsonType.Null: - SetToken(NewtonsoftJsonToken.Null); - bsonReader.ReadNull(); - break; - case BsonType.String: - SetToken(NewtonsoftJsonToken.String, bsonReader.ReadString()); - break; - case BsonType.Binary: - SetToken(NewtonsoftJsonToken.Bytes, bsonReader.ReadBinaryData().Bytes); - break; - case BsonType.Boolean: - SetToken(NewtonsoftJsonToken.Boolean, bsonReader.ReadBoolean()); - break; - case BsonType.DateTime: - SetToken(NewtonsoftJsonToken.Date, bsonReader.ReadDateTime()); - break; - case BsonType.Int32: - SetToken(NewtonsoftJsonToken.Integer, bsonReader.ReadInt32()); - break; - case BsonType.Int64: - SetToken(NewtonsoftJsonToken.Integer, bsonReader.ReadInt64()); - break; - case BsonType.Double: - SetToken(NewtonsoftJsonToken.Float, bsonReader.ReadDouble()); - break; - case BsonType.Decimal128: - SetToken(NewtonsoftJsonToken.Float, Decimal128.ToDouble(bsonReader.ReadDecimal128())); - break; - default: - ThrowHelper.NotSupportedException(); - break; - } - } - else if (bsonReader.State == BsonReaderState.EndOfDocument) - { - SetToken(NewtonsoftJsonToken.EndObject); - bsonReader.ReadEndDocument(); - } - else if (bsonReader.State == BsonReaderState.EndOfArray) - { - SetToken(NewtonsoftJsonToken.EndArray); - bsonReader.ReadEndArray(); - } - - if (bsonReader.State == BsonReaderState.Initial) - { - return true; - } - - return !bsonReader.IsAtEndOfFile(); - } - } -} diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonSerializer.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonSerializer.cs index e0263e9ae..cd45cb91b 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonSerializer.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonSerializer.cs @@ -5,24 +5,17 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Text.Json; using MongoDB.Bson; +using MongoDB.Bson.IO; using MongoDB.Bson.Serialization; using MongoDB.Bson.Serialization.Serializers; -using Newtonsoft.Json; +using Squidex.Infrastructure.ObjectPool; namespace Squidex.Infrastructure.MongoDb { public sealed class BsonJsonSerializer : ClassSerializerBase where T : class { - private readonly JsonSerializer serializer; - - public BsonJsonSerializer(JsonSerializer serializer) - { - Guard.NotNull(serializer); - - this.serializer = serializer; - } - public override T? Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) { var bsonReader = context.Reader; @@ -30,30 +23,169 @@ namespace Squidex.Infrastructure.MongoDb if (bsonReader.GetCurrentBsonType() == BsonType.Null) { bsonReader.ReadNull(); - return null; } - else + + using var stream = DefaultPools.MemoryStream.GetStream(); + + using (var writer = new Utf8JsonWriter(stream)) + { + FromBson(bsonReader, writer); + } + + stream.Position = 0; + + return JsonSerializer.Deserialize(stream, BsonJsonConvention.Options); + } + + private static void FromBson(IBsonReader reader, Utf8JsonWriter writer) + { + void ReadDocument() + { + reader.ReadStartDocument(); + writer.WriteStartObject(); + + while (reader.ReadBsonType() != BsonType.EndOfDocument) + { + Read(); + } + + writer.WriteEndObject(); + reader.ReadEndDocument(); + } + + void ReadArray() + { + reader.ReadStartArray(); + writer.WriteStartArray(); + + while (reader.ReadBsonType() != BsonType.EndOfDocument) + { + Read(); + } + + writer.WriteEndArray(); + reader.ReadEndArray(); + } + + void Read() { - var jsonReader = new BsonJsonReader(bsonReader); + switch (reader.State) + { + case BsonReaderState.Initial: + case BsonReaderState.Type: + reader.ReadBsonType(); + Read(); + break; + case BsonReaderState.Name: + writer.WritePropertyName(reader.ReadName().UnescapeBson()); + Read(); + break; + case BsonReaderState.Value: + switch (reader.CurrentBsonType) + { + case BsonType.Null: + reader.ReadNull(); + writer.WriteNullValue(); + break; + case BsonType.Binary: + var valueBinary = reader.ReadBinaryData(); + writer.WriteBase64StringValue(valueBinary.Bytes.AsSpan()); + break; + case BsonType.Boolean: + var valueBoolean = reader.ReadBoolean(); + writer.WriteBooleanValue(valueBoolean); + break; + case BsonType.Int32: + var valueInt32 = reader.ReadInt32(); + writer.WriteNumberValue(valueInt32); + break; + case BsonType.Int64: + var valueInt64 = reader.ReadInt64(); + writer.WriteNumberValue(valueInt64); + break; + case BsonType.Double: + var valueDouble = reader.ReadDouble(); + writer.WriteNumberValue(valueDouble); + break; + case BsonType.String: + var valueString = reader.ReadString(); + writer.WriteStringValue(valueString); + break; + case BsonType.Array: + ReadArray(); + break; + case BsonType.Document: + ReadDocument(); + break; + default: + throw new NotSupportedException(); + } - return serializer.Deserialize(jsonReader); + break; + case BsonReaderState.Done: + break; + case BsonReaderState.Closed: + break; + } } + + Read(); } public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, T? value) { var bsonWriter = context.Writer; - if (value == null) + using (var jsonDocument = JsonSerializer.SerializeToDocument(value, BsonJsonConvention.Options)) { - bsonWriter.WriteNull(); + WriteElement(bsonWriter, jsonDocument.RootElement); } - else + } + + private static void WriteElement(IBsonWriter writer, JsonElement element) + { + switch (element.ValueKind) { - var jsonWriter = new BsonJsonWriter(bsonWriter); + case JsonValueKind.Null: + writer.WriteNull(); + break; + case JsonValueKind.String: + writer.WriteString(element.GetString()); + break; + case JsonValueKind.Number: + writer.WriteDouble(element.GetDouble()); + break; + case JsonValueKind.True: + writer.WriteBoolean(true); + break; + case JsonValueKind.False: + writer.WriteBoolean(false); + break; + case JsonValueKind.Array: + writer.WriteStartArray(); + + foreach (var item in element.EnumerateArray()) + { + WriteElement(writer, item); + } + + writer.WriteEndArray(); + break; + case JsonValueKind.Object: + writer.WriteStartDocument(); + + foreach (var property in element.EnumerateObject()) + { + writer.WriteName(property.Name.EscapeJson()); + WriteElement(writer, property.Value); + } - serializer.Serialize(jsonWriter, value); + writer.WriteEndDocument(); + break; + default: + ThrowHelper.NotSupportedException(); + break; } } } diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonValueSerializer.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonValueSerializer.cs new file mode 100644 index 000000000..d3d137820 --- /dev/null +++ b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonValueSerializer.cs @@ -0,0 +1,91 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Serializers; +using Squidex.Infrastructure.Json.Objects; + +namespace Squidex.Infrastructure.MongoDb +{ + public sealed class BsonJsonValueSerializer : SerializerBase + { + public static void Register() + { + try + { + BsonSerializer.RegisterSerializer(new BsonJsonValueSerializer()); + } + catch (BsonSerializationException) + { + return; + } + } + + public override JsonValue Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) + { + var reader = context.Reader; + + switch (reader.CurrentBsonType) + { + case BsonType.Undefined: + reader.ReadUndefined(); + return JsonValue.Null; + case BsonType.Null: + reader.ReadNull(); + return JsonValue.Null; + case BsonType.Boolean: + return reader.ReadBoolean(); + case BsonType.Double: + return reader.ReadDouble(); + case BsonType.Int32: + return reader.ReadInt32(); + case BsonType.Int64: + return reader.ReadInt64(); + case BsonType.String: + return reader.ReadString(); + case BsonType.Array: + return BsonSerializer.Deserialize(reader); + case BsonType.Document: + return BsonSerializer.Deserialize(reader); + default: + ThrowHelper.NotSupportedException("Unsupported Representation."); + return default!; + } + } + + public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, JsonValue value) + { + var writer = context.Writer; + + switch (value.Value) + { + case null: + writer.WriteNull(); + break; + case bool b: + writer.WriteBoolean(b); + break; + case string s: + writer.WriteString(s); + break; + case double n: + writer.WriteDouble(n); + break; + case JsonArray a: + BsonSerializer.Serialize(writer, a); + break; + case JsonObject o: + BsonSerializer.Serialize(writer, o); + break; + default: + ThrowHelper.NotSupportedException(); + break; + } + } + } +} diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonWriter.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonWriter.cs deleted file mode 100644 index 400ef0747..000000000 --- a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonWriter.cs +++ /dev/null @@ -1,190 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using MongoDB.Bson.IO; -using NewtonsoftJsonWriter = Newtonsoft.Json.JsonWriter; - -namespace Squidex.Infrastructure.MongoDb -{ - public sealed class BsonJsonWriter : NewtonsoftJsonWriter - { - private readonly IBsonWriter bsonWriter; - - public BsonJsonWriter(IBsonWriter bsonWriter) - { - Guard.NotNull(bsonWriter); - - this.bsonWriter = bsonWriter; - } - - public override void WritePropertyName(string name) - { - bsonWriter.WriteName(name.EscapeJson()); - } - - public override void WritePropertyName(string name, bool escape) - { - bsonWriter.WriteName(name.EscapeJson()); - } - - public override void WriteStartArray() - { - bsonWriter.WriteStartArray(); - } - - public override void WriteEndArray() - { - bsonWriter.WriteEndArray(); - } - - public override void WriteStartObject() - { - bsonWriter.WriteStartDocument(); - } - - public override void WriteEndObject() - { - bsonWriter.WriteEndDocument(); - } - - public override void WriteNull() - { - bsonWriter.WriteNull(); - } - - public override void WriteUndefined() - { - bsonWriter.WriteUndefined(); - } - - public override void WriteValue(string? value) - { - if (value == null) - { - bsonWriter.WriteNull(); - } - else - { - bsonWriter.WriteString(value); - } - } - - public override void WriteValue(int value) - { - bsonWriter.WriteInt32(value); - } - - public override void WriteValue(uint value) - { - bsonWriter.WriteInt32((int)value); - } - - public override void WriteValue(long value) - { - bsonWriter.WriteInt64(value); - } - - public override void WriteValue(ulong value) - { - bsonWriter.WriteInt64((long)value); - } - - public override void WriteValue(float value) - { - bsonWriter.WriteDouble(value); - } - - public override void WriteValue(double value) - { - bsonWriter.WriteDouble(value); - } - - public override void WriteValue(bool value) - { - bsonWriter.WriteBoolean(value); - } - - public override void WriteValue(short value) - { - bsonWriter.WriteInt32(value); - } - - public override void WriteValue(ushort value) - { - bsonWriter.WriteInt32(value); - } - - public override void WriteValue(char value) - { - bsonWriter.WriteInt32(value); - } - - public override void WriteValue(byte value) - { - bsonWriter.WriteInt32(value); - } - - public override void WriteValue(sbyte value) - { - bsonWriter.WriteInt32(value); - } - - public override void WriteValue(decimal value) - { - bsonWriter.WriteDecimal128(value); - } - - public override void WriteValue(DateTime value) - { - bsonWriter.WriteString(value.ToIso8601()); - } - - public override void WriteValue(DateTimeOffset value) - { - bsonWriter.WriteString(value.UtcDateTime.ToIso8601()); - } - - public override void WriteValue(byte[]? value) - { - if (value == null) - { - bsonWriter.WriteNull(); - } - else - { - bsonWriter.WriteBytes(value); - } - } - - public override void WriteValue(Uri? value) - { - if (value == null) - { - bsonWriter.WriteNull(); - } - else - { - bsonWriter.WriteString(value.ToString()); - } - } - - public override void WriteValue(TimeSpan value) - { - bsonWriter.WriteString(value.ToString()); - } - - public override void WriteValue(Guid value) - { - bsonWriter.WriteString(value.ToString()); - } - - public override void Flush() - { - bsonWriter.Flush(); - } - } -} diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/TypeConverterStringSerializer.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonStringSerializer.cs similarity index 91% rename from backend/src/Squidex.Infrastructure.MongoDb/MongoDb/TypeConverterStringSerializer.cs rename to backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonStringSerializer.cs index 427dfd28d..42518e634 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/TypeConverterStringSerializer.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonStringSerializer.cs @@ -12,7 +12,7 @@ using MongoDB.Bson.Serialization.Serializers; namespace Squidex.Infrastructure.MongoDb { - public sealed class TypeConverterStringSerializer : SerializerBase + public sealed class BsonStringSerializer : SerializerBase { private readonly TypeConverter typeConverter = TypeDescriptor.GetConverter(typeof(T)); @@ -20,7 +20,7 @@ namespace Squidex.Infrastructure.MongoDb { try { - BsonSerializer.RegisterSerializer(new TypeConverterStringSerializer()); + BsonSerializer.RegisterSerializer(new BsonStringSerializer()); } catch (BsonSerializationException) { diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/FieldDefinitionBuilder.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/FieldDefinitionBuilder.cs deleted file mode 100644 index abc359d7c..000000000 --- a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/FieldDefinitionBuilder.cs +++ /dev/null @@ -1,31 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Linq.Expressions; -using MongoDB.Driver; - -namespace Squidex.Infrastructure.MongoDb -{ - public sealed class FieldDefinitionBuilder - { - public static readonly FieldDefinitionBuilder Instance = new FieldDefinitionBuilder(); - - private FieldDefinitionBuilder() - { - } - - public FieldDefinition Build(Expression> expression) - { - return new ExpressionFieldDefinition(expression); - } - - public FieldDefinition Build(string name) - { - return new StringFieldDefinition(name); - } - } -} diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoBase.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoBase.cs index c85845b02..c38e9ec38 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoBase.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoBase.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using MongoDB.Bson; using MongoDB.Driver; #pragma warning disable RECS0108 // Warns about static fields in generic types @@ -13,9 +14,6 @@ namespace Squidex.Infrastructure.MongoDb { public abstract class MongoBase { - protected static readonly FieldDefinitionBuilder FieldBuilder = - FieldDefinitionBuilder.Instance; - protected static readonly FilterDefinitionBuilder Filter = Builders.Filter; @@ -43,13 +41,19 @@ namespace Squidex.Infrastructure.MongoDb protected static readonly UpdateOptions Upsert = new UpdateOptions { IsUpsert = true }; + protected static readonly BsonDocument FindAll = + new BsonDocument(); + static MongoBase() { - TypeConverterStringSerializer.Register(); - - InstantSerializer.Register(); - - DomainIdSerializer.Register(); + BsonDomainIdSerializer.Register(); + BsonInstantSerializer.Register(); + BsonJsonConvention.Register(); + BsonJsonValueSerializer.Register(); + BsonStringSerializer.Register(); + BsonStringSerializer>.Register(); + BsonStringSerializer>.Register(); + BsonStringSerializer>.Register(); } } } diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs index 4fb7552bb..36513d771 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs @@ -5,21 +5,27 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Globalization; using MongoDB.Driver; using Squidex.Hosting; using Squidex.Hosting.Configuration; namespace Squidex.Infrastructure.MongoDb { - public abstract class MongoRepositoryBase : MongoBase, IInitializable + public abstract class MongoRepositoryBase : MongoBase, IInitializable { private readonly IMongoDatabase mongoDatabase; - private IMongoCollection mongoCollection; + private IMongoCollection mongoCollection; - protected IMongoCollection Collection + protected IMongoCollection Collection { get { + if (mongoCollection == null) + { + InitializeAsync(default).Wait(); + } + if (mongoCollection == null) { ThrowHelper.InvalidOperationException("Collection has not been initialized yet."); @@ -35,16 +41,11 @@ namespace Squidex.Infrastructure.MongoDb get => mongoDatabase; } - protected MongoRepositoryBase(IMongoDatabase database, bool setup = false) + protected MongoRepositoryBase(IMongoDatabase database) { Guard.NotNull(database); mongoDatabase = database; - - if (setup) - { - CreateCollection(); - } } protected virtual MongoCollectionSettings CollectionSettings() @@ -52,9 +53,12 @@ namespace Squidex.Infrastructure.MongoDb return new MongoCollectionSettings(); } - protected abstract string CollectionName(); + protected virtual string CollectionName() + { + return string.Format(CultureInfo.InvariantCulture, "{0}Set", typeof(T).Name); + } - protected virtual Task SetupCollectionAsync(IMongoCollection collection, + protected virtual Task SetupCollectionAsync(IMongoCollection collection, CancellationToken ct) { return Task.CompletedTask; @@ -99,7 +103,7 @@ namespace Squidex.Infrastructure.MongoDb private void CreateCollection() { - mongoCollection = mongoDatabase.GetCollection( + mongoCollection = mongoDatabase.GetCollection( CollectionName(), CollectionSettings() ?? new MongoCollectionSettings()); } diff --git a/backend/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs b/backend/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs index 6ddfcb716..ef3f14b31 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs @@ -6,14 +6,13 @@ // ========================================================================== using MongoDB.Driver; -using Newtonsoft.Json; namespace Squidex.Infrastructure.States { public sealed class MongoSnapshotStore : MongoSnapshotStoreBase> { - public MongoSnapshotStore(IMongoDatabase database, JsonSerializer serializer) - : base(database, serializer) + public MongoSnapshotStore(IMongoDatabase database) + : base(database) { } } diff --git a/backend/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStoreBase.cs b/backend/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStoreBase.cs index cd3e0b021..37c0f3da4 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStoreBase.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStoreBase.cs @@ -8,27 +8,17 @@ using System.Runtime.CompilerServices; using MongoDB.Bson; using MongoDB.Driver; -using Newtonsoft.Json; using Squidex.Infrastructure.MongoDb; namespace Squidex.Infrastructure.States { public abstract class MongoSnapshotStoreBase : MongoRepositoryBase, ISnapshotStore where TState : MongoState, new() { - protected MongoSnapshotStoreBase(IMongoDatabase database, JsonSerializer serializer) - : base(database, Register(serializer)) + protected MongoSnapshotStoreBase(IMongoDatabase database) + : base(database) { } - private static bool Register(JsonSerializer serializer) - { - Guard.NotNull(serializer); - - BsonJsonConvention.Register(serializer); - - return true; - } - protected override string CollectionName() { var attribute = typeof(T).GetCustomAttributes(true).OfType().FirstOrDefault(); @@ -101,7 +91,7 @@ namespace Squidex.Infrastructure.States { using (Telemetry.Activities.StartActivity("MongoSnapshotStoreBase/ReadAllAsync")) { - var find = Collection.Find(new BsonDocument(), Batching.Options); + var find = Collection.Find(FindAll, Batching.Options); await foreach (var document in find.ToAsyncEnumerable(ct)) { diff --git a/backend/src/Squidex.Infrastructure/Collections/ReadonlyDictionary.cs b/backend/src/Squidex.Infrastructure/Collections/ReadonlyDictionary.cs index 117c6b94e..01be52e71 100644 --- a/backend/src/Squidex.Infrastructure/Collections/ReadonlyDictionary.cs +++ b/backend/src/Squidex.Infrastructure/Collections/ReadonlyDictionary.cs @@ -11,9 +11,7 @@ namespace Squidex.Infrastructure.Collections { private static class Empties where TKey : notnull { -#pragma warning disable SA1401 // Fields should be private public static readonly ReadonlyDictionary Instance = new ReadonlyDictionary(); -#pragma warning restore SA1401 // Fields should be private } public static ReadonlyDictionary Empty() where TKey : notnull diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/DefaultEventFormatter.cs b/backend/src/Squidex.Infrastructure/EventSourcing/DefaultEventFormatter.cs index 74d0f5221..933cc0373 100644 --- a/backend/src/Squidex.Infrastructure/EventSourcing/DefaultEventFormatter.cs +++ b/backend/src/Squidex.Infrastructure/EventSourcing/DefaultEventFormatter.cs @@ -75,7 +75,7 @@ namespace Squidex.Infrastructure.EventSourcing } var payloadType = typeNameRegistry.GetName(payloadValue.GetType()); - var payloadJson = serializer.Serialize(envelope.Payload); + var payloadJson = serializer.Serialize(envelope.Payload, envelope.Payload.GetType()); envelope.SetCommitId(commitId); diff --git a/backend/src/Squidex.Infrastructure/Json/IJsonSerializer.cs b/backend/src/Squidex.Infrastructure/Json/IJsonSerializer.cs index 46fe2ed11..56c9585a3 100644 --- a/backend/src/Squidex.Infrastructure/Json/IJsonSerializer.cs +++ b/backend/src/Squidex.Infrastructure/Json/IJsonSerializer.cs @@ -9,10 +9,14 @@ namespace Squidex.Infrastructure.Json { public interface IJsonSerializer { - string Serialize(T value, bool intented = false); + string Serialize(T value, bool indented = false); + + string Serialize(object? value, Type type, bool indented = false); void Serialize(T value, Stream stream, bool leaveOpen = false); + void Serialize(object? value, Type type, Stream stream, bool leaveOpen = false); + T Deserialize(string value, Type? actualType = null); T Deserialize(Stream stream, Type? actualType = null, bool leaveOpen = false); diff --git a/backend/src/Squidex.Infrastructure/Json/Newtonsoft/ConverterContractResolver.cs b/backend/src/Squidex.Infrastructure/Json/Newtonsoft/ConverterContractResolver.cs deleted file mode 100644 index 3c3fe0e76..000000000 --- a/backend/src/Squidex.Infrastructure/Json/Newtonsoft/ConverterContractResolver.cs +++ /dev/null @@ -1,98 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; - -namespace Squidex.Infrastructure.Json.Newtonsoft -{ - public sealed class ConverterContractResolver : CamelCasePropertyNamesContractResolver - { - private readonly JsonConverter[] converters; - private readonly object lockObject = new object(); - private Dictionary converterCache = new Dictionary(); - - public ConverterContractResolver(params JsonConverter[] converters) - { - NamingStrategy = new CamelCaseNamingStrategy(false, true); - - this.converters = converters; - - foreach (var converter in converters) - { - if (converter is ISupportedTypes supportedTypes) - { - foreach (var type in supportedTypes.SupportedTypes) - { - converterCache[type] = converter; - } - } - } - } - - protected override JsonArrayContract CreateArrayContract(Type objectType) - { - if (objectType.IsGenericType && objectType.GetGenericTypeDefinition() == typeof(IReadOnlyList<>)) - { - var implementationType = typeof(List<>).MakeGenericType(objectType.GetGenericArguments()); - - return base.CreateArrayContract(implementationType); - } - - return base.CreateArrayContract(objectType); - } - - protected override JsonDictionaryContract CreateDictionaryContract(Type objectType) - { - if (objectType.IsGenericType && objectType.GetGenericTypeDefinition() == typeof(IReadOnlyDictionary<,>)) - { - var implementationType = typeof(Dictionary<,>).MakeGenericType(objectType.GetGenericArguments()); - - return base.CreateDictionaryContract(implementationType); - } - - return base.CreateDictionaryContract(objectType); - } - - protected override JsonConverter? ResolveContractConverter(Type objectType) - { - var result = base.ResolveContractConverter(objectType); - - if (result != null) - { - return result; - } - - var cache = converterCache; - - if (cache == null || !cache.TryGetValue(objectType, out result)) - { - foreach (var converter in converters) - { - if (converter.CanConvert(objectType)) - { - result = converter; - } - } - - lock (lockObject) - { - cache = converterCache; - - var updatedCache = (cache != null) - ? new Dictionary(cache) - : new Dictionary(); - updatedCache[objectType] = result; - - converterCache = updatedCache; - } - } - - return result; - } - } -} diff --git a/backend/src/Squidex.Infrastructure/Json/Newtonsoft/JsonClassConverter.cs b/backend/src/Squidex.Infrastructure/Json/Newtonsoft/JsonClassConverter.cs deleted file mode 100644 index 1d8a7b5a7..000000000 --- a/backend/src/Squidex.Infrastructure/Json/Newtonsoft/JsonClassConverter.cs +++ /dev/null @@ -1,49 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Newtonsoft.Json; - -namespace Squidex.Infrastructure.Json.Newtonsoft -{ - public abstract class JsonClassConverter : JsonConverter, ISupportedTypes where T : class - { - public virtual IEnumerable SupportedTypes - { - get { yield return typeof(T); } - } - - public sealed override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) - { - if (reader.TokenType == JsonToken.Null) - { - return null; - } - - return ReadValue(reader, objectType, serializer); - } - - protected abstract T? ReadValue(JsonReader reader, Type objectType, JsonSerializer serializer); - - public sealed override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) - { - if (value == null) - { - writer.WriteNull(); - return; - } - - WriteValue(writer, (T)value, serializer); - } - - protected abstract void WriteValue(JsonWriter writer, T value, JsonSerializer serializer); - - public override bool CanConvert(Type objectType) - { - return objectType == typeof(T); - } - } -} diff --git a/backend/src/Squidex.Infrastructure/Json/Newtonsoft/JsonValueConverter.cs b/backend/src/Squidex.Infrastructure/Json/Newtonsoft/JsonValueConverter.cs deleted file mode 100644 index 32bd9703f..000000000 --- a/backend/src/Squidex.Infrastructure/Json/Newtonsoft/JsonValueConverter.cs +++ /dev/null @@ -1,207 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Newtonsoft.Json; -using Squidex.Infrastructure.Json.Objects; - -#pragma warning disable RECS0018 // Comparison of floating point numbers with equality operator - -namespace Squidex.Infrastructure.Json.Newtonsoft -{ - public class JsonValueConverter : JsonConverter, ISupportedTypes - { - private readonly HashSet supportedTypes = new HashSet - { - typeof(JsonValue) - }; - - public virtual IEnumerable SupportedTypes - { - get => supportedTypes; - } - - public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) - { - var previousDateParseHandling = reader.DateParseHandling; - - reader.DateParseHandling = DateParseHandling.None; - try - { - return ReadJsonCore(reader); - } - finally - { - reader.DateParseHandling = previousDateParseHandling; - } - } - - private static JsonValue ReadJsonCore(JsonReader reader) - { - switch (reader.TokenType) - { - case JsonToken.Null: - return default; - case JsonToken.Undefined: - return default; - case JsonToken.String: - return new JsonValue((string)reader.Value!); - case JsonToken.Integer: - return new JsonValue((long)reader.Value!); - case JsonToken.Float: - return new JsonValue((double)reader.Value!); - case JsonToken.Boolean: - return (bool)reader.Value! ? JsonValue.True : JsonValue.False; - case JsonToken.StartObject: - { - var result = new JsonObject(4); - - Dictionary dictionary = result; - - while (reader.Read()) - { - switch (reader.TokenType) - { - case JsonToken.PropertyName: - var propertyName = reader.Value!.ToString()!; - - if (!reader.Read()) - { - ThrowInvalidObjectException(); - } - - var value = ReadJsonCore(reader); - - dictionary.Add(propertyName, value); - break; - case JsonToken.EndObject: - result.TrimExcess(); - - return new JsonValue(result); - } - } - - ThrowInvalidObjectException(); - return default!; - } - - case JsonToken.StartArray: - { - var result = new JsonArray(4); - - while (reader.Read()) - { - switch (reader.TokenType) - { - case JsonToken.Comment: - continue; - case JsonToken.EndArray: - result.TrimExcess(); - - return new JsonValue(result); - default: - var value = ReadJsonCore(reader); - - result.Add(value); - break; - } - } - - ThrowInvalidArrayException(); - return default!; - } - - case JsonToken.Comment: - reader.Read(); - break; - } - - ThrowUnsupportedTypeException(); - return default; - } - - private static void ThrowUnsupportedTypeException() - { - throw new JsonSerializationException("Unsupported type."); - } - - private static void ThrowInvalidArrayException() - { - throw new JsonSerializationException("Unexpected end when reading Array."); - } - - private static void ThrowInvalidObjectException() - { - throw new JsonSerializationException("Unexpected end when reading Object."); - } - - public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) - { - if (value == null) - { - writer.WriteNull(); - return; - } - - WriteJson(writer, (JsonValue)value); - } - - private static void WriteJson(JsonWriter writer, JsonValue value) - { - switch (value.Value) - { - case null: - writer.WriteNull(); - break; - case bool b: - writer.WriteValue(b); - break; - case string s: - writer.WriteValue(s); - break; - case double n: - if (n % 1 == 0) - { - writer.WriteValue((long)n); - } - else - { - writer.WriteValue(n); - } - - break; - case JsonArray a: - writer.WriteStartArray(); - - foreach (var item in a) - { - WriteJson(writer, item); - } - - writer.WriteEndArray(); - break; - - case JsonObject o: - writer.WriteStartObject(); - - foreach (var (key, jsonValue) in o) - { - writer.WritePropertyName(key); - - WriteJson(writer, jsonValue); - } - - writer.WriteEndObject(); - break; - } - } - - public override bool CanConvert(Type objectType) - { - return supportedTypes.Contains(objectType); - } - } -} diff --git a/backend/src/Squidex.Infrastructure/Json/Newtonsoft/NewtonsoftJsonSerializer.cs b/backend/src/Squidex.Infrastructure/Json/Newtonsoft/NewtonsoftJsonSerializer.cs deleted file mode 100644 index a5aec1eb3..000000000 --- a/backend/src/Squidex.Infrastructure/Json/Newtonsoft/NewtonsoftJsonSerializer.cs +++ /dev/null @@ -1,98 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Newtonsoft.Json; -using NewtonsoftException = Newtonsoft.Json.JsonException; - -namespace Squidex.Infrastructure.Json.Newtonsoft -{ - public sealed class NewtonsoftJsonSerializer : IJsonSerializer - { - private readonly JsonSerializerSettings settings; - private readonly JsonSerializer serializer; - - public NewtonsoftJsonSerializer(JsonSerializerSettings settings) - { - Guard.NotNull(settings); - - this.settings = settings; - - serializer = JsonSerializer.Create(settings); - } - - public string Serialize(T value, bool intented = false) - { - var formatting = intented ? Formatting.Indented : Formatting.None; - - return JsonConvert.SerializeObject(value, formatting, settings); - } - - public void Serialize(T value, Stream stream, bool leaveOpen = false) - { - try - { - using (var writer = new StreamWriter(stream, leaveOpen: leaveOpen)) - { - serializer.Serialize(writer, value); - - writer.Flush(); - } - } - catch (NewtonsoftException ex) - { - ThrowHelper.JsonException(ex.Message, ex); - } - } - - public T Deserialize(string value, Type? actualType = null) - { - try - { - using (var textReader = new StringReader(value)) - { - actualType ??= typeof(T); - - using (var reader = GetReader(textReader)) - { - return (T)serializer.Deserialize(reader, actualType)!; - } - } - } - catch (NewtonsoftException ex) - { - ThrowHelper.JsonException(ex.Message, ex); - return default!; - } - } - - public T Deserialize(Stream stream, Type? actualType = null, bool leaveOpen = false) - { - try - { - using (var textReader = new StreamReader(stream, leaveOpen: leaveOpen)) - { - actualType ??= typeof(T); - - using (var reader = GetReader(textReader)) - { - return (T)serializer.Deserialize(reader, actualType)!; - } - } - } - catch (NewtonsoftException ex) - { - ThrowHelper.JsonException(ex.Message, ex); - return default!; - } - } - - private static JsonTextReader GetReader(TextReader textReader) - { - return new JsonTextReader(textReader); - } - } -} diff --git a/backend/src/Squidex.Infrastructure/Json/Newtonsoft/SurrogateConverter.cs b/backend/src/Squidex.Infrastructure/Json/Newtonsoft/SurrogateConverter.cs deleted file mode 100644 index 1035fc666..000000000 --- a/backend/src/Squidex.Infrastructure/Json/Newtonsoft/SurrogateConverter.cs +++ /dev/null @@ -1,30 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Newtonsoft.Json; - -namespace Squidex.Infrastructure.Json.Newtonsoft -{ - public sealed class SurrogateConverter : JsonClassConverter where T : class where TSurrogate : ISurrogate, new() - { - protected override T? ReadValue(JsonReader reader, Type objectType, JsonSerializer serializer) - { - var surrogate = serializer.Deserialize(reader); - - return surrogate?.ToSource(); - } - - protected override void WriteValue(JsonWriter writer, T value, JsonSerializer serializer) - { - var surrogate = new TSurrogate(); - - surrogate.FromSource(value); - - serializer.Serialize(writer, surrogate); - } - } -} diff --git a/backend/src/Squidex.Infrastructure/Json/Newtonsoft/TypeConverterJsonConverter.cs b/backend/src/Squidex.Infrastructure/Json/Newtonsoft/TypeConverterJsonConverter.cs deleted file mode 100644 index a440a5de3..000000000 --- a/backend/src/Squidex.Infrastructure/Json/Newtonsoft/TypeConverterJsonConverter.cs +++ /dev/null @@ -1,50 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.ComponentModel; -using Newtonsoft.Json; - -namespace Squidex.Infrastructure.Json.Newtonsoft -{ - public sealed class TypeConverterJsonConverter : JsonConverter, ISupportedTypes - { - private readonly TypeConverter typeConverter = TypeDescriptor.GetConverter(typeof(T)); - - public IEnumerable SupportedTypes - { - get { yield return typeof(T); } - } - - public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) - { - if (reader.TokenType == JsonToken.Null) - { - return default(T); - } - - try - { - return typeConverter.ConvertFromInvariantString(reader.Value?.ToString()!); - } - catch (Exception ex) - { - ThrowHelper.JsonException("Error while converting from string.", ex); - return default; - } - } - - public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) - { - writer.WriteValue(typeConverter.ConvertToInvariantString(value)); - } - - public override bool CanConvert(Type objectType) - { - return objectType == typeof(T); - } - } -} diff --git a/backend/src/Squidex.Infrastructure/Json/Newtonsoft/TypeNameSerializationBinder.cs b/backend/src/Squidex.Infrastructure/Json/Newtonsoft/TypeNameSerializationBinder.cs deleted file mode 100644 index 0060f1bfe..000000000 --- a/backend/src/Squidex.Infrastructure/Json/Newtonsoft/TypeNameSerializationBinder.cs +++ /dev/null @@ -1,45 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Newtonsoft.Json.Serialization; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Infrastructure.Json.Newtonsoft -{ - public sealed class TypeNameSerializationBinder : DefaultSerializationBinder - { - private readonly TypeNameRegistry typeNameRegistry; - - public TypeNameSerializationBinder(TypeNameRegistry typeNameRegistry) - { - this.typeNameRegistry = typeNameRegistry; - } - - public override Type BindToType(string? assemblyName, string typeName) - { - var type = typeNameRegistry.GetTypeOrNull(typeName); - - return type ?? base.BindToType(assemblyName, typeName); - } - - public override void BindToName(Type serializedType, out string? assemblyName, out string? typeName) - { - assemblyName = null; - - var name = typeNameRegistry.GetNameOrNull(serializedType); - - if (name != null) - { - typeName = name; - } - else - { - base.BindToName(serializedType, out assemblyName, out typeName); - } - } - } -} \ No newline at end of file diff --git a/backend/src/Squidex.Infrastructure/Json/Newtonsoft/WriteonlyGeoJsonConverter.cs b/backend/src/Squidex.Infrastructure/Json/Newtonsoft/WriteonlyGeoJsonConverter.cs deleted file mode 100644 index 5a70e2f92..000000000 --- a/backend/src/Squidex.Infrastructure/Json/Newtonsoft/WriteonlyGeoJsonConverter.cs +++ /dev/null @@ -1,16 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using GeoJSON.Net.Converters; - -namespace Squidex.Infrastructure.Json.Newtonsoft -{ - public sealed class WriteonlyGeoJsonConverter : GeoJsonConverter - { - public override bool CanWrite => false; - } -} diff --git a/backend/src/Squidex.Infrastructure/Json/System/InheritanceConverter.cs b/backend/src/Squidex.Infrastructure/Json/System/InheritanceConverter.cs new file mode 100644 index 000000000..be34bec72 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Json/System/InheritanceConverter.cs @@ -0,0 +1,54 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Text.Json; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Infrastructure.Json.System +{ + public sealed class InheritanceConverter : InheritanceConverterBase where T : notnull + { + private readonly TypeNameRegistry typeNameRegistry; + + public InheritanceConverter(TypeNameRegistry typeNameRegistry) + : base("$type") + { + this.typeNameRegistry = typeNameRegistry; + } + + public override Type GetDiscriminatorType(string name, Type typeToConvert) + { + var typeInfo = typeNameRegistry.GetTypeOrNull(name); + + if (typeInfo == null) + { + typeInfo = Type.GetType(name); + } + + if (typeInfo == null) + { + ThrowHelper.JsonException($"Object has invalid discriminator '{name}'."); + return default!; + } + + return typeInfo; + } + + public override string GetDiscriminatorValue(Type type) + { + var typeName = typeNameRegistry.GetNameOrNull(type); + + if (typeName == null) + { + // Use the type name as a fallback. + typeName = type.AssemblyQualifiedName!; + } + + return typeName; + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Json/System/InheritanceConverterBase.cs b/backend/src/Squidex.Infrastructure/Json/System/InheritanceConverterBase.cs new file mode 100644 index 000000000..9f7b6b8b3 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Json/System/InheritanceConverterBase.cs @@ -0,0 +1,95 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Text.Json; +using System.Text.Json.Serialization; +using Squidex.Infrastructure.ObjectPool; + +namespace Squidex.Infrastructure.Json.System +{ + public abstract class InheritanceConverterBase : JsonConverter where T : notnull + { + private readonly JsonEncodedText discriminatorProperty; + + public string DiscriminatorName { get; } + + protected InheritanceConverterBase(string discriminatorName) + { + discriminatorProperty = JsonEncodedText.Encode(discriminatorName); + + DiscriminatorName = discriminatorName; + } + + public abstract Type GetDiscriminatorType(string name, Type typeToConvert); + + public abstract string GetDiscriminatorValue(Type type); + + public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + // Creating a copy of the reader (The derived deserialisation has to be done from the start) + Utf8JsonReader typeReader = reader; + + if (typeReader.TokenType != JsonTokenType.StartObject) + { + throw new JsonException(); + } + + if (!typeReader.Read() || typeReader.TokenType != JsonTokenType.PropertyName) + { + throw new JsonException(); + } + + var propertyName = typeReader.GetString(); + + if (typeReader.Read() && typeReader.TokenType == JsonTokenType.String && propertyName == DiscriminatorName) + { + var type = GetDiscriminatorType(typeReader.GetString()!, typeToConvert); + + return (T?)JsonSerializer.Deserialize(ref reader, type, options); + } + else + { + using var document = JsonDocument.ParseValue(ref reader); + + if (!document.RootElement.TryGetProperty(DiscriminatorName, out var discriminator)) + { + ThrowHelper.JsonException($"Object has no discriminator '{DiscriminatorName}."); + return default!; + } + + var type = GetDiscriminatorType(discriminator.GetString()!, typeToConvert); + + using var bufferWriter = DefaultPools.MemoryStream.GetStream(); + + using (var writer = new Utf8JsonWriter(bufferWriter)) + { + document.RootElement.WriteTo(writer); + } + + return (T?)JsonSerializer.Deserialize(bufferWriter.ToArray(), type, options); + } + } + + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + var name = GetDiscriminatorValue(value.GetType()); + + writer.WriteStartObject(); + writer.WriteString(discriminatorProperty, name); + + using (var document = JsonSerializer.SerializeToDocument(value, value.GetType(), options)) + { + foreach (var property in document.RootElement.EnumerateObject()) + { + property.WriteTo(writer); + } + } + + writer.WriteEndObject(); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Json/System/JsonValueConverter.cs b/backend/src/Squidex.Infrastructure/Json/System/JsonValueConverter.cs new file mode 100644 index 000000000..1e5ad318d --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Json/System/JsonValueConverter.cs @@ -0,0 +1,72 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Text.Json; +using System.Text.Json.Serialization; +using SquidexJsonArray = Squidex.Infrastructure.Json.Objects.JsonArray; +using SquidexJsonObject = Squidex.Infrastructure.Json.Objects.JsonObject; +using SquidexJsonValue = Squidex.Infrastructure.Json.Objects.JsonValue; + +namespace Squidex.Infrastructure.Json.System +{ + public sealed class JsonValueConverter : JsonConverter + { + public override bool HandleNull => true; + + public override SquidexJsonValue Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + switch (reader.TokenType) + { + case JsonTokenType.True: + return SquidexJsonValue.True; + case JsonTokenType.False: + return SquidexJsonValue.False; + case JsonTokenType.Null: + return SquidexJsonValue.Null; + case JsonTokenType.Number: + return SquidexJsonValue.Create(reader.GetDouble()); + case JsonTokenType.String: + return SquidexJsonValue.Create(reader.GetString()); + case JsonTokenType.StartObject: + return JsonSerializer.Deserialize(ref reader, options); + case JsonTokenType.StartArray: + return JsonSerializer.Deserialize(ref reader, options); + default: + ThrowHelper.NotSupportedException(); + return default; + } + } + + public override void Write(Utf8JsonWriter writer, SquidexJsonValue value, JsonSerializerOptions options) + { + switch (value.Value) + { + case null: + writer.WriteNullValue(); + break; + case bool b: + writer.WriteBooleanValue(b); + break; + case string s: + writer.WriteStringValue(s); + break; + case double n: + writer.WriteNumberValue(n); + break; + case SquidexJsonArray a: + JsonSerializer.Serialize(writer, a, options); + break; + case SquidexJsonObject o: + JsonSerializer.Serialize(writer, o, options); + break; + default: + ThrowHelper.NotSupportedException(); + break; + } + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Json/System/ReadonlyDictionaryConverterFactory.cs b/backend/src/Squidex.Infrastructure/Json/System/ReadonlyDictionaryConverterFactory.cs new file mode 100644 index 000000000..29d65e411 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Json/System/ReadonlyDictionaryConverterFactory.cs @@ -0,0 +1,69 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Linq.Expressions; +using System.Reflection; +using System.Reflection.Emit; +using System.Text.Json; +using System.Text.Json.Serialization; +using Squidex.Infrastructure.Collections; + +namespace Squidex.Infrastructure.Json.System +{ + public sealed class ReadonlyDictionaryConverterFactory : JsonConverterFactory + { + private sealed class Converter : JsonConverter where TKey : notnull + { + private readonly Type innerType = typeof(IReadOnlyDictionary); + private readonly Func, TInstance> creator; + + public Converter() + { + creator = ReflectionHelper.CreateParameterizedConstructor>(); + } + + public override TInstance Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var inner = JsonSerializer.Deserialize>(ref reader, options)!; + + return creator(inner); + } + + public override void Write(Utf8JsonWriter writer, TInstance value, JsonSerializerOptions options) + { + JsonSerializer.Serialize(writer, value, innerType, options); + } + } + + public override bool CanConvert(Type typeToConvert) + { + return IsReadonlyDictionary(typeToConvert) || IsReadonlyDictionary(typeToConvert.BaseType); + } + + public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + var typeToCreate = IsReadonlyDictionary(typeToConvert) ? typeToConvert : typeToConvert.BaseType!; + + var concreteType = typeof(Converter<,,>).MakeGenericType( + new Type[] + { + typeToCreate.GetGenericArguments()[0], + typeToCreate.GetGenericArguments()[1], + typeToConvert + }); + + var converter = (JsonConverter)Activator.CreateInstance(concreteType)!; + + return converter; + } + + private static bool IsReadonlyDictionary(Type? type) + { + return type != null && type.IsGenericType && type.GetGenericTypeDefinition() == typeof(ReadonlyDictionary<,>); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Json/System/ReadonlyListConverterFactory.cs b/backend/src/Squidex.Infrastructure/Json/System/ReadonlyListConverterFactory.cs new file mode 100644 index 000000000..7d10a033b --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Json/System/ReadonlyListConverterFactory.cs @@ -0,0 +1,65 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Text.Json; +using System.Text.Json.Serialization; +using Squidex.Infrastructure.Collections; + +namespace Squidex.Infrastructure.Json.System +{ + public sealed class ReadonlyListConverterFactory : JsonConverterFactory + { + private sealed class Converter : JsonConverter + { + private readonly Type innerType = typeof(IReadOnlyList); + private readonly Func, TInstance> creator; + + public Converter() + { + creator = ReflectionHelper.CreateParameterizedConstructor>(); + } + + public override TInstance Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var inner = JsonSerializer.Deserialize>(ref reader, options)!; + + return creator(inner); + } + + public override void Write(Utf8JsonWriter writer, TInstance value, JsonSerializerOptions options) + { + JsonSerializer.Serialize(writer, value, innerType, options); + } + } + + public override bool CanConvert(Type typeToConvert) + { + return IsReadonlyList(typeToConvert) || IsReadonlyList(typeToConvert.BaseType); + } + + public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + var typeToCreate = IsReadonlyList(typeToConvert) ? typeToConvert : typeToConvert.BaseType!; + + var concreteType = typeof(Converter<,>).MakeGenericType( + new Type[] + { + typeToCreate.GetGenericArguments()[0], + typeToConvert, + }); + + var converter = (JsonConverter)Activator.CreateInstance(concreteType)!; + + return converter; + } + + private static bool IsReadonlyList(Type? type) + { + return type != null && type.IsGenericType && type.GetGenericTypeDefinition() == typeof(ReadonlyList<>); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Json/System/ReflectionHelper.cs b/backend/src/Squidex.Infrastructure/Json/System/ReflectionHelper.cs new file mode 100644 index 000000000..e8b472815 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Json/System/ReflectionHelper.cs @@ -0,0 +1,46 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Reflection; +using System.Reflection.Emit; + +namespace Squidex.Infrastructure.Json.System +{ + internal static class ReflectionHelper + { + public static Func CreateParameterizedConstructor() + { + var method = CreateParameterizedConstructor(typeof(TInstance), typeof(TInput)); + + return method.CreateDelegate>(); + } + + private static DynamicMethod CreateParameterizedConstructor(Type type, Type parameterType) + { + var constructor = + type.GetConstructors() + .Single(x => + x.GetParameters().Length == 1 && + x.GetParameters()[0].ParameterType == parameterType); + + var dynamicMethod = new DynamicMethod( + ConstructorInfo.ConstructorName, + type, + new[] { parameterType }, + typeof(ReflectionHelper).Module, + true); + + var generator = dynamicMethod.GetILGenerator(); + + generator.Emit(OpCodes.Ldarg_0); + generator.Emit(OpCodes.Newobj, constructor); + generator.Emit(OpCodes.Ret); + + return dynamicMethod; + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Json/System/StringConverter.cs b/backend/src/Squidex.Infrastructure/Json/System/StringConverter.cs new file mode 100644 index 000000000..199081d38 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Json/System/StringConverter.cs @@ -0,0 +1,62 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.ComponentModel; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Squidex.Infrastructure.Json.System +{ + public sealed class StringConverter : JsonConverter where T : notnull + { + private readonly Func convertFromString; + private readonly Func convertToString; + + public StringConverter(Func convertFromString, Func? convertToString = null) + { + this.convertFromString = convertFromString; + this.convertToString = convertToString ?? (x => x.ToString()); + } + + public StringConverter() + { + var typeConverter = TypeDescriptor.GetConverter(typeof(T)); + + convertFromString = x => (T)typeConverter.ConvertFromInvariantString(x)!; + convertToString = x => typeConverter.ConvertToInvariantString(x)!; + } + + public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var text = reader.GetString(); + try + { + return convertFromString(text!); + } + catch (Exception ex) + { + ThrowHelper.JsonException("Error while converting from string.", ex); + return default; + } + } + + public override T ReadAsPropertyName(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return convertFromString(reader.GetString()!); + } + + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + writer.WriteStringValue(convertToString(value)); + } + + public override void WriteAsPropertyName(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + writer.WritePropertyName(convertToString(value)!); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Json/System/SurrogateJsonConverter.cs b/backend/src/Squidex.Infrastructure/Json/System/SurrogateJsonConverter.cs new file mode 100644 index 000000000..c0b066eeb --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Json/System/SurrogateJsonConverter.cs @@ -0,0 +1,31 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Squidex.Infrastructure.Json.System +{ + public sealed class SurrogateJsonConverter : JsonConverter where T : class where TSurrogate : ISurrogate, new() + { + public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var surrogate = JsonSerializer.Deserialize(ref reader, options); + + return surrogate?.ToSource(); + } + + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + var surrogate = new TSurrogate(); + + surrogate.FromSource(value); + + JsonSerializer.Serialize(writer, surrogate, options); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Json/System/SystemJsonSerializer.cs b/backend/src/Squidex.Infrastructure/Json/System/SystemJsonSerializer.cs new file mode 100644 index 000000000..93a26518f --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Json/System/SystemJsonSerializer.cs @@ -0,0 +1,108 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Text.Json; +using SystemJsonException = System.Text.Json.JsonException; + +namespace Squidex.Infrastructure.Json.System +{ + public sealed class SystemJsonSerializer : IJsonSerializer + { + private readonly JsonSerializerOptions optionsNormal; + private readonly JsonSerializerOptions optionsIndented; + + public SystemJsonSerializer(JsonSerializerOptions options) + { + optionsNormal = new JsonSerializerOptions(options) + { + WriteIndented = false + }; + + optionsIndented = new JsonSerializerOptions(options) + { + WriteIndented = true + }; + } + + public T Deserialize(string value, Type? actualType = null) + { + try + { + return (T)JsonSerializer.Deserialize(value, actualType ?? typeof(T), optionsNormal)!; + } + catch (SystemJsonException ex) + { + ThrowHelper.JsonException(ex.Message, ex); + return default!; + } + } + + public T Deserialize(Stream stream, Type? actualType = null, bool leaveOpen = false) + { + try + { + return (T)JsonSerializer.Deserialize(stream, actualType ?? typeof(T), optionsNormal)!; + } + catch (SystemJsonException ex) + { + ThrowHelper.JsonException(ex.Message, ex); + return default!; + } + finally + { + if (!leaveOpen) + { + stream.Dispose(); + } + } + } + + public string Serialize(T value, bool indented = false) + { + return Serialize(value, typeof(T), indented); + } + + public string Serialize(object? value, Type type, bool intented = false) + { + try + { + var options = intented ? optionsIndented : optionsNormal; + + return JsonSerializer.Serialize(value, type, options); + } + catch (SystemJsonException ex) + { + ThrowHelper.JsonException(ex.Message, ex); + return default!; + } + } + + public void Serialize(T value, Stream stream, bool leaveOpen = false) + { + Serialize(value, typeof(T), stream, leaveOpen); + } + + public void Serialize(object? value, Type type, Stream stream, bool leaveOpen = false) + { + try + { + JsonSerializer.Serialize(stream, value, optionsNormal); + } + catch (SystemJsonException ex) + { + ThrowHelper.JsonException(ex.Message, ex); + } + finally + { + if (!leaveOpen) + { + stream.Dispose(); + } + } + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Json/System/UnsafeRawJsonConverter.cs b/backend/src/Squidex.Infrastructure/Json/System/UnsafeRawJsonConverter.cs new file mode 100644 index 000000000..5b8d8d0fb --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Json/System/UnsafeRawJsonConverter.cs @@ -0,0 +1,27 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Squidex.Infrastructure.Json.System +{ + public sealed class UnsafeRawJsonConverter : JsonConverter + { + public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + using var document = JsonDocument.ParseValue(ref reader); + + return document.RootElement.GetRawText(); + } + + public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) + { + writer.WriteRawValue(value, true); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj b/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj index a2775d664..20cd517f5 100644 --- a/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj +++ b/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj @@ -11,7 +11,6 @@ True - @@ -22,7 +21,6 @@ - diff --git a/backend/src/Squidex.Infrastructure/StringExtensions.cs b/backend/src/Squidex.Infrastructure/StringExtensions.cs index a0afdc2cb..a522c964a 100644 --- a/backend/src/Squidex.Infrastructure/StringExtensions.cs +++ b/backend/src/Squidex.Infrastructure/StringExtensions.cs @@ -6,23 +6,37 @@ // ========================================================================== using System.Globalization; +using System.Text.Encodings.Web; +using System.Text.Json; using System.Text.RegularExpressions; namespace Squidex.Infrastructure { public static class StringExtensions { - private static readonly Regex EmailRegex = new Regex(@"^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-||_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+([a-z]+|\d|-|\.{0,1}|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])?([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture); - private static readonly Regex PropertyNameRegex = new Regex("^[a-zA-Z0-9]+(\\-[a-zA-Z0-9]+)*$", RegexOptions.Compiled | RegexOptions.ExplicitCapture); + private static readonly Regex RegexEmail = new Regex(@"^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-||_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+([a-z]+|\d|-|\.{0,1}|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])?([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture); + private static readonly Regex RegexProperty = new Regex("^[a-zA-Z0-9]+(\\-[a-zA-Z0-9]+)*$", RegexOptions.Compiled | RegexOptions.ExplicitCapture); + private static readonly JsonSerializerOptions JsonEscapeOptions = new JsonSerializerOptions + { + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }; + + public static string JsonEscape(this string value) + { + value = JsonSerializer.Serialize(value, JsonEscapeOptions); + value = value[1..^1]; + + return value; + } public static bool IsEmail(this string? value) { - return value != null && EmailRegex.IsMatch(value); + return value != null && RegexEmail.IsMatch(value); } public static bool IsPropertyName(this string? value) { - return value != null && PropertyNameRegex.IsMatch(value); + return value != null && RegexProperty.IsMatch(value); } public static string Or(this string? value, string fallback) diff --git a/backend/src/Squidex.Web/ApiModelValidationAttribute.cs b/backend/src/Squidex.Web/ApiModelValidationAttribute.cs index 1f513c186..0108e8495 100644 --- a/backend/src/Squidex.Web/ApiModelValidationAttribute.cs +++ b/backend/src/Squidex.Web/ApiModelValidationAttribute.cs @@ -1,14 +1,14 @@ -// ========================================================================== +// ========================================================================== // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex UG (haftungsbeschraenkt) // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Text.Json; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.ModelBinding; -using Newtonsoft.Json; using Squidex.Infrastructure.Translations; using Squidex.Infrastructure.Validation; using Squidex.Text; diff --git a/backend/src/Squidex.Web/GraphQL/BufferingGraphQLSerializer.cs b/backend/src/Squidex.Web/GraphQL/BufferingGraphQLSerializer.cs deleted file mode 100644 index e3c023bb5..000000000 --- a/backend/src/Squidex.Web/GraphQL/BufferingGraphQLSerializer.cs +++ /dev/null @@ -1,61 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using GraphQL; -using Microsoft.AspNetCore.WebUtilities; - -namespace Squidex.Web.GraphQL -{ - public sealed class BufferingGraphQLSerializer : IGraphQLTextSerializer - { - private readonly IGraphQLTextSerializer inner; - - public BufferingGraphQLSerializer(IGraphQLTextSerializer inner) - { - this.inner = inner; - } - - public async ValueTask ReadAsync(Stream stream, - CancellationToken cancellationToken = default) - { - await using (var bufferStream = new FileBufferingReadStream(stream, 30 * 1024)) - { - await bufferStream.DrainAsync(cancellationToken); - - bufferStream.Seek(0L, SeekOrigin.Begin); - - return await inner.ReadAsync(bufferStream, cancellationToken); - } - } - - public async Task WriteAsync(Stream stream, T? value, - CancellationToken cancellationToken = default) - { - await using (var bufferStream = new FileBufferingWriteStream()) - { - await inner.WriteAsync(bufferStream, value, cancellationToken); - - await bufferStream.DrainBufferAsync(stream, cancellationToken); - } - } - - public string Serialize(T? value) - { - return inner.Serialize(value); - } - - public T? Deserialize(string? value) - { - return inner.Deserialize(value); - } - - public T? ReadNode(object? value) - { - return inner.ReadNode(value); - } - } -} diff --git a/backend/src/Squidex.Web/Json/TypedJsonInheritanceConverter.cs b/backend/src/Squidex.Web/Json/JsonInheritanceConverter.cs similarity index 66% rename from backend/src/Squidex.Web/Json/TypedJsonInheritanceConverter.cs rename to backend/src/Squidex.Web/Json/JsonInheritanceConverter.cs index 56335c1cb..81c3f9034 100644 --- a/backend/src/Squidex.Web/Json/TypedJsonInheritanceConverter.cs +++ b/backend/src/Squidex.Web/Json/JsonInheritanceConverter.cs @@ -7,15 +7,14 @@ using System.Reflection; using System.Runtime.Serialization; -using Newtonsoft.Json.Linq; -using NJsonSchema.Converters; using Squidex.Infrastructure; +using Squidex.Infrastructure.Json.System; #pragma warning disable RECS0108 // Warns about static fields in generic types namespace Squidex.Web.Json { - public class TypedJsonInheritanceConverter : JsonInheritanceConverter + public class JsonInheritanceConverter : InheritanceConverterBase where T : notnull { private static readonly Lazy> DefaultMapping = new Lazy>(() => { @@ -25,14 +24,14 @@ namespace Squidex.Web.Json void AddType(Type type) { - var discriminator = type.Name; + var typeName = type.Name; - if (discriminator.EndsWith(baseName, StringComparison.CurrentCulture)) + if (typeName.EndsWith(baseName, StringComparison.CurrentCulture)) { - discriminator = discriminator[..^baseName.Length]; + typeName = typeName[..^baseName.Length]; } - result[discriminator] = type; + result[typeName] = type; } foreach (var attribute in typeof(T).GetCustomAttributes()) @@ -68,28 +67,34 @@ namespace Squidex.Web.Json private readonly IReadOnlyDictionary mapping; - public TypedJsonInheritanceConverter(string discriminator) - : this(discriminator, DefaultMapping.Value) + public JsonInheritanceConverter() + : this(null, DefaultMapping.Value) { } - public TypedJsonInheritanceConverter(string discriminator, IReadOnlyDictionary mapping) - : base(typeof(T), discriminator) + public JsonInheritanceConverter(string? discriminatorName) + : this(discriminatorName, DefaultMapping.Value) + { + } + + public JsonInheritanceConverter(string? discriminatorName, IReadOnlyDictionary mapping) + : base(GetDiscriminatorName(discriminatorName)) { this.mapping = mapping ?? DefaultMapping.Value; } - protected override Type GetDiscriminatorType(JObject jObject, Type objectType, string discriminatorValue) + private static string GetDiscriminatorName(string? discriminatorName) { - if (discriminatorValue == null) - { - ThrowHelper.JsonException("Cannot find discriminator."); - return default!; - } + var attribute = typeof(T).GetCustomAttribute(); + + return attribute?.DiscriminatorName ?? discriminatorName ?? "discriminator"; + } - if (!mapping.TryGetValue(discriminatorValue, out var type)) + public override Type GetDiscriminatorType(string name, Type typeToConvert) + { + if (!mapping.TryGetValue(name, out var type)) { - ThrowHelper.JsonException($"Could not find subtype of '{objectType.Name}' with discriminator '{discriminatorValue}'."); + ThrowHelper.JsonException($"Could not find subtype of '{typeToConvert.Name}' with discriminator '{name}'."); return default!; } diff --git a/backend/src/Squidex.Web/Json/JsonInheritanceConverterAttribute.cs b/backend/src/Squidex.Web/Json/JsonInheritanceConverterAttribute.cs new file mode 100644 index 000000000..51763236e --- /dev/null +++ b/backend/src/Squidex.Web/Json/JsonInheritanceConverterAttribute.cs @@ -0,0 +1,22 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Text.Json.Serialization; + +namespace Squidex.Web.Json +{ + public sealed class JsonInheritanceConverterAttribute : JsonConverterAttribute + { + public string DiscriminatorName { get; } + + public JsonInheritanceConverterAttribute(Type baseType, string discriminatorName = "$type") + : base(typeof(JsonInheritanceConverter<>).MakeGenericType(baseType)) + { + DiscriminatorName = discriminatorName; + } + } +} diff --git a/backend/src/Squidex.Web/Resource.cs b/backend/src/Squidex.Web/Resource.cs index 375ac2303..b8d64c311 100644 --- a/backend/src/Squidex.Web/Resource.cs +++ b/backend/src/Squidex.Web/Resource.cs @@ -6,7 +6,7 @@ // ========================================================================== using System.ComponentModel.DataAnnotations; -using Newtonsoft.Json; +using System.Text.Json.Serialization; using Squidex.Infrastructure; using Squidex.Infrastructure.Validation; @@ -16,7 +16,7 @@ namespace Squidex.Web { [LocalizedRequired] [Display(Description = "The links.")] - [JsonProperty("_links")] + [JsonPropertyName("_links")] public Dictionary Links { get; } = new Dictionary(); protected void AddSelfLink(string href) diff --git a/backend/src/Squidex.Web/Squidex.Web.csproj b/backend/src/Squidex.Web/Squidex.Web.csproj index 0263a513c..6949953a6 100644 --- a/backend/src/Squidex.Web/Squidex.Web.csproj +++ b/backend/src/Squidex.Web/Squidex.Web.csproj @@ -14,7 +14,7 @@ - + all diff --git a/backend/src/Squidex/Areas/Api/Config/OpenApi/OpenApiServices.cs b/backend/src/Squidex/Areas/Api/Config/OpenApi/OpenApiServices.cs index 27e3bb519..6eaabac3f 100644 --- a/backend/src/Squidex/Areas/Api/Config/OpenApi/OpenApiServices.cs +++ b/backend/src/Squidex/Areas/Api/Config/OpenApi/OpenApiServices.cs @@ -5,7 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using Newtonsoft.Json; +using System.Text.Json; using NJsonSchema; using NJsonSchema.Generation; using NJsonSchema.Generation.TypeMappers; @@ -60,10 +60,7 @@ namespace Squidex.Areas.Api.Config.OpenApi services.AddSingleton(c => { - var settings = new JsonSchemaGeneratorSettings - { - SerializerSettings = c.GetRequiredService() - }; + var settings = new JsonSchemaGeneratorSettings(); ConfigureSchemaSettings(settings, true); @@ -72,10 +69,7 @@ namespace Squidex.Areas.Api.Config.OpenApi services.AddSingleton(c => { - var settings = new OpenApiDocumentGeneratorSettings - { - SerializerSettings = c.GetRequiredService() - }; + var settings = new OpenApiDocumentGeneratorSettings(); ConfigureSchemaSettings(settings, true); @@ -119,6 +113,7 @@ namespace Squidex.Areas.Api.Config.OpenApi CreateObjectMap(), CreateObjectMap(), + CreateAnyMap(), CreateAnyMap(), CreateAnyMap>() }; diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorsDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorsDto.cs index f320aaed5..748503e5a 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorsDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorsDto.cs @@ -5,7 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using Newtonsoft.Json; +using System.Text.Json.Serialization; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps.Plans; using Squidex.Infrastructure.Validation; @@ -30,8 +30,8 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models /// /// The metadata to provide information about this request. /// - [JsonProperty("_meta")] - public ContributorsMetadata Metadata { get; set; } + [JsonPropertyName("_meta")] + public ContributorsMetadata? Metadata { get; set; } public static async Task FromAppAsync(IAppEntity app, Resources resources, IUserResolver userResolver, IAppPlansProvider plans, bool invited) { diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs index ff42c271f..d2873301c 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs @@ -5,7 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using Newtonsoft.Json; +using System.Text.Json.Serialization; using NodaTime; using Squidex.Domain.Apps.Core.Assets; using Squidex.Domain.Apps.Entities.Assets; @@ -130,8 +130,8 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models /// /// The metadata. /// - [JsonProperty("_meta")] - public AssetMeta Meta { get; set; } + [JsonPropertyName("_meta")] + public AssetMeta? Meta { get; set; } /// /// Determines of the created file is an image. diff --git a/backend/src/Squidex/Areas/Api/Controllers/QueryDto.cs b/backend/src/Squidex/Areas/Api/Controllers/QueryDto.cs index 3d6b205f6..664933ba5 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/QueryDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/QueryDto.cs @@ -5,8 +5,8 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; using Squidex.Domain.Apps.Entities; using Squidex.Infrastructure; @@ -27,8 +27,8 @@ namespace Squidex.Areas.Api.Controllers /// /// The optional json query. /// - [JsonProperty("q")] - public JObject? JsonQuery { get; set; } + [JsonPropertyName("q")] + public JsonDocument? JsonQuery { get; set; } /// /// The parent id (for assets). @@ -51,7 +51,7 @@ namespace Squidex.Areas.Api.Controllers if (JsonQuery != null) { - result = result.WithJsonQuery(JsonQuery.ToString()); + result = result.WithJsonQuery(JsonQuery.RootElement.ToString()); } return result; diff --git a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/CreateRuleDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/CreateRuleDto.cs index 369bb34b8..738e6c9f0 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/CreateRuleDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/CreateRuleDto.cs @@ -5,7 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using Newtonsoft.Json; +using System.Text.Json.Serialization; using Squidex.Domain.Apps.Core.Rules; using Squidex.Domain.Apps.Entities.Rules.Commands; using Squidex.Infrastructure.Validation; diff --git a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionConverter.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionConverter.cs index 75cc0f542..7c8b3098d 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionConverter.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionConverter.cs @@ -10,7 +10,7 @@ using Squidex.Web.Json; namespace Squidex.Areas.Api.Controllers.Rules.Models { - public sealed class RuleActionConverter : TypedJsonInheritanceConverter + public sealed class RuleActionConverter : JsonInheritanceConverter { public static IReadOnlyDictionary Mapping { get; set; } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionProcessor.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionProcessor.cs index 794c0b813..f119e3291 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionProcessor.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionProcessor.cs @@ -9,7 +9,6 @@ using Namotion.Reflection; using NJsonSchema; using NSwag.Generation.Processors; using NSwag.Generation.Processors.Contexts; -using Squidex.Domain.Apps.Core.GenerateJsonSchema; using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.Rules; @@ -32,17 +31,24 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models if (schema != null) { + var converter = new RuleActionConverter(); + schema.DiscriminatorObject = new OpenApiDiscriminator { - JsonInheritanceConverter = new RuleActionConverter(), - PropertyName = "actionType" + PropertyName = converter.DiscriminatorName, + + // The converter must be set so that NJsonSchema can get the allowed types for that. + JsonInheritanceConverter = converter, }; - schema.Properties["actionType"] = new JsonSchemaProperty + schema.Properties[converter.DiscriminatorName] = new JsonSchemaProperty { - Type = JsonObjectType.String - }.SetRequired(true); + Type = JsonObjectType.String, + IsRequired = true, + IsNullableRaw = true + }; + // The types come from another assembly so we have to fix it here. foreach (var (key, value) in ruleRegistry.Actions) { var derivedSchema = context.SchemaGenerator.Generate(value.Type.ToContextualType(), context.SchemaResolver); diff --git a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleDto.cs index c56d7f642..bf251ba7c 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleDto.cs @@ -5,7 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using Newtonsoft.Json; +using System.Text.Json.Serialization; using NodaTime; using Squidex.Areas.Api.Controllers.Rules.Models.Converters; using Squidex.Domain.Apps.Core.Rules; diff --git a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleTriggerDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleTriggerDto.cs index b3df99af2..377a107da 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleTriggerDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleTriggerDto.cs @@ -6,13 +6,12 @@ // ========================================================================== using System.Runtime.Serialization; -using Newtonsoft.Json; using Squidex.Domain.Apps.Core.Rules; using Squidex.Web.Json; namespace Squidex.Areas.Api.Controllers.Rules.Models { - [JsonConverter(typeof(TypedJsonInheritanceConverter), "triggerType")] + [JsonInheritanceConverter(typeof(RuleTriggerDto), "triggerType")] [KnownType(nameof(Subtypes))] public abstract class RuleTriggerDto { diff --git a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/UpdateRuleDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/UpdateRuleDto.cs index 87379b1d1..ec7fbdd6a 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/UpdateRuleDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/UpdateRuleDto.cs @@ -5,7 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using Newtonsoft.Json; +using System.Text.Json.Serialization; using Squidex.Domain.Apps.Core.Rules; using Squidex.Domain.Apps.Entities.Rules.Commands; using Squidex.Infrastructure; diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldPropertiesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldPropertiesDto.cs index bcc29d165..947f053b4 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldPropertiesDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldPropertiesDto.cs @@ -6,7 +6,6 @@ // ========================================================================== using System.Runtime.Serialization; -using Newtonsoft.Json; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure.Collections; using Squidex.Infrastructure.Validation; @@ -14,7 +13,7 @@ using Squidex.Web.Json; namespace Squidex.Areas.Api.Controllers.Schemas.Models { - [JsonConverter(typeof(TypedJsonInheritanceConverter), "fieldType")] + [JsonInheritanceConverter(typeof(FieldPropertiesDto), "fieldType")] [KnownType(nameof(Subtypes))] public abstract class FieldPropertiesDto { diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/AssetsFieldPropertiesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/AssetsFieldPropertiesDto.cs index 5ea6bc329..2004ef6d4 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/AssetsFieldPropertiesDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/AssetsFieldPropertiesDto.cs @@ -22,7 +22,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields /// /// The language specific default value as a list of asset ids. /// - public LocalizedValue?> DefaultValues { get; set; } + public LocalizedValue?>? DefaultValues { get; set; } /// /// The default value as a list of asset ids. diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/BooleanFieldPropertiesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/BooleanFieldPropertiesDto.cs index 67d749001..3f8985511 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/BooleanFieldPropertiesDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/BooleanFieldPropertiesDto.cs @@ -15,7 +15,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields /// /// The language specific default value for the field value. /// - public LocalizedValue DefaultValues { get; set; } + public LocalizedValue? DefaultValues { get; set; } /// /// The default value for the field value. diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/DateTimeFieldPropertiesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/DateTimeFieldPropertiesDto.cs index d44e2301b..882e8a6b9 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/DateTimeFieldPropertiesDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/DateTimeFieldPropertiesDto.cs @@ -16,7 +16,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields /// /// The language specific default value for the field value. /// - public LocalizedValue DefaultValues { get; set; } + public LocalizedValue? DefaultValues { get; set; } /// /// The default value for the field value. diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/NumberFieldPropertiesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/NumberFieldPropertiesDto.cs index 908645f50..15689dcfd 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/NumberFieldPropertiesDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/NumberFieldPropertiesDto.cs @@ -16,7 +16,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields /// /// The language specific default value for the field value. /// - public LocalizedValue DefaultValues { get; set; } + public LocalizedValue? DefaultValues { get; set; } /// /// The default value for the field value. diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ReferencesFieldPropertiesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ReferencesFieldPropertiesDto.cs index a320918a6..ea56d0c74 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ReferencesFieldPropertiesDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ReferencesFieldPropertiesDto.cs @@ -17,7 +17,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields /// /// The language specific default value as a list of content ids. /// - public LocalizedValue?> DefaultValues { get; set; } + public LocalizedValue?>? DefaultValues { get; set; } /// /// The default value as a list of content ids. diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/StringFieldPropertiesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/StringFieldPropertiesDto.cs index 8db13e685..f48cfa54a 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/StringFieldPropertiesDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/StringFieldPropertiesDto.cs @@ -17,7 +17,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields /// /// The language specific default value for the field value. /// - public LocalizedValue DefaultValues { get; set; } + public LocalizedValue? DefaultValues { get; set; } /// /// The default value for the field value. diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/TagsFieldPropertiesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/TagsFieldPropertiesDto.cs index 4a270fcff..6001706d6 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/TagsFieldPropertiesDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/TagsFieldPropertiesDto.cs @@ -16,7 +16,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields /// /// The language specific default value for the field value. /// - public LocalizedValue?> DefaultValues { get; set; } + public LocalizedValue?>? DefaultValues { get; set; } /// /// The default value. diff --git a/backend/src/Squidex/Areas/Api/Controllers/UI/MyUIOptions.cs b/backend/src/Squidex/Areas/Api/Controllers/UI/MyUIOptions.cs index 0b39d5e37..c0df1c0c3 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/UI/MyUIOptions.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/UI/MyUIOptions.cs @@ -5,50 +5,69 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Text.Json.Serialization; + namespace Squidex.Areas.Api.Controllers.UI { - public sealed class MyUIOptions + public sealed record MyUIOptions { - public Dictionary RegexSuggestions { get; set; } + [JsonExtensionData] + public Dictionary More { get; set; } = new Dictionary(); - public Dictionary More { get; set; } = new Dictionary(); + [JsonPropertyName("regexSuggestions")] + public Dictionary RegexSuggestions { get; set; } + [JsonPropertyName("map")] public MapOptions Map { get; set; } + [JsonPropertyName("google")] public GoogleOptions Google { get; set; } + [JsonPropertyName("referencesDropdownItemCount")] public int ReferencesDropdownItemCount { get; set; } = 100; + [JsonPropertyName("showInfo")] public bool ShowInfo { get; set; } + [JsonPropertyName("hideNews")] public bool HideNews { get; set; } + [JsonPropertyName("hideOnboarding")] public bool HideOnboarding { get; set; } + [JsonPropertyName("hideDateButtons")] public bool HideDateButtons { get; set; } + [JsonPropertyName("hideDateTimeModeButton")] public bool HideDateTimeModeButton { get; set; } + [JsonPropertyName("disableScheduledChanges")] public bool DisableScheduledChanges { get; set; } + [JsonPropertyName("redirectToLogin")] public bool RedirectToLogin { get; set; } + [JsonPropertyName("onlyAdminsCanCreateApps")] public bool OnlyAdminsCanCreateApps { get; set; } public sealed class MapOptions { + [JsonPropertyName("type")] public string Type { get; set; } + [JsonPropertyName("googleMaps")] public MapGoogleOptions GoogleMaps { get; set; } } public sealed class MapGoogleOptions { + [JsonPropertyName("key")] public string Key { get; set; } } public sealed class GoogleOptions { + [JsonPropertyName("analyticsId")] public string AnalyticsId { get; set; } } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/UI/UIController.cs b/backend/src/Squidex/Areas/Api/Controllers/UI/UIController.cs index 9a95ee380..339cee55c 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/UI/UIController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/UI/UIController.cs @@ -1,4 +1,4 @@ -// ========================================================================== +// ========================================================================== // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex UG (haftungsbeschraenkt) @@ -66,7 +66,7 @@ namespace Squidex.Areas.Api.Controllers.UI [ApiPermission] public async Task GetSettings(string app) { - var result = await appUISettings.GetAsync(AppId, null); + var result = await appUISettings.GetAsync(AppId, null, HttpContext.RequestAborted); return Ok(result); } @@ -85,7 +85,7 @@ namespace Squidex.Areas.Api.Controllers.UI [ApiPermission] public async Task GetUserSettings(string app) { - var result = await appUISettings.GetAsync(AppId, UserId()); + var result = await appUISettings.GetAsync(AppId, UserId(), HttpContext.RequestAborted); return Ok(result); } @@ -105,7 +105,7 @@ namespace Squidex.Areas.Api.Controllers.UI [ApiPermission] public async Task PutSetting(string app, string key, [FromBody] UpdateSettingDto request) { - await appUISettings.SetAsync(AppId, null, key, request.Value); + await appUISettings.SetAsync(AppId, null, key, request.Value, HttpContext.RequestAborted); return NoContent(); } @@ -125,7 +125,7 @@ namespace Squidex.Areas.Api.Controllers.UI [ApiPermission] public async Task PutUserSetting(string app, string key, [FromBody] UpdateSettingDto request) { - await appUISettings.SetAsync(AppId, UserId(), key, request.Value); + await appUISettings.SetAsync(AppId, UserId(), key, request.Value, HttpContext.RequestAborted); return NoContent(); } @@ -144,7 +144,7 @@ namespace Squidex.Areas.Api.Controllers.UI [ApiPermission] public async Task DeleteSetting(string app, string key) { - await appUISettings.RemoveAsync(AppId, null, key); + await appUISettings.RemoveAsync(AppId, null, key, HttpContext.RequestAborted); return NoContent(); } @@ -163,7 +163,7 @@ namespace Squidex.Areas.Api.Controllers.UI [ApiPermission] public async Task DeleteUserSetting(string app, string key) { - await appUISettings.RemoveAsync(AppId, UserId(), key); + await appUISettings.RemoveAsync(AppId, UserId(), key, HttpContext.RequestAborted); return NoContent(); } diff --git a/backend/src/Squidex/Areas/Frontend/Middlewares/IndexExtensions.cs b/backend/src/Squidex/Areas/Frontend/Middlewares/IndexExtensions.cs index c6ff38a30..5a9619fe4 100644 --- a/backend/src/Squidex/Areas/Frontend/Middlewares/IndexExtensions.cs +++ b/backend/src/Squidex/Areas/Frontend/Middlewares/IndexExtensions.cs @@ -7,10 +7,8 @@ using System.Collections.Concurrent; using System.Globalization; +using System.Text.Json; using Microsoft.Extensions.Options; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using Newtonsoft.Json.Serialization; using Squidex.Areas.Api.Controllers.UI; using Squidex.Domain.Apps.Entities.History; using Squidex.Web; @@ -20,10 +18,6 @@ namespace Squidex.Areas.Frontend.Middlewares public static class IndexExtensions { private static readonly ConcurrentDictionary Texts = new ConcurrentDictionary(); - private static readonly JsonSerializer JsonSerializer = JsonSerializer.CreateDefault(new JsonSerializerSettings - { - ContractResolver = new CamelCasePropertyNamesContractResolver() - }); public static string AddOptions(this string html, HttpContext httpContext) { @@ -43,22 +37,28 @@ namespace Squidex.Areas.Frontend.Middlewares if (uiOptions != null) { - var json = JObject.FromObject(uiOptions, JsonSerializer); + var clonedOptions = uiOptions with + { + More = new Dictionary + { + ["culture"] = CultureInfo.CurrentUICulture.Name + } + }; + + var jsonOptions = httpContext.RequestServices.GetRequiredService(); - var values = httpContext.RequestServices.GetService(); + using var jsonDocument = JsonSerializer.SerializeToDocument(uiOptions, jsonOptions); - if (values != null) + if (httpContext.RequestServices.GetService() is ExposedValues values) { - json["more"] ??= new JObject(); - json["more"]!["info"] = values.ToString(); + clonedOptions.More["info"] = values.ToString(); } - var notifo = httpContext.RequestServices!.GetService>(); + var notifo = httpContext.RequestServices!.GetService>()?.Value; - if (notifo?.Value.IsConfigured() == true) + if (notifo?.IsConfigured() == true) { - json["more"] ??= new JObject(); - json["more"]!["notifoApi"] = notifo.Value.ApiUrl; + clonedOptions.More["notifoApi"] = notifo.ApiUrl; } var options = httpContext.Features.Get(); @@ -67,14 +67,11 @@ namespace Squidex.Areas.Frontend.Middlewares { foreach (var (key, value) in options.Options) { - json[key] = JToken.FromObject(value); + clonedOptions.More[key] = value; } } - json["more"] ??= new JObject(); - json["more"]!["culture"] = CultureInfo.CurrentUICulture.Name; - - scripts.Add($"var options = {json.ToString(Formatting.Indented)};"); + scripts.Add($"var options = {JsonSerializer.Serialize(clonedOptions)};"); } html = html.Replace(Placeholder, string.Join(Environment.NewLine, scripts), StringComparison.OrdinalIgnoreCase); diff --git a/backend/src/Squidex/Areas/IdentityServer/Config/IdentityServerServices.cs b/backend/src/Squidex/Areas/IdentityServer/Config/IdentityServerServices.cs index fe8d71c95..2a73f00e7 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Config/IdentityServerServices.cs +++ b/backend/src/Squidex/Areas/IdentityServer/Config/IdentityServerServices.cs @@ -116,19 +116,19 @@ namespace Squidex.Areas.IdentityServer.Config options.UseAspNetCore(); }); - services.Configure((services, options) => + services.Configure((c, options) => { - var identityOptions = services.GetRequiredService>().Value; + var identityOptions = c.GetRequiredService>().Value; options.SuppressXFrameOptionsHeader = identityOptions.SuppressXFrameOptionsHeader; }); - services.Configure((services, options) => + services.Configure((c, options) => { - var urlGenerator = services.GetRequiredService(); + var urlGenerator = c.GetRequiredService(); var identityPrefix = Constants.PrefixIdentityServer; - var identityOptions = services.GetRequiredService>().Value; + var identityOptions = c.GetRequiredService>().Value; Func buildUrl; diff --git a/backend/src/Squidex/Config/Domain/SerializationServices.cs b/backend/src/Squidex/Config/Domain/SerializationServices.cs index 9bdf71f2e..0ee133646 100644 --- a/backend/src/Squidex/Config/Domain/SerializationServices.cs +++ b/backend/src/Squidex/Config/Domain/SerializationServices.cs @@ -6,9 +6,13 @@ // ========================================================================== using System.Security.Claims; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.AspNetCore.Mvc; using Migrations; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; +using NetTopologySuite.IO.Converters; +using NodaTime; +using NodaTime.Serialization.SystemTextJson; using Squidex.Domain.Apps.Core; using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Apps.Json; @@ -20,9 +24,10 @@ using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas.Json; using Squidex.Domain.Apps.Events; using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Json; -using Squidex.Infrastructure.Json.Newtonsoft; using Squidex.Infrastructure.Json.Objects; +using Squidex.Infrastructure.Json.System; using Squidex.Infrastructure.Queries; using Squidex.Infrastructure.Queries.Json; using Squidex.Infrastructure.Reflection; @@ -31,35 +36,43 @@ namespace Squidex.Config.Domain { public static class SerializationServices { - private static JsonSerializerSettings ConfigureJson(TypeNameHandling typeNameHandling, JsonSerializerSettings? settings = null) + private static JsonSerializerOptions ConfigureJson(TypeNameRegistry typeNameRegistry, JsonSerializerOptions? options = null) { - settings ??= new JsonSerializerSettings(); - settings.Converters.Add(new StringEnumConverter()); - - settings.ContractResolver = new ConverterContractResolver( - new ContentFieldDataConverter(), - new JsonValueConverter(), - new StringEnumConverter(), - new SurrogateConverter(), - new SurrogateConverter, JsonFilterSurrogate>(), - new SurrogateConverter(), - new SurrogateConverter(), - new SurrogateConverter(), - new SurrogateConverter(), - new SurrogateConverter(), - new SurrogateConverter(), - new SurrogateConverter(), - new TypeConverterJsonConverter(), - new WriteonlyGeoJsonConverter()); - - settings.NullValueHandling = NullValueHandling.Ignore; - - settings.DateFormatHandling = DateFormatHandling.IsoDateFormat; - settings.DateParseHandling = DateParseHandling.None; - - settings.TypeNameHandling = typeNameHandling; - - return settings; + options ??= new JsonSerializerOptions(JsonSerializerDefaults.Web); + + options.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb); + // It is also a readonly list, so we have to register it first, so that other converters do not pick this up. + options.Converters.Add(new StringConverter(x => x)); + options.Converters.Add(new GeoJsonConverterFactory()); + options.Converters.Add(new InheritanceConverter(typeNameRegistry)); + options.Converters.Add(new InheritanceConverter(typeNameRegistry)); + options.Converters.Add(new InheritanceConverter(typeNameRegistry)); + options.Converters.Add(new InheritanceConverter(typeNameRegistry)); + options.Converters.Add(new JsonValueConverter()); + options.Converters.Add(new ReadonlyDictionaryConverterFactory()); + options.Converters.Add(new ReadonlyListConverterFactory()); + options.Converters.Add(new SurrogateJsonConverter()); + options.Converters.Add(new SurrogateJsonConverter, JsonFilterSurrogate>()); + options.Converters.Add(new SurrogateJsonConverter()); + options.Converters.Add(new SurrogateJsonConverter()); + options.Converters.Add(new SurrogateJsonConverter()); + options.Converters.Add(new SurrogateJsonConverter()); + options.Converters.Add(new SurrogateJsonConverter()); + options.Converters.Add(new SurrogateJsonConverter()); + options.Converters.Add(new SurrogateJsonConverter()); + options.Converters.Add(new StringConverter()); + options.Converters.Add(new StringConverter()); + options.Converters.Add(new StringConverter>()); + options.Converters.Add(new StringConverter>()); + options.Converters.Add(new StringConverter>()); + options.Converters.Add(new StringConverter()); + options.Converters.Add(new StringConverter(x => x)); + options.Converters.Add(new StringConverter()); + options.Converters.Add(new StringConverter()); + options.Converters.Add(new JsonStringEnumConverter()); + options.IncludeFields = true; + + return options; } public static IServiceCollection AddSquidexSerializers(this IServiceCollection services) @@ -79,39 +92,30 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); - services.AddSingletonAs() + services.AddSingletonAs() .As(); services.AddSingletonAs() .AsSelf(); - services.AddSingletonAs(c => JsonSerializer.Create(c.GetRequiredService())) - .AsSelf(); - - services.AddSingletonAs(c => - { - var serializerSettings = ConfigureJson(TypeNameHandling.Auto, new JsonSerializerSettings()); - - var typeNameRegistry = c.GetService(); + services.AddSingletonAs(c => ConfigureJson(c.GetRequiredService())) + .As(); - if (typeNameRegistry != null) - { - serializerSettings.SerializationBinder = new TypeNameSerializationBinder(typeNameRegistry); - } - - return serializerSettings; - }).As(); + services.Configure((c, options) => + { + ConfigureJson(c.GetRequiredService(), options); + }); return services; } public static IMvcBuilder AddSquidexSerializers(this IMvcBuilder builder) { - builder.AddNewtonsoftJson(options => + builder.Services.Configure((c, options) => { - options.AllowInputFormatterExceptionMessages = false; + ConfigureJson(c.GetRequiredService(), options.JsonSerializerOptions); - ConfigureJson(TypeNameHandling.None, options.SerializerSettings); + options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; }); return builder; diff --git a/backend/src/Squidex/Config/Domain/StoreServices.cs b/backend/src/Squidex/Config/Domain/StoreServices.cs index 3786f7916..ba1408dd0 100644 --- a/backend/src/Squidex/Config/Domain/StoreServices.cs +++ b/backend/src/Squidex/Config/Domain/StoreServices.cs @@ -5,12 +5,12 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Text.Json; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Caching.Distributed; using Migrations.Migrations.MongoDb; using MongoDB.Driver; using MongoDB.Driver.Core.Extensions.DiagnosticSources; -using Newtonsoft.Json; using Squidex.Assets; using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities.Apps.DomainObject; @@ -174,10 +174,10 @@ namespace Squidex.Config.Domain .AsOptional().As(); } - services.AddInitializer("Serializer (BSON)", jsonNetSerializer => + services.AddInitializer("Serializer (BSON)", jsonSerializerOptions => { - BsonJsonConvention.Register(jsonNetSerializer); - }, -1); + BsonJsonConvention.Options = jsonSerializerOptions; + }, int.MinValue); } }); diff --git a/backend/src/Squidex/Config/Messaging/MessagingServices.cs b/backend/src/Squidex/Config/Messaging/MessagingServices.cs index d5b618e9e..9db3817f1 100644 --- a/backend/src/Squidex/Config/Messaging/MessagingServices.cs +++ b/backend/src/Squidex/Config/Messaging/MessagingServices.cs @@ -5,7 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using Newtonsoft.Json; +using System.Text.Json; using Squidex.Domain.Apps.Entities.Apps.Plans; using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Backup; @@ -54,7 +54,7 @@ namespace Squidex.Config.Messaging } services.AddSingleton(c => - new NewtonsoftJsonTransportSerializer(c.GetRequiredService())); + new SystemTextJsonTransportSerializer(c.GetRequiredService())); services.AddMessagingTransport(config); services.AddMessaging(options => diff --git a/backend/src/Squidex/Config/Web/WebServices.cs b/backend/src/Squidex/Config/Web/WebServices.cs index 0ad5d1481..97b83b5b4 100644 --- a/backend/src/Squidex/Config/Web/WebServices.cs +++ b/backend/src/Squidex/Config/Web/WebServices.cs @@ -10,8 +10,8 @@ using GraphQL.DataLoader; using GraphQL.DI; using GraphQL.Execution; using GraphQL.MicrosoftDI; -using GraphQL.NewtonsoftJson; using GraphQL.Server.Transports.AspNetCore; +using GraphQL.SystemTextJson; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.ModelBinding; @@ -22,7 +22,6 @@ using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities.Contents.GraphQL; using Squidex.Infrastructure.Caching; -using Squidex.Infrastructure.Json.Newtonsoft; using Squidex.Infrastructure.Json.Objects; using Squidex.Pipeline.Plugins; using Squidex.Web; @@ -105,7 +104,7 @@ namespace Squidex.Config.Web { builder.AddApolloTracing(); builder.AddSchema(); - builder.AddSquidexJson(); // Use Newtonsoft.JSON for custom converters. + builder.AddSystemTextJson(); builder.AddDataLoader(); }); @@ -121,21 +120,5 @@ namespace Squidex.Config.Web services.AddSingletonAs() .AsSelf(); } - - private static IGraphQLBuilder AddSquidexJson(this IGraphQLBuilder builder) - { - builder.AddSerializer(c => - { - var errorInfoProvider = c.GetRequiredService(); - - return new BufferingGraphQLSerializer(new GraphQLSerializer(options => - { - options.Converters.Add(new JsonValueConverter()); - options.Converters.Add(new WriteonlyGeoJsonConverter()); - })); - }); - - return builder; - } } } diff --git a/backend/src/Squidex/Squidex.csproj b/backend/src/Squidex/Squidex.csproj index 2dd7d757c..a7533c9b5 100644 --- a/backend/src/Squidex/Squidex.csproj +++ b/backend/src/Squidex/Squidex.csproj @@ -37,9 +37,7 @@ - - - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -48,7 +46,6 @@ - @@ -62,9 +59,10 @@ - + + diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Rules/RuleTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Rules/RuleTests.cs index 7722efacc..34b5bfaaa 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Rules/RuleTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Rules/RuleTests.cs @@ -82,12 +82,9 @@ namespace Squidex.Domain.Apps.Core.Model.Rules var rule_3 = rule_2.Enable(); Assert.NotSame(rule_1, rule_2); - Assert.False(rule_1.IsEnabled); - Assert.True(rule_2.IsEnabled); Assert.True(rule_3.IsEnabled); - Assert.Same(rule_2, rule_3); } @@ -98,10 +95,8 @@ namespace Squidex.Domain.Apps.Core.Model.Rules var rule_2 = rule_1.Disable(); Assert.NotSame(rule_0, rule_1); - Assert.False(rule_1.IsEnabled); Assert.False(rule_2.IsEnabled); - Assert.Same(rule_1, rule_2); } @@ -112,7 +107,6 @@ namespace Squidex.Domain.Apps.Core.Model.Rules var rule_2 = rule_1.Rename("MyName"); Assert.NotSame(rule_0, rule_1); - Assert.Equal("MyName", rule_1.Name); Assert.Equal("MyName", rule_2.Name); Assert.Same(rule_1, rule_2); diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaTests.cs index e9f7f8f59..39b15bab1 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaTests.cs @@ -445,7 +445,6 @@ namespace Squidex.Domain.Apps.Core.Model.Schemas var schema_2 = schema_1.SetPreviewUrls(urls2); Assert.Equal("Url", schema_1.PreviewUrls["web"]); - Assert.Equal(urls1, schema_1.PreviewUrls); Assert.Equal(urls1, schema_2.PreviewUrls); Assert.Same(schema_1, schema_2); @@ -455,7 +454,7 @@ namespace Squidex.Domain.Apps.Core.Model.Schemas public void Should_serialize_and_deserialize_schema() { var schemaSource = - TestUtils.MixedSchema(SchemaType.Singleton).Schema + TestSchema.MixedSchema(SchemaType.Singleton).Schema .ChangeCategory("Category") .SetFieldRules(FieldRule.Hide("2")) .SetFieldsInLists("field2") diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateFilters/FiltersTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateFilters/FiltersTests.cs index b7fb3aa1e..fa142fc8b 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateFilters/FiltersTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateFilters/FiltersTests.cs @@ -22,7 +22,7 @@ namespace Squidex.Domain.Apps.Core.Operations.GenerateFilters { var languagesConfig = LanguagesConfig.English.Set(Language.DE); - var (schema, components) = TestUtils.MixedSchema(); + var (schema, components) = TestSchema.MixedSchema(); var queryModel = ContentQueryModel.Build(schema, languagesConfig.ToResolver(), components); diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateJsonSchema/JsonSchemaTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateJsonSchema/JsonSchemaTests.cs index e6993bff8..e65858341 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateJsonSchema/JsonSchemaTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateJsonSchema/JsonSchemaTests.cs @@ -22,7 +22,7 @@ namespace Squidex.Domain.Apps.Core.Operations.GenerateJsonSchema { var languagesConfig = LanguagesConfig.English.Set(Language.DE); - var (schema, components) = TestUtils.MixedSchema(); + var (schema, components) = TestSchema.MixedSchema(); var jsonSchema = schema.BuildJsonSchema(languagesConfig.ToResolver(), components); @@ -34,7 +34,7 @@ namespace Squidex.Domain.Apps.Core.Operations.GenerateJsonSchema { var languagesConfig = LanguagesConfig.English.Set(Language.DE); - var (schema, components) = TestUtils.MixedSchema(); + var (schema, components) = TestSchema.MixedSchema(); var jsonSchema = schema.BuildJsonSchemaDynamic(languagesConfig.ToResolver(), components); @@ -46,7 +46,7 @@ namespace Squidex.Domain.Apps.Core.Operations.GenerateJsonSchema { var languagesConfig = LanguagesConfig.English.Set(Language.DE); - var (schema, components) = TestUtils.MixedSchema(); + var (schema, components) = TestSchema.MixedSchema(); var jsonSchema = schema.BuildJsonSchemaFlat(languagesConfig.ToResolver(), components); diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj b/backend/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj index 12a1cf1f7..e248b6cf6 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj @@ -10,6 +10,7 @@ + @@ -22,7 +23,8 @@ - + + diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/TestSchema.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/TestSchema.cs new file mode 100644 index 000000000..0b6de2307 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/TestSchema.cs @@ -0,0 +1,105 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Collections; + +namespace Squidex.Domain.Apps.Core.TestHelpers +{ + public static class TestSchema + { + public static (Schema Schema, ResolvedComponents) MixedSchema(SchemaType type = SchemaType.Default) + { + var componentId1 = DomainId.NewGuid(); + var componentId2 = DomainId.NewGuid(); + var componentIds = ReadonlyList.Create(componentId1, componentId2); + + var component1 = new Schema("component1") + .Publish() + .AddString(1, "unique1", Partitioning.Invariant) + .AddString(2, "shared1", Partitioning.Invariant) + .AddBoolean(3, "shared2", Partitioning.Invariant); + + var component2 = new Schema("component2") + .Publish() + .AddNumber(1, "unique1", Partitioning.Invariant) + .AddNumber(2, "shared1", Partitioning.Invariant) + .AddBoolean(3, "shared2", Partitioning.Invariant); + + var resolvedComponents = new ResolvedComponents(new Dictionary + { + [componentId1] = component1, + [componentId2] = component2 + }); + + var schema = new Schema("user", type: type) + .Publish() + .AddArray(101, "root-array", Partitioning.Language, f => f + .AddAssets(201, "nested-assets", + new AssetsFieldProperties()) + .AddBoolean(202, "nested-boolean", + new BooleanFieldProperties()) + .AddDateTime(203, "nested-datetime", + new DateTimeFieldProperties { Editor = DateTimeFieldEditor.DateTime }) + .AddDateTime(204, "nested-date", + new DateTimeFieldProperties { Editor = DateTimeFieldEditor.Date }) + .AddGeolocation(205, "nested-geolocation", + new GeolocationFieldProperties()) + .AddJson(206, "nested-json", + new JsonFieldProperties()) + .AddJson(207, "nested-json2", + new JsonFieldProperties()) + .AddNumber(208, "nested-number", + new NumberFieldProperties()) + .AddReferences(209, "nested-references", + new ReferencesFieldProperties()) + .AddString(210, "nested-string", + new StringFieldProperties()) + .AddTags(211, "nested-tags", + new TagsFieldProperties()) + .AddUI(212, "nested-ui", + new UIFieldProperties())) + .AddAssets(102, "root-assets", Partitioning.Invariant, + new AssetsFieldProperties()) + .AddBoolean(103, "root-boolean", Partitioning.Invariant, + new BooleanFieldProperties()) + .AddDateTime(104, "root-datetime", Partitioning.Invariant, + new DateTimeFieldProperties { Editor = DateTimeFieldEditor.DateTime }) + .AddDateTime(105, "root-date", Partitioning.Invariant, + new DateTimeFieldProperties { Editor = DateTimeFieldEditor.Date }) + .AddGeolocation(106, "root-geolocation", Partitioning.Invariant, + new GeolocationFieldProperties()) + .AddJson(107, "root-json", Partitioning.Invariant, + new JsonFieldProperties()) + .AddNumber(108, "root-number", Partitioning.Invariant, + new NumberFieldProperties { MinValue = 1, MaxValue = 10 }) + .AddReferences(109, "root-references", Partitioning.Invariant, + new ReferencesFieldProperties()) + .AddString(110, "root-string1", Partitioning.Invariant, + new StringFieldProperties { Label = "My String1", IsRequired = true, AllowedValues = ReadonlyList.Create("a", "b") }) + .AddString(111, "root-string2", Partitioning.Invariant, + new StringFieldProperties { Hints = "My String1" }) + .AddTags(112, "root-tags", Partitioning.Language, + new TagsFieldProperties()) + .AddUI(113, "root-ui", Partitioning.Language, + new UIFieldProperties()) + .AddComponent(114, "root-component", Partitioning.Language, + new ComponentFieldProperties { SchemaIds = componentIds }) + .AddComponents(115, "root-components", Partitioning.Language, + new ComponentsFieldProperties { SchemaIds = componentIds }) + .Update(new SchemaProperties { Hints = "The User" }) + .HideField(104) + .HideField(211, 101) + .DisableField(109) + .DisableField(212, 101) + .LockField(105); + + return (schema, resolvedComponents); + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/TestUtils.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/TestUtils.cs index d8806e30a..ca6f7f284 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/TestUtils.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/TestUtils.cs @@ -5,11 +5,16 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Runtime.Serialization.Formatters.Binary; using System.Security.Claims; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; +using System.Text.Json; +using System.Text.Json.Serialization; +using MongoDB.Bson.IO; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Attributes; +using NetTopologySuite.IO.Converters; using NodaTime; -using NodaTime.Serialization.JsonNet; +using NodaTime.Serialization.SystemTextJson; using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Apps.Json; using Squidex.Domain.Apps.Core.Contents; @@ -20,24 +25,53 @@ using Squidex.Domain.Apps.Core.Rules.Json; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas.Json; using Squidex.Infrastructure; -using Squidex.Infrastructure.Collections; +using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Json; -using Squidex.Infrastructure.Json.Newtonsoft; using Squidex.Infrastructure.Json.Objects; +using Squidex.Infrastructure.Json.System; +using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.Queries; using Squidex.Infrastructure.Queries.Json; using Squidex.Infrastructure.Reflection; +#pragma warning disable SYSLIB0011 // Type or member is obsolete + namespace Squidex.Domain.Apps.Core.TestHelpers { public static class TestUtils { public static readonly IJsonSerializer DefaultSerializer = CreateSerializer(); - public static readonly JsonSerializerSettings DefaultSerializerSettings = CreateSerializerSettings(); + public sealed class ObjectHolder + { + [BsonRequired] + public T Value1 { get; set; } + + [BsonRequired] + public T Value2 { get; set; } + } - public static JsonSerializerSettings CreateSerializerSettings(TypeNameHandling typeNameHandling = TypeNameHandling.Auto, - JsonConverter? converter = null) + static TestUtils() + { + SetupBson(); + } + + public static void SetupBson() + { + BsonDomainIdSerializer.Register(); + BsonInstantSerializer.Register(); + BsonJsonConvention.Register(DefaultOptions()); + BsonJsonValueSerializer.Register(); + } + + public static IJsonSerializer CreateSerializer(Action? configure = null) + { + var serializerSettings = DefaultOptions(configure); + + return new SystemJsonSerializer(serializerSettings); + } + + public static JsonSerializerOptions DefaultOptions(Action? configure = null) { var typeNameRegistry = new TypeNameRegistry() @@ -45,131 +79,71 @@ namespace Squidex.Domain.Apps.Core.TestHelpers .Map(new RuleTypeProvider()) .MapUnmapped(typeof(TestUtils).Assembly); - var serializerSettings = new JsonSerializerSettings - { - SerializationBinder = new TypeNameSerializationBinder(typeNameRegistry), - - ContractResolver = new ConverterContractResolver( - new ContentFieldDataConverter(), - new JsonValueConverter(), - new StringEnumConverter(), - new SurrogateConverter(), - new SurrogateConverter, JsonFilterSurrogate>(), - new SurrogateConverter(), - new SurrogateConverter(), - new SurrogateConverter(), - new SurrogateConverter(), - new SurrogateConverter(), - new SurrogateConverter(), - new SurrogateConverter(), - new TypeConverterJsonConverter(), - new WriteonlyGeoJsonConverter()), - - TypeNameHandling = typeNameHandling - }.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb); - - if (converter != null) - { - serializerSettings.Converters.Add(converter); - } - - return serializerSettings; + var options = new JsonSerializerOptions(JsonSerializerDefaults.Web); + + options.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb); + // It is also a readonly list, so we have to register it first, so that other converters do not pick this up. + options.Converters.Add(new StringConverter(x => x)); + options.Converters.Add(new GeoJsonConverterFactory()); + options.Converters.Add(new InheritanceConverter(typeNameRegistry)); + options.Converters.Add(new InheritanceConverter(typeNameRegistry)); + options.Converters.Add(new InheritanceConverter(typeNameRegistry)); + options.Converters.Add(new InheritanceConverter(typeNameRegistry)); + options.Converters.Add(new JsonValueConverter()); + options.Converters.Add(new ReadonlyDictionaryConverterFactory()); + options.Converters.Add(new ReadonlyListConverterFactory()); + options.Converters.Add(new SurrogateJsonConverter()); + options.Converters.Add(new SurrogateJsonConverter, JsonFilterSurrogate>()); + options.Converters.Add(new SurrogateJsonConverter()); + options.Converters.Add(new SurrogateJsonConverter()); + options.Converters.Add(new SurrogateJsonConverter()); + options.Converters.Add(new SurrogateJsonConverter()); + options.Converters.Add(new SurrogateJsonConverter()); + options.Converters.Add(new SurrogateJsonConverter()); + options.Converters.Add(new SurrogateJsonConverter()); + options.Converters.Add(new StringConverter()); + options.Converters.Add(new StringConverter()); + options.Converters.Add(new StringConverter>()); + options.Converters.Add(new StringConverter>()); + options.Converters.Add(new StringConverter>()); + options.Converters.Add(new StringConverter()); + options.Converters.Add(new StringConverter()); + options.Converters.Add(new StringConverter()); + options.Converters.Add(new JsonStringEnumConverter()); + configure?.Invoke(options); + + return options; } - public static IJsonSerializer CreateSerializer(TypeNameHandling typeNameHandling = TypeNameHandling.Auto, JsonConverter? converter = null) + public static T SerializeAndDeserializeBinary(this T source) { - var serializerSettings = CreateSerializerSettings(typeNameHandling, converter); + using (var stream = new MemoryStream()) + { + var formatter = new BinaryFormatter(); + + formatter.Serialize(stream, source!); + + stream.Position = 0; - return new NewtonsoftJsonSerializer(serializerSettings); + return (T)formatter.Deserialize(stream); + } } - public static (Schema Schema, ResolvedComponents) MixedSchema(SchemaType type = SchemaType.Default) + public static T SerializeAndDeserializeBson(this T value) { - var componentId1 = DomainId.NewGuid(); - var componentId2 = DomainId.NewGuid(); - var componentIds = ReadonlyList.Create(componentId1, componentId2); - - var component1 = new Schema("component1") - .Publish() - .AddString(1, "unique1", Partitioning.Invariant) - .AddString(2, "shared1", Partitioning.Invariant) - .AddBoolean(3, "shared2", Partitioning.Invariant); - - var component2 = new Schema("component2") - .Publish() - .AddNumber(1, "unique1", Partitioning.Invariant) - .AddNumber(2, "shared1", Partitioning.Invariant) - .AddBoolean(3, "shared2", Partitioning.Invariant); - - var resolvedComponents = new ResolvedComponents(new Dictionary + using var stream = new MemoryStream(); + + using (var writer = new BsonBinaryWriter(stream)) + { + BsonSerializer.Serialize(writer, new ObjectHolder { Value1 = value, Value2 = value }); + } + + stream.Position = 0; + + using (var reader = new BsonBinaryReader(stream)) { - [componentId1] = component1, - [componentId2] = component2 - }); - - var schema = new Schema("user", type: type) - .Publish() - .AddArray(101, "root-array", Partitioning.Language, f => f - .AddAssets(201, "nested-assets", - new AssetsFieldProperties()) - .AddBoolean(202, "nested-boolean", - new BooleanFieldProperties()) - .AddDateTime(203, "nested-datetime", - new DateTimeFieldProperties { Editor = DateTimeFieldEditor.DateTime }) - .AddDateTime(204, "nested-date", - new DateTimeFieldProperties { Editor = DateTimeFieldEditor.Date }) - .AddGeolocation(205, "nested-geolocation", - new GeolocationFieldProperties()) - .AddJson(206, "nested-json", - new JsonFieldProperties()) - .AddJson(207, "nested-json2", - new JsonFieldProperties()) - .AddNumber(208, "nested-number", - new NumberFieldProperties()) - .AddReferences(209, "nested-references", - new ReferencesFieldProperties()) - .AddString(210, "nested-string", - new StringFieldProperties()) - .AddTags(211, "nested-tags", - new TagsFieldProperties()) - .AddUI(212, "nested-ui", - new UIFieldProperties())) - .AddAssets(102, "root-assets", Partitioning.Invariant, - new AssetsFieldProperties()) - .AddBoolean(103, "root-boolean", Partitioning.Invariant, - new BooleanFieldProperties()) - .AddDateTime(104, "root-datetime", Partitioning.Invariant, - new DateTimeFieldProperties { Editor = DateTimeFieldEditor.DateTime }) - .AddDateTime(105, "root-date", Partitioning.Invariant, - new DateTimeFieldProperties { Editor = DateTimeFieldEditor.Date }) - .AddGeolocation(106, "root-geolocation", Partitioning.Invariant, - new GeolocationFieldProperties()) - .AddJson(107, "root-json", Partitioning.Invariant, - new JsonFieldProperties()) - .AddNumber(108, "root-number", Partitioning.Invariant, - new NumberFieldProperties { MinValue = 1, MaxValue = 10 }) - .AddReferences(109, "root-references", Partitioning.Invariant, - new ReferencesFieldProperties()) - .AddString(110, "root-string1", Partitioning.Invariant, - new StringFieldProperties { Label = "My String1", IsRequired = true, AllowedValues = ReadonlyList.Create("a", "b") }) - .AddString(111, "root-string2", Partitioning.Invariant, - new StringFieldProperties { Hints = "My String1" }) - .AddTags(112, "root-tags", Partitioning.Language, - new TagsFieldProperties()) - .AddUI(113, "root-ui", Partitioning.Language, - new UIFieldProperties()) - .AddComponent(114, "root-component", Partitioning.Language, - new ComponentFieldProperties { SchemaIds = componentIds }) - .AddComponents(115, "root-components", Partitioning.Language, - new ComponentsFieldProperties { SchemaIds = componentIds }) - .Update(new SchemaProperties { Hints = "The User" }) - .HideField(104) - .HideField(211, 101) - .DisableField(109) - .DisableField(212, 101) - .LockField(105); - - return (schema, resolvedComponents); + return BsonSerializer.Deserialize>(reader).Value1; + } } public static T SerializeAndDeserialize(this object value) @@ -181,9 +155,30 @@ namespace Squidex.Domain.Apps.Core.TestHelpers public static T SerializeAndDeserialize(this T value) { - var json = DefaultSerializer.Serialize(value); + var json = DefaultSerializer.Serialize(new ObjectHolder { Value1 = value, Value2 = value }); - return DefaultSerializer.Deserialize(json); + return DefaultSerializer.Deserialize>(json).Value1; + } + + public static T Deserialize(string value) + { + var json = DefaultSerializer.Serialize(new ObjectHolder { Value1 = value, Value2 = value }); + + return DefaultSerializer.Deserialize>(json).Value1; + } + + public static T Deserialize(object value) + { + var json = DefaultSerializer.Serialize(new ObjectHolder { Value1 = value, Value2 = value }); + + return DefaultSerializer.Deserialize>(json).Value1; + } + + public static string CleanJson(this string json) + { + using var document = JsonDocument.Parse(json); + + return DefaultSerializer.Serialize(document, true); } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppState.json b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppState.json new file mode 100644 index 000000000..d8cc06e8d --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppState.json @@ -0,0 +1,133 @@ +{ + "name": "test", + "label": "label", + "description": "description", + "roles": { + "custom": { + "permissions": [ + + ], + "properties": { + "ui_§§_schemas_§§_hide": true + } + } + }, + "clients": { + "default": { + "name": "default", + "secret": "o3yzxz8bpcpwd4zllox14nicrktgwlxxop2zfmahtiex", + "role": "Owner", + "apiCallsLimit": 0, + "apiTrafficLimit": 0, + "allowAnonymous": false + } + }, + "settings": { + "patterns": [ + { + "name": "Email", + "regex": "^[a-zA-Z0-9.!#$%&’*+\\/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:.[a-zA-Z0-9-]+)*$" + }, + { + "name": "Phone", + "regex": "^\\(*\\+*[1-9]{0,3}\\)*-*[1-9]{0,3}[-. /]*\\(*[2-9]\\d{2}\\)*[-. /]*\\d{3}[-. /]*\\d{4} *e*x*t*\\.* *\\d{0,4}$" + }, + { + "name": "Slug", + "regex": "^[a-z0-9]+(\\-[a-z0-9]+)*$" + }, + { + "name": "Url", + "regex": "^(?:http(s)?:\\/\\/)?[\\w.-]+(?:\\.[\\w\\.-]+)+[\\w\\-\\._~:\\/?#%[\\]@!\\$&'\\(\\)\\*\\+,;=.]+$" + } + ], + "editors": [ + + ], + "hideScheduler": false, + "hideDateTimeModeButton": false + }, + "contributors": { + "62cee33bcc71c0d140ad103a": "Owner" + }, + "assetScripts": { + + }, + "languages": { + "languages": { + "en": { + "fallback": [ + + ], + "isOptional": false + }, + "de": { + "fallback": [ + + ], + "isOptional": false + }, + "de-DE": { + "fallback": [ + "en" + ], + "isOptional": true + } + }, + "master": "en" + }, + "workflows": { + "274c3c3f-ad0e-4304-aa87-9d686bf082c1": { + "initial": "Draft", + "name": "test", + "steps": { + "Archived": { + "transitions": { + "Draft": { + + } + }, + "noUpdate": false, + "noUpdateRules": { + + }, + "color": "#eb3142" + }, + "Draft": { + "transitions": { + "Archived": { + + }, + "Published": { + + } + }, + "noUpdate": false, + "color": "#8091a5" + }, + "Published": { + "transitions": { + "Archived": { + + }, + "Draft": { + + } + }, + "noUpdate": false, + "color": "#4bb958" + } + }, + "schemaIds": [ + + ] + } + }, + "isDeleted": false, + "id": "7c45937e-e489-453f-81f8-a1432e778ea2", + "createdBy": "subject:62cee33bcc71c0d140ad103a", + "lastModifiedBy": "subject:62cee33bcc71c0d140ad103a", + "created": "2022-07-22T17:16:05Z", + "lastModified": "2022-07-22T17:17:19Z", + "version": 10 +} \ No newline at end of file diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppStateTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppStateTests.cs new file mode 100644 index 000000000..454460a9d --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppStateTests.cs @@ -0,0 +1,42 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Text.Json.Serialization; +using Squidex.Domain.Apps.Core.TestHelpers; +using Squidex.Infrastructure.Json; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Apps.DomainObject +{ + public class AppStateTests + { + private readonly IJsonSerializer serializer = TestUtils.CreateSerializer(options => + { + options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; + }); + + [Fact] + public void Should_deserialize_state() + { + var json = File.ReadAllText("Apps/DomainObject/AppState.json"); + + var deserialized = serializer.Deserialize(json); + + Assert.NotNull(deserialized); + } + + [Fact] + public void Should_serialize_deserialize_state() + { + var json = File.ReadAllText("Apps/DomainObject/AppState.json").CleanJson(); + + var serialized = serializer.Serialize(serializer.Deserialize(json), true); + + Assert.Equal(json, serialized); + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetQueryTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetQueryTests.cs index 5f838af8e..bebf3f9b0 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetQueryTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetQueryTests.cs @@ -29,12 +29,12 @@ namespace Squidex.Domain.Apps.Entities.Assets.MongoDb static AssetQueryTests() { - DomainIdSerializer.Register(); + BsonDomainIdSerializer.Register(); - TypeConverterStringSerializer.Register(); - TypeConverterStringSerializer.Register(); + BsonStringSerializer.Register(); + BsonStringSerializer.Register(); - InstantSerializer.Register(); + BsonInstantSerializer.Register(); } [Fact] diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetsQueryFixture.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetsQueryFixture.cs index 1dfedd20c..b0cda90de 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetsQueryFixture.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetsQueryFixture.cs @@ -40,7 +40,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.MongoDb public AssetsQueryFixture() { - SetupJson(); + BsonJsonConvention.Register(TestUtils.DefaultOptions()); mongoClient = new MongoClient(TestConfig.Configuration["mongodb:configuration"]); mongoDatabase = mongoClient.GetDatabase(TestConfig.Configuration["mongodb:database"]); @@ -138,13 +138,6 @@ namespace Squidex.Domain.Apps.Entities.Assets.MongoDb await mongoDatabase.RunCommandAsync("{ profile : 2 }", cancellationToken: ct); } - private static void SetupJson() - { - var serializer = JsonSerializer.Create(TestUtils.DefaultSerializerSettings); - - BsonJsonConvention.Register(serializer); - } - public DomainId RandomAppId() { return AppIds[random.Next(0, AppIds.Length)].Id; diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLMutationTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLMutationTests.cs index 3454951ca..72a885067 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLMutationTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLMutationTests.cs @@ -5,13 +5,13 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Text.Json; using System.Text.RegularExpressions; using FakeItEasy; using GraphQL; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using NodaTime.Text; using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.TestHelpers; using Squidex.Domain.Apps.Entities.Contents.Commands; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; @@ -741,16 +741,16 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL private string CreateQuery(string query) { query = query - .Replace("", contentId.ToString(), StringComparison.Ordinal) .Replace("'", "\"", StringComparison.Ordinal) .Replace("`", "\"", StringComparison.Ordinal) + .Replace("", contentId.ToString(), StringComparison.Ordinal) .Replace("", TestContent.AllFields, StringComparison.Ordinal); if (query.Contains("", StringComparison.Ordinal)) { var data = TestContent.Input(content, TestSchemas.Ref1.Id, TestSchemas.Ref2.Id); - var dataJson = JsonConvert.SerializeObject(data, Formatting.Indented); + var dataJson = TestUtils.DefaultSerializer.Serialize(data, true); // Use Properties without quotes. dataJson = Regex.Replace(dataJson, "\"([^\"]+)\":", x => x.Groups[1].Value + ":"); @@ -775,7 +775,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL data = TestContent.Input(content, TestSchemas.Ref1.Id, TestSchemas.Ref2.Id) }; - return serializer.ReadNode(JObject.FromObject(input))!; + var element = JsonSerializer.SerializeToElement(input, TestUtils.DefaultOptions()); + + return serializer.ReadNode(element)!; } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs index 05dc11870..312545f66 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs @@ -1153,6 +1153,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL { return query .Replace("'", "\"", StringComparison.Ordinal) + .Replace("`", "\"", StringComparison.Ordinal) .Replace("", id.ToString(), StringComparison.Ordinal) .Replace("", TestAsset.AllFields, StringComparison.Ordinal) .Replace("", TestContent.AllFields, StringComparison.Ordinal) diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs index 6b25a3cd4..8612f90b3 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs @@ -9,12 +9,11 @@ using FakeItEasy; using GraphQL; using GraphQL.DataLoader; using GraphQL.Execution; -using GraphQL.NewtonsoftJson; +using GraphQL.SystemTextJson; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Newtonsoft.Json; using Squidex.Caching; using Squidex.Domain.Apps.Core; using Squidex.Domain.Apps.Core.ExtractReferenceIds; @@ -25,7 +24,6 @@ using Squidex.Domain.Apps.Entities.Contents.TestData; using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Json.Newtonsoft; using Squidex.Shared; using Squidex.Shared.Users; using Xunit; @@ -36,13 +34,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL { public class GraphQLTestBase : IClassFixture { - protected readonly GraphQLSerializer serializer = new GraphQLSerializer(options => - { - options.Formatting = Formatting.Indented; - options.Converters.Add(new JsonValueConverter()); - options.Converters.Add(new WriteonlyGeoJsonConverter()); - }); - + protected readonly GraphQLSerializer serializer = new GraphQLSerializer(TestUtils.DefaultOptions()); protected readonly IAssetQueryService assetQuery = A.Fake(); protected readonly ICommandBus commandBus = A.Fake(); protected readonly IContentQueryService contentQuery = A.Fake(); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentQueryTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentQueryTests.cs index 7b227b4a4..70301b2ec 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentQueryTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentQueryTests.cs @@ -35,12 +35,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb static ContentQueryTests() { - DomainIdSerializer.Register(); + BsonDomainIdSerializer.Register(); - TypeConverterStringSerializer.Register(); - TypeConverterStringSerializer.Register(); + BsonStringSerializer.Register(); + BsonStringSerializer.Register(); - InstantSerializer.Register(); + BsonInstantSerializer.Register(); } public ContentQueryTests() diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryFixture.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryFixture.cs index 78dbfed90..33b29ea79 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryFixture.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryFixture.cs @@ -74,7 +74,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb protected ContentsQueryFixtureBase(bool dedicatedCollections) { - SetupJson(); + BsonJsonConvention.Register(TestUtils.DefaultOptions()); mongoClient = new MongoClient(TestConfig.Configuration["mongodb:configuration"]); mongoDatabase = mongoClient.GetDatabase(TestConfig.Configuration["mongodb:database"]); @@ -203,13 +203,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb return appProvider; } - private static void SetupJson() - { - var serializer = JsonSerializer.Create(TestUtils.DefaultSerializerSettings); - - BsonJsonConvention.Register(serializer); - } - public DomainId RandomAppId() { return AppIds[random.Next(0, AppIds.Length)].Id; diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/StatusSerializerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/StatusSerializerTests.cs index 0eb7ac714..4a9e8d4e2 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/StatusSerializerTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/StatusSerializerTests.cs @@ -22,7 +22,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb public StatusSerializerTests() { - TypeConverterStringSerializer.Register(); + BsonStringSerializer.Register(); } [Fact] diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryParserTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryParserTests.cs index cf6094d82..8a7ef75ad 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryParserTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryParserTests.cs @@ -87,7 +87,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries [Fact] public async Task Should_parse_json_query_without_schema() { - var query = Q.Empty.WithJsonQuery("{ 'filter': { 'path': 'status', 'op': 'eq', 'value': 'Draft' } }"); + var query = Q.Empty.WithJsonQuery("{ \"filter\": { \"path\": \"status\", \"op\": \"eq\", \"value\": \"Draft\" } }"); var q = await sut.ParseAsync(requestContext, query); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/AtlasTextIndexFixture.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/AtlasTextIndexFixture.cs index 5b7b66507..52450ee9f 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/AtlasTextIndexFixture.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/AtlasTextIndexFixture.cs @@ -8,31 +8,40 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Options; using MongoDB.Driver; -using Newtonsoft.Json; using Squidex.Domain.Apps.Core.TestHelpers; using Squidex.Domain.Apps.Entities.MongoDb.Text; using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Infrastructure.MongoDb; +using Xunit; namespace Squidex.Domain.Apps.Entities.Contents.Text { - public sealed class AtlasTextIndexFixture + public sealed class AtlasTextIndexFixture : IAsyncLifetime { public AtlasTextIndex Index { get; } public AtlasTextIndexFixture() { - BsonJsonConvention.Register(JsonSerializer.Create(TestUtils.CreateSerializerSettings())); + BsonJsonConvention.Register(TestUtils.DefaultOptions()); - DomainIdSerializer.Register(); + BsonDomainIdSerializer.Register(); var mongoClient = new MongoClient(TestConfig.Configuration["atlas:configuration"]); var mongoDatabase = mongoClient.GetDatabase(TestConfig.Configuration["atlas:database"]); var options = TestConfig.Configuration.GetSection("atlas").Get(); - Index = new AtlasTextIndex(mongoDatabase, Options.Create(options), false); - Index.InitializeAsync(default).Wait(); + Index = new AtlasTextIndex(mongoDatabase, Options.Create(options)); + } + + public Task InitializeAsync() + { + return Index.InitializeAsync(default); + } + + public Task DisposeAsync() + { + return Task.CompletedTask; } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/AzureTextIndexFixture.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/AzureTextIndexFixture.cs index 02476e645..a024f104e 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/AzureTextIndexFixture.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/AzureTextIndexFixture.cs @@ -7,10 +7,11 @@ using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Extensions.Text.Azure; +using Xunit; namespace Squidex.Domain.Apps.Entities.Contents.Text { - public sealed class AzureTextIndexFixture + public sealed class AzureTextIndexFixture : IAsyncLifetime { public AzureTextIndex Index { get; } @@ -20,7 +21,16 @@ namespace Squidex.Domain.Apps.Entities.Contents.Text TestConfig.Configuration["azureText:serviceEndpoint"], TestConfig.Configuration["azureText:apiKey"], TestConfig.Configuration["azureText:indexName"]); - Index.InitializeAsync(default).Wait(); + } + + public Task InitializeAsync() + { + return Index.InitializeAsync(default); + } + + public Task DisposeAsync() + { + return Task.CompletedTask; } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/ElasticTextIndexFixture.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/ElasticTextIndexFixture.cs index 725393341..6aa818ad8 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/ElasticTextIndexFixture.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/ElasticTextIndexFixture.cs @@ -5,12 +5,14 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using Squidex.Domain.Apps.Core.TestHelpers; using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Extensions.Text.ElasticSearch; +using Xunit; namespace Squidex.Domain.Apps.Entities.Contents.Text { - public sealed class ElasticTextIndexFixture + public sealed class ElasticTextIndexFixture : IAsyncLifetime { public ElasticSearchTextIndex Index { get; } @@ -18,8 +20,18 @@ namespace Squidex.Domain.Apps.Entities.Contents.Text { Index = new ElasticSearchTextIndex( TestConfig.Configuration["elastic:configuration"], - TestConfig.Configuration["elastic:indexName"]); - Index.InitializeAsync(default).Wait(); + TestConfig.Configuration["elastic:indexName"], + TestUtils.DefaultSerializer); + } + + public Task InitializeAsync() + { + return Index.InitializeAsync(default); + } + + public Task DisposeAsync() + { + return Task.CompletedTask; } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/MongoTextIndexFixture.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/MongoTextIndexFixture.cs index e20ed859a..7e5611f64 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/MongoTextIndexFixture.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/MongoTextIndexFixture.cs @@ -6,29 +6,38 @@ // ========================================================================== using MongoDB.Driver; -using Newtonsoft.Json; using Squidex.Domain.Apps.Core.TestHelpers; using Squidex.Domain.Apps.Entities.MongoDb.Text; using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Infrastructure.MongoDb; +using Xunit; namespace Squidex.Domain.Apps.Entities.Contents.Text { - public sealed class MongoTextIndexFixture + public sealed class MongoTextIndexFixture : IAsyncLifetime { public MongoTextIndex Index { get; } public MongoTextIndexFixture() { - BsonJsonConvention.Register(JsonSerializer.Create(TestUtils.CreateSerializerSettings())); + BsonJsonConvention.Register(TestUtils.DefaultOptions()); - DomainIdSerializer.Register(); + BsonDomainIdSerializer.Register(); var mongoClient = new MongoClient(TestConfig.Configuration["mongodb:configuration"]); var mongoDatabase = mongoClient.GetDatabase(TestConfig.Configuration["mongodb:database"]); - Index = new MongoTextIndex(mongoDatabase, false); - Index.InitializeAsync(default).Wait(); + Index = new MongoTextIndex(mongoDatabase); + } + + public Task InitializeAsync() + { + return Index.InitializeAsync(default); + } + + public Task DisposeAsync() + { + return Task.CompletedTask; } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/SchemaState.json b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/SchemaState.json new file mode 100644 index 000000000..498ac7af4 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/SchemaState.json @@ -0,0 +1,324 @@ +{ + "appId": "7c45937e-e489-453f-81f8-a1432e778ea2,test", + "schemaDef": { + "name": "test", + "isPublished": true, + "type": "Default", + "properties": { + "tags": [ + "1", + "2", + "3" + ], + "contentsSidebarUrl": "contents-sidebar", + "contentSidebarUrl": "content-sidebar", + "contentEditorUrl": "content-editor", + "validateOnPublish": true, + "label": "label", + "hints": "hints" + }, + "scripts": { + "change": "change", + "create": "create", + "update": "update", + "delete": "delete", + "query": "query", + "queryPre": "prepare" + }, + "fieldsInLists": [ + "string" + ], + "fieldsInReferences": [ + "boolean" + ], + "fieldRules": [ + { + "action": "Disable", + "field": "array", + "condition": "1 == 2" + } + ], + "fields": [ + { + "id": 1, + "name": "string", + "partitioning": "language", + "isHidden": false, + "isLocked": false, + "isDisabled": false, + "properties": { + "$type": "StringField", + "isUnique": false, + "isEmbeddable": false, + "inlineEditable": true, + "createEnum": false, + "contentType": "Unspecified", + "editor": "Input", + "isRequired": true, + "isRequiredOnPublish": true, + "isHalfWidth": false + } + }, + { + "id": 2, + "name": "assets", + "partitioning": "language", + "isHidden": false, + "isLocked": false, + "isDisabled": false, + "properties": { + "$type": "AssetsField", + "previewMode": "FileName", + "expectedType": "Image", + "allowDuplicates": false, + "resolveFirst": false, + "allowedExtensions": [ + "a", + "b", + "c", + "d" + ], + "isRequired": false, + "isRequiredOnPublish": false, + "isHalfWidth": false + } + }, + { + "id": 3, + "name": "boolean", + "partitioning": "language", + "isHidden": false, + "isLocked": false, + "isDisabled": false, + "properties": { + "$type": "BooleanField", + "defaultValues": { + "en": true, + "de": false + }, + "inlineEditable": false, + "editor": "Checkbox", + "isRequired": false, + "isRequiredOnPublish": false, + "isHalfWidth": false + } + }, + { + "id": 4, + "name": "component", + "partitioning": "language", + "isHidden": false, + "isLocked": false, + "isDisabled": false, + "properties": { + "$type": "ComponentField", + "schemaId": "62a1d2a8-f08d-4870-8cb9-cb3ba286d56c", + "schemaIds": [ + "62a1d2a8-f08d-4870-8cb9-cb3ba286d56c" + ], + "isRequired": false, + "isRequiredOnPublish": false, + "isHalfWidth": false + } + }, + { + "id": 5, + "name": "components", + "partitioning": "language", + "isHidden": false, + "isLocked": false, + "isDisabled": false, + "properties": { + "$type": "ComponentsField", + "schemaId": "62a1d2a8-f08d-4870-8cb9-cb3ba286d56c", + "schemaIds": [ + "62a1d2a8-f08d-4870-8cb9-cb3ba286d56c" + ], + "isRequired": false, + "isRequiredOnPublish": false, + "isHalfWidth": false + } + }, + { + "id": 6, + "name": "date", + "partitioning": "language", + "isHidden": false, + "isLocked": false, + "isDisabled": false, + "properties": { + "$type": "DateTimeField", + "maxValue": "2022-07-22T17:21:13Z", + "minValue": "2022-07-22T17:21:12Z", + "editor": "DateTime", + "isRequired": true, + "isRequiredOnPublish": true, + "isHalfWidth": false + } + }, + { + "id": 7, + "name": "geolocation", + "partitioning": "language", + "isHidden": false, + "isLocked": false, + "isDisabled": false, + "properties": { + "$type": "GeolocationField", + "editor": "Map", + "isRequired": false, + "isRequiredOnPublish": false, + "isHalfWidth": true + } + }, + { + "id": 8, + "name": "json", + "partitioning": "language", + "isHidden": false, + "isLocked": false, + "isDisabled": false, + "properties": { + "$type": "JsonField", + "isRequired": false, + "isRequiredOnPublish": false, + "isHalfWidth": false + } + }, + { + "id": 9, + "name": "number", + "partitioning": "language", + "isHidden": false, + "isLocked": false, + "isDisabled": false, + "properties": { + "$type": "NumberField", + "isUnique": false, + "inlineEditable": false, + "editor": "Input", + "isRequired": false, + "isRequiredOnPublish": false, + "isHalfWidth": false, + "tags": [ + "1", + "2", + "3" + ] + } + }, + { + "id": 10, + "name": "references", + "partitioning": "language", + "isHidden": false, + "isLocked": false, + "isDisabled": false, + "properties": { + "$type": "ReferencesField", + "resolveReference": false, + "allowDuplicates": false, + "mustBePublished": false, + "editor": "List", + "schemaId": "62a1d2a8-f08d-4870-8cb9-cb3ba286d56c", + "schemaIds": [ + "62a1d2a8-f08d-4870-8cb9-cb3ba286d56c" + ], + "isRequired": false, + "isRequiredOnPublish": false, + "isHalfWidth": false + } + }, + { + "id": 11, + "name": "tags", + "partitioning": "language", + "isHidden": false, + "isLocked": false, + "isDisabled": false, + "properties": { + "$type": "TagsField", + "defaultValues": { + "de-DE": [ + "4", + "5", + "6" + ] + }, + "defaultValue": [ + "1", + "2", + "3" + ], + "createEnum": false, + "editor": "Tags", + "normalization": "None", + "isRequired": false, + "isRequiredOnPublish": false, + "isHalfWidth": false + } + }, + { + "id": 12, + "name": "array", + "partitioning": "language", + "isHidden": false, + "isLocked": false, + "isDisabled": false, + "properties": { + "$type": "ArrayField", + "isRequired": false, + "isRequiredOnPublish": false, + "isHalfWidth": false + }, + "children": [ + { + "id": 14, + "name": "nested", + "isHidden": false, + "isLocked": false, + "isDisabled": false, + "properties": { + "$type": "StringField", + "isUnique": false, + "isEmbeddable": false, + "inlineEditable": false, + "createEnum": false, + "contentType": "Unspecified", + "editor": "Input", + "isRequired": false, + "isRequiredOnPublish": false, + "isHalfWidth": false + } + } + ] + }, + { + "id": 13, + "name": "ui", + "partitioning": "invariant", + "isHidden": false, + "isLocked": false, + "isDisabled": false, + "properties": { + "$type": "UIField", + "editor": "Separator", + "isRequired": false, + "isRequiredOnPublish": false, + "isHalfWidth": false + } + } + ], + "previewUrls": { + "Web": "URL" + } + }, + "schemaFieldsTotal": 14, + "isDeleted": false, + "id": "62a1d2a8-f08d-4870-8cb9-cb3ba286d56c", + "createdBy": "subject:62cee33bcc71c0d140ad103a", + "lastModifiedBy": "subject:62cee33bcc71c0d140ad103a", + "created": "2022-07-22T17:18:04Z", + "lastModified": "2022-07-22T17:21:54Z", + "version": 30 +} \ No newline at end of file diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/SchemaStateTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/SchemaStateTests.cs new file mode 100644 index 000000000..3958cd77e --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/SchemaStateTests.cs @@ -0,0 +1,42 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Text.Json.Serialization; +using Squidex.Domain.Apps.Core.TestHelpers; +using Squidex.Infrastructure.Json; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject +{ + public class SchemaStateTests + { + private readonly IJsonSerializer serializer = TestUtils.CreateSerializer(options => + { + options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; + }); + + [Fact] + public void Should_deserialize_state() + { + var json = File.ReadAllText("Schemas/DomainObject/SchemaState.json"); + + var deserialized = serializer.Deserialize(json); + + Assert.NotNull(deserialized); + } + + [Fact] + public void Should_serialize_deserialize_state() + { + var json = File.ReadAllText("Schemas/DomainObject/SchemaState.json").CleanJson(); + + var serialized = serializer.Serialize(serializer.Deserialize(json), true); + + Assert.Equal(json, serialized); + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/MongoDb/SchemasHashFixture.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/MongoDb/SchemasHashFixture.cs index 032522cc1..fd95465dc 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/MongoDb/SchemasHashFixture.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/MongoDb/SchemasHashFixture.cs @@ -18,7 +18,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.MongoDb public SchemasHashFixture() { - InstantSerializer.Register(); + BsonInstantSerializer.Register(); var mongoClient = new MongoClient(TestConfig.Configuration["mongodb:configuration"]); var mongoDatabase = mongoClient.GetDatabase(TestConfig.Configuration["mongodb:database"]); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj index 2bdaa7354..fa7db82fe 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj @@ -24,7 +24,7 @@ - + all @@ -61,6 +61,9 @@ + + PreserveNewest + PreserveNewest @@ -70,5 +73,8 @@ PreserveNewest + + PreserveNewest + \ No newline at end of file diff --git a/backend/tests/Squidex.Infrastructure.Tests/Collections/ListDictionaryTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Collections/ListDictionaryTests.cs index c299b3c1d..e5abfafcb 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Collections/ListDictionaryTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Collections/ListDictionaryTests.cs @@ -6,6 +6,7 @@ // ========================================================================== using System.Collections; +using Squidex.Infrastructure.TestHelpers; using Xunit; #pragma warning disable xUnit2017 // Do not use Contains() to check if a value exists in a collection @@ -460,5 +461,20 @@ namespace Squidex.Infrastructure.Collections Assert.Equal(2, sut.Capacity); } + + [Fact] + public void Should_serialize_and_deserialize() + { + var sut = new Dictionary + { + [11] = 1, + [12] = 2, + [13] = 3 + }.ToReadonlyDictionary(); + + var serialized = sut.SerializeAndDeserialize(); + + Assert.Equal(sut, serialized); + } } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Collections/ReadonlyDictionaryTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Collections/ReadonlyDictionaryTests.cs index 38c24e1e4..247e6f415 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Collections/ReadonlyDictionaryTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Collections/ReadonlyDictionaryTests.cs @@ -12,6 +12,14 @@ namespace Squidex.Infrastructure.Collections { public class ReadonlyDictionaryTests { + internal sealed class Inherited : ReadonlyDictionary + { + public Inherited(IDictionary inner) + : base(inner) + { + } + } + [Fact] public void Should_return_empty_instance_for_empty_source() { @@ -96,5 +104,20 @@ namespace Squidex.Infrastructure.Collections Assert.Equal(sut, serialized); } + + [Fact] + public void Should_serialize_and_deserialize_inherited() + { + var sut = new Inherited(new Dictionary + { + [11] = 1, + [12] = 2, + [13] = 3 + }); + + var serialized = sut.SerializeAndDeserialize(); + + Assert.Equal(sut, serialized); + } } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Collections/ReadonlyListTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Collections/ReadonlyListTests.cs index 932fd58ab..3034352e7 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Collections/ReadonlyListTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Collections/ReadonlyListTests.cs @@ -12,6 +12,14 @@ namespace Squidex.Infrastructure.Collections { public class ReadonlyListTests { + internal sealed class Inherited : ReadonlyList + { + public Inherited(IList inner) + : base(inner) + { + } + } + [Fact] public void Should_return_empty_instance_for_empty_array() { @@ -61,7 +69,27 @@ namespace Squidex.Infrastructure.Collections [Fact] public void Should_serialize_and_deserialize() { - var sut = ReadonlyList.Create(1, 2, 3); + var sut = new List + { + 1, + 2, + 3 + }.ToReadonlyList(); + + var serialized = sut.SerializeAndDeserialize(); + + Assert.Equal(sut, serialized); + } + + [Fact] + public void Should_serialize_and_deserialize_inherited() + { + var sut = new Inherited(new List + { + 1, + 2, + 3 + }); var serialized = sut.SerializeAndDeserialize(); diff --git a/backend/tests/Squidex.Infrastructure.Tests/Commands/CommandResultTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Commands/CommandResultTests.cs index 029b9af8a..e71d21e5a 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Commands/CommandResultTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Commands/CommandResultTests.cs @@ -15,17 +15,7 @@ namespace Squidex.Infrastructure.Commands [Fact] public void Should_serialize_and_deserialize() { - var sut = new CommandResult(DomainId.NewGuid(), 3, 2, "result"); - - var serialized = sut.SerializeAndDeserialize(); - - Assert.Equal(sut, serialized); - } - - [Fact] - public void Should_serialize_and_deserialize_empty() - { - var sut = CommandResult.Empty(DomainId.NewGuid(), 3, 2); + var sut = new CommandResult(DomainId.NewGuid(), 3, 2, null!); var serialized = sut.SerializeAndDeserialize(); diff --git a/backend/tests/Squidex.Infrastructure.Tests/DomainIdTests.cs b/backend/tests/Squidex.Infrastructure.Tests/DomainIdTests.cs index 9fefa5299..9e3421e90 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/DomainIdTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/DomainIdTests.cs @@ -6,7 +6,7 @@ // ========================================================================== using System.ComponentModel; -using System.Runtime.Serialization; +using System.Text.Json.Serialization; using FluentAssertions; using Squidex.Infrastructure.TestHelpers; using Xunit; @@ -19,7 +19,7 @@ namespace Squidex.Infrastructure public class MyTest { - [IgnoreDataMember] + [JsonIgnore] public DomainId Calculated { get => DomainId.Combine(Id0, Id1.Id); @@ -163,19 +163,6 @@ namespace Squidex.Infrastructure Assert.Equal(domainId, serialized); } - [Fact] - public void Should_serialize_and_deserialize_as_dictionary() - { - var dictionary = new Dictionary - { - [DomainId.Create("123")] = 321 - }; - - var serialized = dictionary.SerializeAndDeserialize(); - - Assert.Equal(321, serialized[DomainId.Create("123")]); - } - [Fact] public void Should_serialize_and_deserialize_in_object() { diff --git a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/DefaultEventFormatterTests.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/DefaultEventFormatterTests.cs index 2bcbd05c2..6324d70e0 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/DefaultEventFormatterTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/DefaultEventFormatterTests.cs @@ -6,6 +6,7 @@ // ========================================================================== using NodaTime; +using Squidex.Infrastructure.Json.System; using Squidex.Infrastructure.Migrations; using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.TestHelpers; @@ -34,7 +35,7 @@ namespace Squidex.Infrastructure.EventSourcing .Map(typeof(MyEvent), "Event") .Map(typeof(MyOldEvent), "OldEvent"); - sut = new DefaultEventFormatter(typeNameRegistry, TestUtils.CreateSerializer(typeNameRegistry)); + sut = new DefaultEventFormatter(typeNameRegistry, TestUtils.DefaultSerializer); } [Fact] diff --git a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/GetEventStoreFixture.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/GetEventStoreFixture.cs index d70225bc6..71597b2be 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/GetEventStoreFixture.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/GetEventStoreFixture.cs @@ -7,10 +7,11 @@ using EventStore.Client; using Squidex.Infrastructure.TestHelpers; +using Xunit; namespace Squidex.Infrastructure.EventSourcing { - public sealed class GetEventStoreFixture : IDisposable + public sealed class GetEventStoreFixture : IAsyncLifetime { private readonly EventStoreClientSettings settings; @@ -23,15 +24,14 @@ namespace Squidex.Infrastructure.EventSourcing settings = EventStoreClientSettings.Create(TestConfig.Configuration["eventStore:configuration"]); EventStore = new GetEventStore(settings, TestUtils.DefaultSerializer); - EventStore.InitializeAsync(default).Wait(); } - public void Dispose() + public Task InitializeAsync() { - CleanupAsync().Wait(); + return EventStore.InitializeAsync(default); } - private async Task CleanupAsync() + public async Task DisposeAsync() { var projectionsManager = new EventStoreProjectionManagementClient(settings); diff --git a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/MongoEventStoreFixture.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/MongoEventStoreFixture.cs index b3601be1e..8ab042f11 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/MongoEventStoreFixture.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/MongoEventStoreFixture.cs @@ -7,15 +7,15 @@ using FakeItEasy; using MongoDB.Driver; -using Newtonsoft.Json; using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.TestHelpers; +using Xunit; #pragma warning disable MA0048 // File name must match type name namespace Squidex.Infrastructure.EventSourcing { - public abstract class MongoEventStoreFixture : IDisposable + public abstract class MongoEventStoreFixture : IAsyncLifetime { private readonly IMongoClient mongoClient; private readonly IMongoDatabase mongoDatabase; @@ -28,22 +28,19 @@ namespace Squidex.Infrastructure.EventSourcing mongoClient = new MongoClient(connectionString); mongoDatabase = mongoClient.GetDatabase(TestConfig.Configuration["mongodb:database"]); - BsonJsonConvention.Register(JsonSerializer.Create(TestUtils.DefaultSettings())); + BsonJsonConvention.Register(TestUtils.DefaultOptions()); EventStore = new MongoEventStore(mongoDatabase, notifier); - EventStore.InitializeAsync(default).Wait(); } - public void Cleanup() + public Task InitializeAsync() { - mongoClient.DropDatabase("EventStoreTest"); + return EventStore.InitializeAsync(default); } - public void Dispose() + public Task DisposeAsync() { - Cleanup(); - - GC.SuppressFinalize(this); + return mongoClient.DropDatabaseAsync(mongoDatabase.DatabaseNamespace.DatabaseName); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/MongoParallelInsertTests.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/MongoParallelInsertTests.cs index a6ba8d6a1..995fb5ebb 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/MongoParallelInsertTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/MongoParallelInsertTests.cs @@ -70,7 +70,6 @@ namespace Squidex.Infrastructure.EventSourcing public MongoParallelInsertTests(MongoEventStoreReplicaSetFixture fixture) { _ = fixture; - _.Cleanup(); var typeNameRegistry = new TypeNameRegistry().Map(typeof(MyEvent), "My"); diff --git a/backend/tests/Squidex.Infrastructure.Tests/Json/Newtonsoft/ConverterContractResolverTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Json/Newtonsoft/ConverterContractResolverTests.cs deleted file mode 100644 index 9080e5a62..000000000 --- a/backend/tests/Squidex.Infrastructure.Tests/Json/Newtonsoft/ConverterContractResolverTests.cs +++ /dev/null @@ -1,104 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Newtonsoft.Json; -using NodaTime; -using NodaTime.Serialization.JsonNet; -using NodaTime.Text; -using Squidex.Infrastructure.TestHelpers; -using Xunit; - -#pragma warning disable xUnit1004 // Test methods should not be skipped - -namespace Squidex.Infrastructure.Json.Newtonsoft -{ - public class ConverterContractResolverTests - { - public class MyClass - { - [JsonConverter(typeof(TodayConverter))] - public Instant MyProperty { get; set; } - } - - public sealed class TodayConverter : JsonConverter - { - public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) - { - writer.WriteValue("TODAY"); - } - - public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) - { - throw new NotSupportedException(); - } - - public override bool CanConvert(Type objectType) - { - return objectType == typeof(Instant); - } - } - - [Fact] - public void Should_respect_property_converter() - { - var value = Instant.FromUtc(2012, 12, 10, 9, 8, 45); - - var serializerSettings = new JsonSerializerSettings - { - ContractResolver = new ConverterContractResolver(new NodaPatternConverter(InstantPattern.ExtendedIso)), - DateFormatHandling = DateFormatHandling.IsoDateFormat, - DateParseHandling = DateParseHandling.None - }; - - var serializer = new NewtonsoftJsonSerializer(serializerSettings); - - var json = serializer.Serialize(new MyClass { MyProperty = value }, false); - - Assert.Equal(@"{""myProperty"":""TODAY""}", json); - } - - [Fact(Skip = "No idea why it does not work in some cases.")] - public void Should_ignore_other_converters() - { - var value = Instant.FromUtc(2012, 12, 10, 9, 8, 45); - - var serializerSettings = new JsonSerializerSettings - { - ContractResolver = new ConverterContractResolver(new NodaPatternConverter(InstantPattern.ExtendedIso)), - DateFormatHandling = DateFormatHandling.IsoDateFormat, - DateParseHandling = DateParseHandling.None - }; - - serializerSettings.Converters.Add(new TodayConverter()); - - var serializer = new NewtonsoftJsonSerializer(serializerSettings); - var serialized = serializer.Deserialize(serializer.Serialize(value, true))!; - - Assert.Equal(value, serialized); - } - - [Fact] - public void Should_serialize_and_deserialize_instant() - { - var value = Instant.FromUtc(2012, 12, 10, 9, 8, 45); - - var serialized = value.SerializeAndDeserialize(); - - Assert.Equal(value, serialized); - } - - [Fact] - public void Should_serialize_and_deserialize_dictionary() - { - var value = new Dictionary { ["Description"] = "value" }; - - var serialized = value.SerializeAndDeserialize(); - - Assert.Equal(value, serialized); - } - } -} diff --git a/backend/tests/Squidex.Infrastructure.Tests/Json/Objects/JsonValuesSerializationTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Json/Objects/JsonValuesSerializationTests.cs index 830f34d0a..a3136d4a2 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Json/Objects/JsonValuesSerializationTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Json/Objects/JsonValuesSerializationTests.cs @@ -12,6 +12,30 @@ namespace Squidex.Infrastructure.Json.Objects { public class JsonValuesSerializationTests { + public enum SerializerMode + { + Json, + Bson + } + + private static IEnumerable Serializers() + { + yield return new object[] { SerializerMode.Json }; + yield return new object[] { SerializerMode.Bson }; + } + + private static T Serialize(T input, SerializerMode mode) + { + if (mode == SerializerMode.Bson) + { + return input.SerializeAndDeserializeBson(); + } + else + { + return input.SerializeAndDeserialize(); + } + } + [Fact] public void Should_deserialize_integer() { @@ -20,91 +44,100 @@ namespace Squidex.Infrastructure.Json.Objects Assert.Equal(JsonValue.Create(123), serialized); } - [Fact] - public void Should_serialize_and_deserialize_null() + [Theory] + [MemberData(nameof(Serializers))] + public void Should_serialize_and_deserialize_null(SerializerMode mode) { var value = JsonValue.Null; - var serialized = value.SerializeAndDeserialize(); + var serialized = Serialize(value, mode); Assert.Equal(value, serialized); } - [Fact] - public void Should_serialize_and_deserialize_date() + [Theory] + [MemberData(nameof(Serializers))] + public void Should_serialize_and_deserialize_date(SerializerMode mode) { var value = JsonValue.Create("2008-09-15T15:53:00"); - var serialized = value.SerializeAndDeserialize(); + var serialized = Serialize(value, mode); Assert.Equal(value, serialized); } - [Fact] - public void Should_serialize_and_deserialize_string() + [Theory] + [MemberData(nameof(Serializers))] + public void Should_serialize_and_deserialize_string(SerializerMode mode) { var value = JsonValue.Create("my-string"); - var serialized = value.SerializeAndDeserialize(); + var serialized = Serialize(value, mode); Assert.Equal(value, serialized); } - [Fact] - public void Should_serialize_and_deserialize_boolean() + [Theory] + [MemberData(nameof(Serializers))] + public void Should_serialize_and_deserialize_boolean(SerializerMode mode) { var value = JsonValue.Create(true); - var serialized = value.SerializeAndDeserialize(); + var serialized = Serialize(value, mode); Assert.Equal(value, serialized); } - [Fact] - public void Should_serialize_and_deserialize_number() + [Theory] + [MemberData(nameof(Serializers))] + public void Should_serialize_and_deserialize_number(SerializerMode mode) { var value = JsonValue.Create(123); - var serialized = value.SerializeAndDeserialize(); + var serialized = Serialize(value, mode); Assert.Equal(value, serialized); } - [Fact] - public void Should_serialize_and_deserialize_double_number() + [Theory] + [MemberData(nameof(Serializers))] + public void Should_serialize_and_deserialize_double_number(SerializerMode mode) { var value = JsonValue.Create(123.5); - var serialized = value.SerializeAndDeserialize(); + var serialized = Serialize(value, mode); Assert.Equal(value, serialized); } - [Fact] - public void Should_serialize_and_deserialize_array() + [Theory] + [MemberData(nameof(Serializers))] + public void Should_serialize_and_deserialize_array(SerializerMode mode) { var value = JsonValue.Array(1, 2); - var serialized = value.SerializeAndDeserialize(); + var serialized = Serialize(value, mode); Assert.Equal(value, serialized); } - [Fact] - public void Should_serialize_and_deserialize_object() + [Theory] + [MemberData(nameof(Serializers))] + public void Should_serialize_and_deserialize_object(SerializerMode mode) { var value = new JsonObject() .Add("1", 1) .Add("2", 1); - var serialized = value.SerializeAndDeserialize(); + var serialized = Serialize(value, mode); Assert.Equal(value, serialized); } - [Fact] - public void Should_serialize_and_deserialize_complex_object() + [Theory] + [MemberData(nameof(Serializers))] + public void Should_serialize_and_deserialize_complex_object(SerializerMode mode) { var value = new JsonObject() @@ -115,7 +148,7 @@ namespace Squidex.Infrastructure.Json.Objects .Add("2", new JsonObject().Add("2_1", 11)); - var serialized = value.SerializeAndDeserialize(); + var serialized = Serialize(value, mode); Assert.Equal(value, serialized); } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Json/System/JsonInheritanceConverterBaseTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Json/System/JsonInheritanceConverterBaseTests.cs new file mode 100644 index 000000000..5e53d6b16 --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/Json/System/JsonInheritanceConverterBaseTests.cs @@ -0,0 +1,103 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Infrastructure.Reflection; +using Squidex.Infrastructure.TestHelpers; +using Xunit; + +namespace Squidex.Infrastructure.Json.System +{ + public class JsonInheritanceConverterBaseTests + { + private record Base; + + private record A : Base + { + public int PropertyA { get; init; } + } + + private record B : Base + { + public int PropertyB { get; init; } + } + + private class Converter : InheritanceConverterBase + { + public Converter() + : base("$type") + { + } + + public override Type GetDiscriminatorType(string name, Type typeToConvert) + { + return name == "A" ? typeof(A) : typeof(B); + } + + public override string GetDiscriminatorValue(Type type) + { + return type == typeof(A) ? "A" : "B"; + } + } + + [Fact] + public void Should_serialize_and_deserialize() + { + var serializer = CreateSerializer(); + + Base source = new A + { + PropertyA = 42 + }; + + var serialized = serializer.Deserialize(serializer.Serialize(source)); + + Assert.Equal(new A { PropertyA = 42 }, serialized); + } + + [Fact] + public void Should_deserialize_when_discriminiator_is_first_property() + { + var serializer = CreateSerializer(); + + var source = new Dictionary + { + ["$type"] = "A", + ["propertyA"] = 42, + ["propertyOther"] = 44 + }; + + var serialized = serializer.Deserialize(serializer.Serialize(source)); + + Assert.Equal(new A { PropertyA = 42 }, serialized); + } + + [Fact] + public void Should_deserialize_when_discriminiator_is_not_first_property() + { + var serializer = CreateSerializer(); + + var source = new Dictionary + { + ["propertyB"] = 42, + ["propertyOther"] = 44, + ["$type"] = "B" + }; + + var serialized = serializer.Deserialize(serializer.Serialize(source)); + + Assert.Equal(new B { PropertyB = 42 }, serialized); + } + + private static IJsonSerializer CreateSerializer() + { + return TestUtils.CreateSerializer(options => + { + options.Converters.Add(new Converter()); + }); + } + } +} diff --git a/backend/tests/Squidex.Infrastructure.Tests/Json/System/JsonInheritanceConverterTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Json/System/JsonInheritanceConverterTests.cs new file mode 100644 index 000000000..6470f9e2f --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/Json/System/JsonInheritanceConverterTests.cs @@ -0,0 +1,90 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Infrastructure.Reflection; +using Squidex.Infrastructure.TestHelpers; +using Xunit; + +namespace Squidex.Infrastructure.Json.System +{ + public class JsonInheritanceConverterTests + { + private record Base; + + private record A : Base + { + public int PropertyA { get; init; } + } + + private record B : Base + { + public int PropertyB { get; init; } + } + + [Fact] + public void Should_serialize_and_deserialize() + { + var serializer = CreateSerializer(); + + Base source = new A + { + PropertyA = 42 + }; + + var serialized = serializer.Deserialize(serializer.Serialize(source)); + + Assert.Equal(new A { PropertyA = 42 }, serialized); + } + + [Fact] + public void Should_deserialize_when_discriminiator_is_first_property() + { + var serializer = CreateSerializer(); + + var source = new Dictionary + { + ["$type"] = "A", + ["propertyA"] = 42, + ["propertyOther"] = 44 + }; + + var serialized = serializer.Deserialize(serializer.Serialize(source)); + + Assert.Equal(new A { PropertyA = 42 }, serialized); + } + + [Fact] + public void Should_deserialize_when_discriminiator_is_not_first_property() + { + var serializer = CreateSerializer(); + + var source = new Dictionary + { + ["propertyB"] = 42, + ["propertyOther"] = 44, + ["$type"] = "B" + }; + + var serialized = serializer.Deserialize(serializer.Serialize(source)); + + Assert.Equal(new B { PropertyB = 42 }, serialized); + } + + private static IJsonSerializer CreateSerializer() + { + return TestUtils.CreateSerializer(options => + { + var typeRegistry = + new TypeNameRegistry() + .Map(typeof(A), "A") + .Map(typeof(B), "B"); + + options.Converters.Add(new InheritanceConverter(typeRegistry)); + }); + } + } +} diff --git a/backend/tests/Squidex.Infrastructure.Tests/Json/Newtonsoft/ReadOnlyCollectionTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Json/System/ReadOnlyCollectionTests.cs similarity index 55% rename from backend/tests/Squidex.Infrastructure.Tests/Json/Newtonsoft/ReadOnlyCollectionTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/Json/System/ReadOnlyCollectionTests.cs index 250c5f458..b8fb35ca8 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Json/Newtonsoft/ReadOnlyCollectionTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Json/System/ReadOnlyCollectionTests.cs @@ -5,10 +5,10 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using Newtonsoft.Json; +using Squidex.Infrastructure.TestHelpers; using Xunit; -namespace Squidex.Infrastructure.Json.Newtonsoft +namespace Squidex.Infrastructure.Json.System { public class ReadOnlyCollectionTests { @@ -18,7 +18,7 @@ namespace Squidex.Infrastructure.Json.Newtonsoft } [Fact] - public void Should_serialize_and_deserialize_dictionary_without_type_name() + public void Should_serialize_and_deserialize_dictionary() { var source = new MyClass> { @@ -29,16 +29,8 @@ namespace Squidex.Infrastructure.Json.Newtonsoft } }; - var serializerSettings = new JsonSerializerSettings - { - ContractResolver = new ConverterContractResolver() - }; - - var json = JsonConvert.SerializeObject(source, serializerSettings); - - var serialized = JsonConvert.DeserializeObject>>(json)!; + var serialized = source.SerializeAndDeserialize(); - Assert.DoesNotContain("$type", json, StringComparison.Ordinal); Assert.Equal(2, serialized.Values.Count); } @@ -54,16 +46,8 @@ namespace Squidex.Infrastructure.Json.Newtonsoft } }; - var serializerSettings = new JsonSerializerSettings - { - ContractResolver = new ConverterContractResolver() - }; - - var json = JsonConvert.SerializeObject(source, serializerSettings); - - var serialized = JsonConvert.DeserializeObject>>(json)!; + var serialized = source.SerializeAndDeserialize(); - Assert.DoesNotContain("$type", json, StringComparison.Ordinal); Assert.Equal(2, serialized.Values.Count); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/MongoDb/BsonJsonSerializerTests.cs b/backend/tests/Squidex.Infrastructure.Tests/MongoDb/BsonJsonSerializerTests.cs index 87cd7f2b4..3e6f6fd07 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/MongoDb/BsonJsonSerializerTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/MongoDb/BsonJsonSerializerTests.cs @@ -7,74 +7,59 @@ using FluentAssertions; using MongoDB.Bson.IO; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; +using MongoDB.Bson.Serialization; using Xunit; namespace Squidex.Infrastructure.MongoDb { public class BsonJsonSerializerTests { + public class TestWrapper + { + [BsonJson] + public T Value { get; set; } + } + public class TestObject { - [JsonProperty] public bool Bool { get; set; } - [JsonProperty] public byte Byte { get; set; } - [JsonProperty] public byte[] Bytes { get; set; } - [JsonProperty] public int Int32 { get; set; } - [JsonProperty] public long Int64 { get; set; } - [JsonProperty] public short Int16 { get; set; } - [JsonProperty] public uint UInt32 { get; set; } - [JsonProperty] public ulong UInt64 { get; set; } - [JsonProperty] public ushort UInt16 { get; set; } - [JsonProperty] public string String { get; set; } - [JsonProperty] public float Float32 { get; set; } - [JsonProperty] public double Float64 { get; set; } - [JsonProperty] public string[] Strings { get; set; } - [JsonProperty] public Uri Uri { get; set; } - [JsonProperty] public Guid Guid { get; set; } - [JsonProperty] public TimeSpan TimeSpan { get; set; } - [JsonProperty] public DateTime DateTime { get; set; } - [JsonProperty] public DateTimeOffset DateTimeOffset { get; set; } - [JsonProperty] public TestObject Nested { get; set; } - [JsonProperty] public TestObject[] NestedArray { get; set; } public static TestObject CreateWithValues(bool nested = true) @@ -111,30 +96,6 @@ namespace Squidex.Infrastructure.MongoDb } } - [Fact] - public void Should_write_problematic_object() - { - var source = new - { - a = new - { - iv = 1 - }, - b = new - { - iv = JObject.FromObject(new - { - lat = 1.0, - lon = 3.0 - }) - } - }; - - var deserialized = SerializeAndDeserialize(source); - - deserialized.Should().BeEquivalentTo(source); - } - [Fact] public void Should_serialize_with_reader_and_writer() { @@ -186,22 +147,18 @@ namespace Squidex.Infrastructure.MongoDb private static T SerializeAndDeserialize(T value) { - var serializer = JsonSerializer.CreateDefault(); - var stream = new MemoryStream(); - using (var writer = new BsonJsonWriter(new BsonBinaryWriter(stream))) + using (var writer = new BsonBinaryWriter(stream)) { - serializer.Serialize(writer, value); - - writer.Flush(); + BsonSerializer.Serialize(writer, new TestWrapper { Value = value }); } stream.Position = 0; - using (var reader = new BsonJsonReader(new BsonBinaryReader(stream))) + using (var reader = new BsonBinaryReader(stream)) { - return serializer.Deserialize(reader)!; + return BsonSerializer.Deserialize>(reader)!.Value; } } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/MongoDb/DomainIdSerializerTests.cs b/backend/tests/Squidex.Infrastructure.Tests/MongoDb/DomainIdSerializerTests.cs index b80df3860..ca66777eb 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/MongoDb/DomainIdSerializerTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/MongoDb/DomainIdSerializerTests.cs @@ -28,7 +28,7 @@ namespace Squidex.Infrastructure.MongoDb public DomainIdSerializerTests() { - DomainIdSerializer.Register(); + BsonDomainIdSerializer.Register(); } [Fact] diff --git a/backend/tests/Squidex.Infrastructure.Tests/MongoDb/InstantSerializerTests.cs b/backend/tests/Squidex.Infrastructure.Tests/MongoDb/InstantSerializerTests.cs index 65e3e4146..84a738da2 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/MongoDb/InstantSerializerTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/MongoDb/InstantSerializerTests.cs @@ -16,7 +16,7 @@ namespace Squidex.Infrastructure.MongoDb { public InstantSerializerTests() { - InstantSerializer.Register(); + BsonInstantSerializer.Register(); } [Fact] diff --git a/backend/tests/Squidex.Infrastructure.Tests/MongoDb/MongoQueryTests.cs b/backend/tests/Squidex.Infrastructure.Tests/MongoDb/MongoQueryTests.cs index ee8199725..42da581f0 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/MongoDb/MongoQueryTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/MongoDb/MongoQueryTests.cs @@ -35,11 +35,11 @@ namespace Squidex.Infrastructure.MongoDb static MongoQueryTests() { - DomainIdSerializer.Register(); + BsonDomainIdSerializer.Register(); - TypeConverterStringSerializer.Register(); + BsonStringSerializer.Register(); - InstantSerializer.Register(); + BsonInstantSerializer.Register(); } [Fact] diff --git a/backend/tests/Squidex.Infrastructure.Tests/MongoDb/TypeConverterStringSerializerTests.cs b/backend/tests/Squidex.Infrastructure.Tests/MongoDb/TypeConverterStringSerializerTests.cs index 6a2e36503..8ce8494bf 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/MongoDb/TypeConverterStringSerializerTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/MongoDb/TypeConverterStringSerializerTests.cs @@ -20,8 +20,8 @@ namespace Squidex.Infrastructure.MongoDb public TypeConverterStringSerializerTests() { - TypeConverterStringSerializer.Register(); - TypeConverterStringSerializer.Register(); + BsonStringSerializer.Register(); + BsonStringSerializer.Register(); } [Fact] diff --git a/backend/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj b/backend/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj index 5bdf32958..9d33737f0 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj +++ b/backend/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj @@ -23,7 +23,7 @@ - + diff --git a/backend/tests/Squidex.Infrastructure.Tests/StringExtensionsTests.cs b/backend/tests/Squidex.Infrastructure.Tests/StringExtensionsTests.cs index 3f135af5e..1f5eee93c 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/StringExtensionsTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/StringExtensionsTests.cs @@ -93,5 +93,13 @@ namespace Squidex.Infrastructure Assert.Equal("1_2", result); } + + [Fact] + public void Should_escape_json() + { + var result = StringExtensions.JsonEscape("Hello \"World\""); + + Assert.Equal("Hello \\\"World\\\"", result); + } } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/BinaryFormatterHelper.cs b/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/BinaryFormatterHelper.cs deleted file mode 100644 index 35d2a0daf..000000000 --- a/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/BinaryFormatterHelper.cs +++ /dev/null @@ -1,30 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Runtime.Serialization.Formatters.Binary; - -#pragma warning disable SYSLIB0011 // Type or member is obsolete - -namespace Squidex.Infrastructure.TestHelpers -{ - public static class BinaryFormatterHelper - { - private static readonly BinaryFormatter Formatter = new BinaryFormatter(); - - public static T SerializeAndDeserializeBinary(this T source) - { - using (var stream = new MemoryStream()) - { - Formatter.Serialize(stream, source!); - - stream.Position = 0; - - return (T)Formatter.Deserialize(stream); - } - } - } -} diff --git a/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/TestUtils.cs b/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/TestUtils.cs index 0147cca5c..703b1ad78 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/TestUtils.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/TestUtils.cs @@ -5,17 +5,23 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Runtime.Serialization.Formatters.Binary; using System.Security.Claims; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; +using System.Text.Json; +using System.Text.Json.Serialization; +using MongoDB.Bson.IO; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Attributes; using NodaTime; -using NodaTime.Serialization.JsonNet; +using NodaTime.Serialization.SystemTextJson; using Squidex.Infrastructure.Json; -using Squidex.Infrastructure.Json.Newtonsoft; using Squidex.Infrastructure.Json.Objects; +using Squidex.Infrastructure.Json.System; +using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.Queries; using Squidex.Infrastructure.Queries.Json; -using Squidex.Infrastructure.Reflection; + +#pragma warning disable SYSLIB0011 // Type or member is obsolete namespace Squidex.Infrastructure.TestHelpers { @@ -23,45 +29,124 @@ namespace Squidex.Infrastructure.TestHelpers { public static readonly IJsonSerializer DefaultSerializer = CreateSerializer(); - public static IJsonSerializer CreateSerializer(TypeNameRegistry? typeNameRegistry = null) + public sealed class ObjectHolder + { + [BsonRequired] + public T Value1 { get; set; } + + [BsonRequired] + public T Value2 { get; set; } + } + + static TestUtils() + { + SetupBson(); + } + + public static void SetupBson() + { + BsonDomainIdSerializer.Register(); + BsonInstantSerializer.Register(); + BsonJsonConvention.Register(DefaultOptions()); + BsonJsonValueSerializer.Register(); + } + + public static IJsonSerializer CreateSerializer(Action? configure = null) { - var serializerSettings = DefaultSettings(typeNameRegistry); + var serializerSettings = DefaultOptions(configure); - return new NewtonsoftJsonSerializer(serializerSettings); + return new SystemJsonSerializer(serializerSettings); } - public static JsonSerializerSettings DefaultSettings(TypeNameRegistry? typeNameRegistry = null) + public static JsonSerializerOptions DefaultOptions(Action? configure = null) { - return new JsonSerializerSettings + var options = new JsonSerializerOptions(JsonSerializerDefaults.Web); + + options.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb); + // It is also a readonly list, so we have to register it first, so that other converters do not pick this up. + options.Converters.Add(new StringConverter(x => x)); + options.Converters.Add(new JsonValueConverter()); + options.Converters.Add(new ReadonlyDictionaryConverterFactory()); + options.Converters.Add(new ReadonlyListConverterFactory()); + options.Converters.Add(new SurrogateJsonConverter()); + options.Converters.Add(new SurrogateJsonConverter, JsonFilterSurrogate>()); + options.Converters.Add(new StringConverter()); + options.Converters.Add(new StringConverter()); + options.Converters.Add(new StringConverter>()); + options.Converters.Add(new StringConverter>()); + options.Converters.Add(new StringConverter>()); + options.Converters.Add(new StringConverter()); + options.Converters.Add(new StringConverter()); + options.Converters.Add(new JsonStringEnumConverter()); + configure?.Invoke(options); + + return options; + } + + public static T SerializeAndDeserializeBinary(this T source) + { + using (var stream = new MemoryStream()) { - SerializationBinder = new TypeNameSerializationBinder(typeNameRegistry ?? new TypeNameRegistry()), + var formatter = new BinaryFormatter(); - ContractResolver = new ConverterContractResolver( - new JsonValueConverter(), - new StringEnumConverter(), - new SurrogateConverter(), - new SurrogateConverter, JsonFilterSurrogate>(), - new TypeConverterJsonConverter()), + formatter.Serialize(stream, source!); - TypeNameHandling = TypeNameHandling.Auto - }.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb); + stream.Position = 0; + + return (T)formatter.Deserialize(stream); + } + } + + public static T SerializeAndDeserializeBson(this T value) + { + using var stream = new MemoryStream(); + + using (var writer = new BsonBinaryWriter(stream)) + { + BsonSerializer.Serialize(writer, new ObjectHolder { Value1 = value, Value2 = value }); + } + + stream.Position = 0; + + using (var reader = new BsonBinaryReader(stream)) + { + return BsonSerializer.Deserialize>(reader).Value1; + } + } + + public static T SerializeAndDeserialize(this object value) + { + var json = DefaultSerializer.Serialize(value); + + return DefaultSerializer.Deserialize(json); } public static T SerializeAndDeserialize(this T value) { - var json = DefaultSerializer.Serialize(Tuple.Create(value)); + var json = DefaultSerializer.Serialize(new ObjectHolder { Value1 = value, Value2 = value }); - return DefaultSerializer.Deserialize>(json).Item1; + return DefaultSerializer.Deserialize>(json).Value1; } public static T Deserialize(string value) { - return DefaultSerializer.Deserialize>($"{{ \"Item1\": \"{value}\" }}").Item1; + var json = DefaultSerializer.Serialize(new ObjectHolder { Value1 = value, Value2 = value }); + + return DefaultSerializer.Deserialize>(json).Value1; } public static T Deserialize(object value) { - return DefaultSerializer.Deserialize>($"{{ \"Item1\": {value} }}").Item1; + var json = DefaultSerializer.Serialize(new ObjectHolder { Value1 = value, Value2 = value }); + + return DefaultSerializer.Deserialize>(json).Value1; + } + + public static string CleanJson(this string json) + { + using var document = JsonDocument.Parse(json); + + return DefaultSerializer.Serialize(document, true); } } } diff --git a/frontend/src/app/features/apps/pages/apps-page.component.ts b/frontend/src/app/features/apps/pages/apps-page.component.ts index d623248b3..fa1102d4c 100644 --- a/frontend/src/app/features/apps/pages/apps-page.component.ts +++ b/frontend/src/app/features/apps/pages/apps-page.component.ts @@ -42,7 +42,7 @@ export class AppsPageComponent implements OnInit { private readonly uiOptions: UIOptions, ) { if (uiOptions.get('showInfo')) { - this.info = uiOptions.get('more.info'); + this.info = uiOptions.get('info'); } } diff --git a/frontend/src/app/framework/angular/forms/editors/localized-input.component.html b/frontend/src/app/framework/angular/forms/editors/localized-input.component.html index c6a30b808..04a29d604 100644 --- a/frontend/src/app/framework/angular/forms/editors/localized-input.component.html +++ b/frontend/src/app/framework/angular/forms/editors/localized-input.component.html @@ -52,7 +52,8 @@ + (blur)="callTouched()" + sqxIndeterminateValue> diff --git a/frontend/src/app/shared/components/notifo.component.ts b/frontend/src/app/shared/components/notifo.component.ts index a666e253c..6911bbba6 100644 --- a/frontend/src/app/shared/components/notifo.component.ts +++ b/frontend/src/app/shared/components/notifo.component.ts @@ -37,7 +37,7 @@ export class NotifoComponent implements AfterViewInit, OnChanges, OnDestroy { private readonly renderer: Renderer2, ) { this.notifoApiKey = authService.user?.notifoToken; - this.notifoApiUrl = uiOptions.get('more.notifoApi'); + this.notifoApiUrl = uiOptions.get('notifoApi'); if (this.isConfigured) { if (this.notifoApiUrl.indexOf('localhost:5002') >= 0) { diff --git a/frontend/src/app/shell/pages/internal/notifications-menu.component.ts b/frontend/src/app/shell/pages/internal/notifications-menu.component.ts index cebce5f04..20127c0d9 100644 --- a/frontend/src/app/shell/pages/internal/notifications-menu.component.ts +++ b/frontend/src/app/shell/pages/internal/notifications-menu.component.ts @@ -20,7 +20,7 @@ export class NotificationsMenuComponent { constructor(authService: AuthService, uiOptions: UIOptions, ) { const notifoApiKey = authService.user?.notifoToken; - const notifoApiUrl = uiOptions.get('more.notifoApi'); + const notifoApiUrl = uiOptions.get('notifoApi'); this.isNotifoConfigured = !!notifoApiKey && !!notifoApiUrl; } diff --git a/frontend/src/app/shell/pages/internal/profile-menu.component.ts b/frontend/src/app/shell/pages/internal/profile-menu.component.ts index 75be92edf..e59708c79 100644 --- a/frontend/src/app/shell/pages/internal/profile-menu.component.ts +++ b/frontend/src/app/shell/pages/internal/profile-menu.component.ts @@ -34,7 +34,7 @@ interface State { export class ProfileMenuComponent extends StatefulComponent implements OnInit { public modalMenu = new ModalModel(); - public language = this.uiOptions.get('more.culture'); + public language = this.uiOptions.get('culture'); public languages = UILanguages.ALL; constructor(changeDetector: ChangeDetectorRef, apiUrl: ApiUrlConfig,