Browse Source

System text json (#902)

* Started conversion.

* Made progress.

* More fixes.

* Fix tests

* Fix null writing.

* More fixes

* Fix rule formatting.
pull/908/head
Sebastian Stehle 3 years ago
committed by GitHub
parent
commit
c4db9a8c63
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 45
      backend/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaActionHandler.cs
  2. 31
      backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchActionHandler.cs
  3. 1
      backend/extensions/Squidex.Extensions/Squidex.Extensions.csproj
  4. 6
      backend/extensions/Squidex.Extensions/Text/Azure/CommandFactory.cs
  5. 6
      backend/extensions/Squidex.Extensions/Text/ElasticSearch/CommandFactory.cs
  6. 8
      backend/extensions/Squidex.Extensions/Text/ElasticSearch/ElasticSearchTextIndex.cs
  7. 3
      backend/extensions/Squidex.Extensions/Text/ElasticSearch/ElasticSearchTextPlugin.cs
  8. 17
      backend/src/Migrations/Migrations/ConvertEventStore.cs
  9. 23
      backend/src/Migrations/Migrations/ConvertEventStoreAppId.cs
  10. 12
      backend/src/Migrations/Migrations/MongoDb/AddAppIdToEventStream.cs
  11. 9
      backend/src/Migrations/Migrations/MongoDb/ConvertDocumentIds.cs
  12. 15
      backend/src/Migrations/Migrations/MongoDb/ConvertOldSnapshotStores.cs
  13. 7
      backend/src/Migrations/Migrations/MongoDb/ConvertRuleEventsJson.cs
  14. 70
      backend/src/Migrations/Migrations/MongoDb/RenameAssetMetadata.cs
  15. 6
      backend/src/Migrations/Migrations/MongoDb/RenameAssetSlugField.cs
  16. 5
      backend/src/Migrations/Migrations/MongoDb/RestructureContentCollection.cs
  17. 11
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/GeoJsonValue.cs
  18. 48
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/ContentFieldDataConverter.cs
  19. 6
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/WorkflowStepSurrogate.cs
  20. 6
      backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedCommentEvent.cs
  21. 4
      backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedUserEventBase.cs
  22. 1
      backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj
  23. 20
      backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs
  24. 8
      backend/src/Squidex.Domain.Apps.Core.Operations/Templates/Extensions/StringFluidExtension.cs
  25. 5
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs
  26. 1
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs
  27. 2
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderRepository_SnapshotStore.cs
  28. 2
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs
  29. 2
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs
  30. 2
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs
  31. 2
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs
  32. 2
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/Adapt.cs
  33. 4
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/MongoCountCollection.cs
  34. 5
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs
  35. 5
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository.cs
  36. 4
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemasHash.cs
  37. 4
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/AtlasTextIndex.cs
  38. 8
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/CommandFactory.cs
  39. 4
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/MongoTextIndex.cs
  40. 25
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/MongoTextIndexBase.cs
  41. 4
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/MongoTextIndexEntity.cs
  42. 4
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/MongoTextIndexerState.cs
  43. 2
      backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AppCommand.cs
  44. 2
      backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AppUpdateCommand.cs
  45. 2
      backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/CreateApp.cs
  46. 4
      backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppDomainObject.State.cs
  47. 2
      backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/UsageNotifierWorker.cs
  48. 2
      backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/AssetCommand.cs
  49. 2
      backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/AssetFolderCommand.cs
  50. 6
      backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetDomainObject.State.cs
  51. 4
      backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetFolderDomainObject.State.cs
  52. 66
      backend/src/Squidex.Domain.Apps.Entities/Backup/Model/CompatibleStoredEvent.cs
  53. 2
      backend/src/Squidex.Domain.Apps.Entities/Backup/State/BackupState.cs
  54. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentCommand.cs
  55. 12
      backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.State.cs
  56. 8
      backend/src/Squidex.Domain.Apps.Entities/Contents/Text/Extensions.cs
  57. 4
      backend/src/Squidex.Domain.Apps.Entities/Contents/Text/UpsertIndexEntry.cs
  58. 4
      backend/src/Squidex.Domain.Apps.Entities/Rules/DomainObject/RuleDomainObject.State.cs
  59. 1
      backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/CreateSchema.cs
  60. 1
      backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/SchemaUpdateCommand.cs
  61. 4
      backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/SchemaDomainObject.State.cs
  62. 4
      backend/src/Squidex.Infrastructure.MongoDb/Caching/MongoDistributedCache.cs
  63. 2
      backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEvent.cs
  64. 4
      backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs
  65. 20
      backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Reader.cs
  66. 8
      backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Writer.cs
  67. 6
      backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Batching.cs
  68. 6
      backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonDomainIdSerializer.cs
  69. 26
      backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonInstantSerializer.cs
  70. 13
      backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConvention.cs
  71. 105
      backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonReader.cs
  72. 168
      backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonSerializer.cs
  73. 91
      backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonValueSerializer.cs
  74. 190
      backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonWriter.cs
  75. 4
      backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonStringSerializer.cs
  76. 31
      backend/src/Squidex.Infrastructure.MongoDb/MongoDb/FieldDefinitionBuilder.cs
  77. 20
      backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoBase.cs
  78. 28
      backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs
  79. 5
      backend/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs
  80. 16
      backend/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStoreBase.cs
  81. 2
      backend/src/Squidex.Infrastructure/Collections/ReadonlyDictionary.cs
  82. 2
      backend/src/Squidex.Infrastructure/EventSourcing/DefaultEventFormatter.cs
  83. 6
      backend/src/Squidex.Infrastructure/Json/IJsonSerializer.cs
  84. 98
      backend/src/Squidex.Infrastructure/Json/Newtonsoft/ConverterContractResolver.cs
  85. 49
      backend/src/Squidex.Infrastructure/Json/Newtonsoft/JsonClassConverter.cs
  86. 207
      backend/src/Squidex.Infrastructure/Json/Newtonsoft/JsonValueConverter.cs
  87. 98
      backend/src/Squidex.Infrastructure/Json/Newtonsoft/NewtonsoftJsonSerializer.cs
  88. 30
      backend/src/Squidex.Infrastructure/Json/Newtonsoft/SurrogateConverter.cs
  89. 50
      backend/src/Squidex.Infrastructure/Json/Newtonsoft/TypeConverterJsonConverter.cs
  90. 45
      backend/src/Squidex.Infrastructure/Json/Newtonsoft/TypeNameSerializationBinder.cs
  91. 16
      backend/src/Squidex.Infrastructure/Json/Newtonsoft/WriteonlyGeoJsonConverter.cs
  92. 54
      backend/src/Squidex.Infrastructure/Json/System/InheritanceConverter.cs
  93. 95
      backend/src/Squidex.Infrastructure/Json/System/InheritanceConverterBase.cs
  94. 72
      backend/src/Squidex.Infrastructure/Json/System/JsonValueConverter.cs
  95. 69
      backend/src/Squidex.Infrastructure/Json/System/ReadonlyDictionaryConverterFactory.cs
  96. 65
      backend/src/Squidex.Infrastructure/Json/System/ReadonlyListConverterFactory.cs
  97. 46
      backend/src/Squidex.Infrastructure/Json/System/ReflectionHelper.cs
  98. 62
      backend/src/Squidex.Infrastructure/Json/System/StringConverter.cs
  99. 31
      backend/src/Squidex.Infrastructure/Json/System/SurrogateJsonConverter.cs
  100. 108
      backend/src/Squidex.Infrastructure/Json/System/SystemJsonSerializer.cs

45
backend/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaActionHandler.cs

@ -5,12 +5,13 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Text.Json.Serialization;
using Algolia.Search.Clients; using Algolia.Search.Clients;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Infrastructure.Json;
#pragma warning disable IDE0059 // Value assigned to symbol is never used #pragma warning disable IDE0059 // Value assigned to symbol is never used
#pragma warning disable MA0048 // File name must match type name #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 ClientPool<(string AppId, string ApiKey, string IndexName), ISearchIndex> clients;
private readonly IScriptEngine scriptEngine; private readonly IScriptEngine scriptEngine;
private readonly IJsonSerializer serializer;
public AlgoliaActionHandler(RuleEventFormatter formatter, IScriptEngine scriptEngine) public AlgoliaActionHandler(RuleEventFormatter formatter, IScriptEngine scriptEngine, IJsonSerializer serializer)
: base(formatter) : base(formatter)
{ {
clients = new ClientPool<(string AppId, string ApiKey, string IndexName), ISearchIndex>(key => clients = new ClientPool<(string AppId, string ApiKey, string IndexName), ISearchIndex>(key =>
@ -33,6 +35,7 @@ namespace Squidex.Extensions.Actions.Algolia
}); });
this.scriptEngine = scriptEngine; this.scriptEngine = scriptEngine;
this.serializer = serializer;
} }
protected override async Task<(string Description, AlgoliaJob Data)> CreateJobAsync(EnrichedEvent @event, AlgoliaAction action) 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 ruleDescription = string.Empty;
var contentId = entityEvent.Id.ToString(); var contentId = entityEvent.Id.ToString();
var content = (JObject)null; var content = (AlgoliaContent)null;
if (delete) if (delete)
{ {
@ -67,21 +70,27 @@ namespace Squidex.Extensions.Actions.Algolia
jsonString = ToJson(@event); jsonString = ToJson(@event);
} }
content = JObject.Parse(jsonString); content = serializer.Deserialize<AlgoliaContent>(jsonString);
} }
catch (Exception ex) catch (Exception ex)
{ {
content = new JObject(new JProperty("error", $"Invalid JSON: {ex.Message}")); content = new AlgoliaContent
{
More = new Dictionary<string, object>
{
["error"] = $"Invalid JSON: {ex.Message}"
}
};
} }
content["objectID"] = contentId; content.ObjectID = contentId;
} }
var ruleJob = new AlgoliaJob var ruleJob = new AlgoliaJob
{ {
AppId = action.AppId, AppId = action.AppId,
ApiKey = action.ApiKey, ApiKey = action.ApiKey,
Content = content, Content = serializer.Serialize(content, true),
ContentId = contentId, ContentId = contentId,
IndexName = await FormatAsync(action.IndexName, @event) IndexName = await FormatAsync(action.IndexName, @event)
}; };
@ -106,15 +115,20 @@ namespace Squidex.Extensions.Actions.Algolia
{ {
if (job.Content != null) 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 else
{ {
var response = await index.DeleteObjectAsync(job.ContentId, null, ct); 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) 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<string, object> More { get; set; } = new Dictionary<string, object>();
}
public sealed class AlgoliaJob public sealed class AlgoliaJob
{ {
public string AppId { get; set; } public string AppId { get; set; }
@ -134,6 +157,6 @@ namespace Squidex.Extensions.Actions.Algolia
public string IndexName { get; set; } public string IndexName { get; set; }
public JObject Content { get; set; } public string Content { get; set; }
} }
} }

31
backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchActionHandler.cs

@ -5,12 +5,13 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Text.Json.Serialization;
using Elasticsearch.Net; using Elasticsearch.Net;
using Newtonsoft.Json.Linq;
using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Json;
#pragma warning disable IDE0059 // Value assigned to symbol is never used #pragma warning disable IDE0059 // Value assigned to symbol is never used
#pragma warning disable MA0048 // File name must match type name #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 ClientPool<(Uri Host, string Username, string Password), ElasticLowLevelClient> clients;
private readonly IScriptEngine scriptEngine; private readonly IScriptEngine scriptEngine;
private readonly IJsonSerializer serializer;
public ElasticSearchActionHandler(RuleEventFormatter formatter, IScriptEngine scriptEngine) public ElasticSearchActionHandler(RuleEventFormatter formatter, IScriptEngine scriptEngine, IJsonSerializer serializer)
: base(formatter) : base(formatter)
{ {
clients = new ClientPool<(Uri Host, string Username, string Password), ElasticLowLevelClient>(key => clients = new ClientPool<(Uri Host, string Username, string Password), ElasticLowLevelClient>(key =>
@ -38,6 +40,7 @@ namespace Squidex.Extensions.Actions.ElasticSearch
}); });
this.scriptEngine = scriptEngine; this.scriptEngine = scriptEngine;
this.serializer = serializer;
} }
protected override async Task<(string Description, ElasticSearchJob Data)> CreateJobAsync(EnrichedEvent @event, ElasticSearchAction action) 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}"; ruleDescription = $"Upsert to index: {action.IndexName}";
JObject json; ElasticContent content;
try try
{ {
string jsonString; string jsonString;
@ -88,16 +91,22 @@ namespace Squidex.Extensions.Actions.ElasticSearch
jsonString = ToJson(@event); jsonString = ToJson(@event);
} }
json = JObject.Parse(jsonString); content = serializer.Deserialize<ElasticContent>(jsonString);
} }
catch (Exception ex) catch (Exception ex)
{ {
json = new JObject(new JProperty("error", $"Invalid JSON: {ex.Message}")); content = new ElasticContent
{
More = new Dictionary<string, object>
{
["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); return (ruleDescription, ruleJob);
@ -135,6 +144,14 @@ namespace Squidex.Extensions.Actions.ElasticSearch
} }
} }
public sealed class ElasticContent
{
public string ContentId { get; set; }
[JsonExtensionData]
public Dictionary<string, object> More { get; set; } = new Dictionary<string, object>();
}
public sealed class ElasticSearchJob public sealed class ElasticSearchJob
{ {
public string ServerHost { get; set; } public string ServerHost { get; set; }

1
backend/extensions/Squidex.Extensions/Squidex.Extensions.csproj

@ -28,7 +28,6 @@
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0" />
<PackageReference Include="Microsoft.OData.Core" Version="7.11.0" /> <PackageReference Include="Microsoft.OData.Core" Version="7.11.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="NodaTime" Version="3.1.0" /> <PackageReference Include="NodaTime" Version="3.1.0" />
<PackageReference Include="OpenTelemetry.Exporter.Console" Version="1.3.0" /> <PackageReference Include="OpenTelemetry.Exporter.Console" Version="1.3.0" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.3.0" /> <PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.3.0" />

6
backend/extensions/Squidex.Extensions/Text/Azure/CommandFactory.cs

@ -7,7 +7,7 @@
using System.Text; using System.Text;
using Azure.Search.Documents.Models; using Azure.Search.Documents.Models;
using GeoJSON.Net.Geometry; using NetTopologySuite.Geometries;
using Squidex.Domain.Apps.Entities.Contents.Text; using Squidex.Domain.Apps.Entities.Contents.Text;
namespace Squidex.Extensions.Text.Azure namespace Squidex.Extensions.Text.Azure
@ -47,8 +47,8 @@ namespace Squidex.Extensions.Text.Azure
type = "Point", type = "Point",
coordinates = new[] coordinates = new[]
{ {
point.Coordinates.Longitude, point.Coordinate.X,
point.Coordinates.Latitude point.Coordinate.Y
} }
}; };
break; break;

6
backend/extensions/Squidex.Extensions/Text/ElasticSearch/CommandFactory.cs

@ -5,7 +5,7 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using GeoJSON.Net.Geometry; using NetTopologySuite.Geometries;
using Squidex.Domain.Apps.Entities.Contents.Text; using Squidex.Domain.Apps.Entities.Contents.Text;
namespace Squidex.Extensions.Text.ElasticSearch namespace Squidex.Extensions.Text.ElasticSearch
@ -42,8 +42,8 @@ namespace Squidex.Extensions.Text.ElasticSearch
geoField = key; geoField = key;
geoObject = new geoObject = new
{ {
lat = point.Coordinates.Latitude, lat = point.Coordinate.X,
lon = point.Coordinates.Longitude lon = point.Coordinate.Y
}; };
break; break;
} }

8
backend/extensions/Squidex.Extensions/Text/ElasticSearch/ElasticSearchTextIndex.cs

@ -7,12 +7,12 @@
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Elasticsearch.Net; using Elasticsearch.Net;
using Newtonsoft.Json;
using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Contents; using Squidex.Domain.Apps.Entities.Contents;
using Squidex.Domain.Apps.Entities.Contents.Text; using Squidex.Domain.Apps.Entities.Contents.Text;
using Squidex.Hosting; using Squidex.Hosting;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Json;
namespace Squidex.Extensions.Text.ElasticSearch namespace Squidex.Extensions.Text.ElasticSearch
{ {
@ -23,14 +23,16 @@ namespace Squidex.Extensions.Text.ElasticSearch
private readonly ElasticLowLevelClient client; private readonly ElasticLowLevelClient client;
private readonly QueryParser queryParser = new QueryParser(ElasticSearchIndexDefinition.GetFieldPath); private readonly QueryParser queryParser = new QueryParser(ElasticSearchIndexDefinition.GetFieldPath);
private readonly string indexName; 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)); var config = new ConnectionConfiguration(new Uri(configurationString));
client = new ElasticLowLevelClient(config); client = new ElasticLowLevelClient(config);
this.indexName = indexName; this.indexName = indexName;
this.jsonSerializer = jsonSerializer;
} }
public Task InitializeAsync( public Task InitializeAsync(
@ -210,7 +212,7 @@ namespace Squidex.Extensions.Text.ElasticSearch
elasticQuery.query.@bool.should.Add(bySchema); elasticQuery.query.@bool.should.Add(bySchema);
} }
var json = JsonConvert.SerializeObject(elasticQuery, Formatting.Indented); var json = jsonSerializer.Serialize(elasticQuery, true);
return await SearchAsync(elasticQuery, ct); return await SearchAsync(elasticQuery, ct);
} }

3
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.Domain.Apps.Entities.Contents.Text;
using Squidex.Hosting; using Squidex.Hosting;
using Squidex.Hosting.Configuration; using Squidex.Hosting.Configuration;
using Squidex.Infrastructure.Json;
using Squidex.Infrastructure.Plugins; using Squidex.Infrastructure.Plugins;
namespace Squidex.Extensions.Text.ElasticSearch namespace Squidex.Extensions.Text.ElasticSearch
@ -39,7 +40,7 @@ namespace Squidex.Extensions.Text.ElasticSearch
} }
services.AddSingleton( services.AddSingleton(
c => new ElasticSearchTextIndex(elasticConfiguration, indexName)); c => new ElasticSearchTextIndex(elasticConfiguration, indexName, c.GetRequiredService<IJsonSerializer>()));
services.AddSingleton<ITextIndex>( services.AddSingleton<ITextIndex>(
c => c.GetRequiredService<ElasticSearchTextIndex>()); c => c.GetRequiredService<ElasticSearchTextIndex>());

17
backend/src/Migrations/Migrations/ConvertEventStore.cs

@ -9,10 +9,11 @@ using MongoDB.Bson;
using MongoDB.Driver; using MongoDB.Driver;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Migrations; using Squidex.Infrastructure.Migrations;
using Squidex.Infrastructure.MongoDb;
namespace Migrations.Migrations namespace Migrations.Migrations
{ {
public sealed class ConvertEventStore : IMigration public sealed class ConvertEventStore : MongoBase<BsonDocument>, IMigration
{ {
private readonly IEventStore eventStore; private readonly IEventStore eventStore;
@ -26,15 +27,10 @@ namespace Migrations.Migrations
{ {
if (eventStore is MongoEventStore mongoEventStore) 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 collection = mongoEventStore.RawCollection;
var filter = Builders<BsonDocument>.Filter;
var writes = new List<WriteModel<BsonDocument>>(); var writes = new List<WriteModel<BsonDocument>>();
var writeOptions = new BulkWriteOptions
{
IsOrdered = false
};
async Task WriteAsync(WriteModel<BsonDocument>? model, bool force) async Task WriteAsync(WriteModel<BsonDocument>? model, bool force)
{ {
@ -45,13 +41,12 @@ namespace Migrations.Migrations
if (writes.Count == 1000 || (force && writes.Count > 0)) if (writes.Count == 1000 || (force && writes.Count > 0))
{ {
await collection.BulkWriteAsync(writes, writeOptions, ct); await collection.BulkWriteAsync(writes, BulkUnordered, ct);
writes.Clear(); writes.Clear();
} }
} }
await collection.Find(new BsonDocument()).ForEachAsync(async commit => await collection.Find(FindAll).ForEachAsync(async commit =>
{ {
foreach (BsonDocument @event in commit["Events"].AsBsonArray) foreach (BsonDocument @event in commit["Events"].AsBsonArray)
{ {
@ -61,7 +56,7 @@ namespace Migrations.Migrations
@event["Metadata"] = meta; @event["Metadata"] = meta;
} }
await WriteAsync(new ReplaceOneModel<BsonDocument>(filter.Eq("_id", commit["_id"].AsString), commit), false); await WriteAsync(new ReplaceOneModel<BsonDocument>(Filter.Eq("_id", commit["_id"].AsString), commit), false);
}, ct); }, ct);
await WriteAsync(null, true); await WriteAsync(null, true);

23
backend/src/Migrations/Migrations/ConvertEventStoreAppId.cs

@ -10,10 +10,11 @@ using MongoDB.Driver;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Migrations; using Squidex.Infrastructure.Migrations;
using Squidex.Infrastructure.MongoDb;
namespace Migrations.Migrations namespace Migrations.Migrations
{ {
public sealed class ConvertEventStoreAppId : IMigration public sealed class ConvertEventStoreAppId : MongoBase<BsonDocument>, IMigration
{ {
private readonly IEventStore eventStore; private readonly IEventStore eventStore;
@ -27,17 +28,10 @@ namespace Migrations.Migrations
{ {
if (eventStore is MongoEventStore mongoEventStore) 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 collection = mongoEventStore.RawCollection;
var filter = Builders<BsonDocument>.Filter;
var updates = Builders<BsonDocument>.Update;
var writes = new List<WriteModel<BsonDocument>>(); var writes = new List<WriteModel<BsonDocument>>();
var writeOptions = new BulkWriteOptions
{
IsOrdered = false
};
async Task WriteAsync(WriteModel<BsonDocument>? model, bool force) async Task WriteAsync(WriteModel<BsonDocument>? model, bool force)
{ {
@ -48,13 +42,12 @@ namespace Migrations.Migrations
if (writes.Count == 1000 || (force && writes.Count > 0)) if (writes.Count == 1000 || (force && writes.Count > 0))
{ {
await collection.BulkWriteAsync(writes, writeOptions, ct); await collection.BulkWriteAsync(writes, BulkUnordered, ct);
writes.Clear(); writes.Clear();
} }
} }
await collection.Find(new BsonDocument()).ForEachAsync(async commit => await collection.Find(FindAll).ForEachAsync(async commit =>
{ {
UpdateDefinition<BsonDocument>? update = null; UpdateDefinition<BsonDocument>? update = null;
@ -68,11 +61,11 @@ namespace Migrations.Migrations
{ {
var appId = NamedId<Guid>.Parse(appIdValue.AsString, Guid.TryParse).Id.ToString(); var appId = NamedId<Guid>.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) if (update != null)
{ {
update = updates.Combine(update, eventUpdate); update = Update.Combine(update, eventUpdate);
} }
else else
{ {
@ -85,7 +78,7 @@ namespace Migrations.Migrations
if (update != null) if (update != null)
{ {
var write = new UpdateOneModel<BsonDocument>(filter.Eq("_id", commit["_id"].AsString), update); var write = new UpdateOneModel<BsonDocument>(Filter.Eq("_id", commit["_id"].AsString), update);
await WriteAsync(write, false); await WriteAsync(write, false);
} }

12
backend/src/Migrations/Migrations/MongoDb/AddAppIdToEventStream.cs

@ -16,7 +16,7 @@ using Squidex.Infrastructure.Tasks;
namespace Migrations.Migrations.MongoDb namespace Migrations.Migrations.MongoDb
{ {
public sealed class AddAppIdToEventStream : IMigration public sealed class AddAppIdToEventStream : MongoBase<BsonDocument>, IMigration
{ {
private readonly IMongoDatabase database; private readonly IMongoDatabase database;
@ -31,6 +31,7 @@ namespace Migrations.Migrations.MongoDb
const int SizeOfBatch = 1000; const int SizeOfBatch = 1000;
const int SizeOfQueue = 20; const int SizeOfQueue = 20;
// Do not resolve in constructor, because most of the time it is not executed anyway.
var collectionV1 = database.GetCollection<BsonDocument>("Events"); var collectionV1 = database.GetCollection<BsonDocument>("Events");
var collectionV2 = database.GetCollection<BsonDocument>("Events2"); var collectionV2 = database.GetCollection<BsonDocument>("Events2");
@ -39,11 +40,6 @@ namespace Migrations.Migrations.MongoDb
BoundedCapacity = SizeOfQueue * SizeOfBatch BoundedCapacity = SizeOfQueue * SizeOfBatch
}); });
var writeOptions = new BulkWriteOptions
{
IsOrdered = false
};
var actionBlock = new ActionBlock<BsonDocument[]>(async batch => var actionBlock = new ActionBlock<BsonDocument[]>(async batch =>
{ {
try try
@ -102,7 +98,7 @@ namespace Migrations.Migrations.MongoDb
if (writes.Count > 0) if (writes.Count > 0)
{ {
await collectionV2.BulkWriteAsync(writes, writeOptions, ct); await collectionV2.BulkWriteAsync(writes, BulkUnordered, ct);
} }
} }
catch (OperationCanceledException ex) catch (OperationCanceledException ex)
@ -119,7 +115,7 @@ namespace Migrations.Migrations.MongoDb
batchBlock.BidirectionalLinkTo(actionBlock); 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)) if (!await batchBlock.SendAsync(commit, ct))
{ {

9
backend/src/Migrations/Migrations/MongoDb/ConvertDocumentIds.cs

@ -15,7 +15,7 @@ using Squidex.Infrastructure.Tasks;
namespace Migrations.Migrations.MongoDb namespace Migrations.Migrations.MongoDb
{ {
public sealed class ConvertDocumentIds : IMigration public sealed class ConvertDocumentIds : MongoBase<BsonDocument>, IMigration
{ {
private readonly IMongoDatabase database; private readonly IMongoDatabase database;
private readonly IMongoDatabase databaseContent; private readonly IMongoDatabase databaseContent;
@ -80,6 +80,7 @@ namespace Migrations.Migrations.MongoDb
collectionNameV2 = $"{collectionNameV1}2"; collectionNameV2 = $"{collectionNameV1}2";
collectionNameV2 = collectionNameV2.Replace("State_", "States_", StringComparison.Ordinal); 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<BsonDocument>(collectionNameV1); var collectionV1 = database.GetCollection<BsonDocument>(collectionNameV1);
var collectionV2 = database.GetCollection<BsonDocument>(collectionNameV2); var collectionV2 = database.GetCollection<BsonDocument>(collectionNameV2);
@ -88,7 +89,7 @@ namespace Migrations.Migrations.MongoDb
return; return;
} }
await collectionV2.DeleteManyAsync(new BsonDocument(), ct); await collectionV2.DeleteManyAsync(FindAll, ct);
var batchBlock = new BatchBlock<BsonDocument>(SizeOfBatch, new GroupingDataflowBlockOptions var batchBlock = new BatchBlock<BsonDocument>(SizeOfBatch, new GroupingDataflowBlockOptions
{ {
@ -126,7 +127,7 @@ namespace Migrations.Migrations.MongoDb
extraAction?.Invoke(document); extraAction?.Invoke(document);
var filter = Builders<BsonDocument>.Filter.Eq("_id", documentIdNew); var filter = Filter.Eq("_id", documentIdNew);
writes.Add(new ReplaceOneModel<BsonDocument>(filter, document) writes.Add(new ReplaceOneModel<BsonDocument>(filter, document)
{ {
@ -153,7 +154,7 @@ namespace Migrations.Migrations.MongoDb
batchBlock.BidirectionalLinkTo(actionBlock); 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)) if (!await batchBlock.SendAsync(document, ct))
{ {

15
backend/src/Migrations/Migrations/MongoDb/ConvertOldSnapshotStores.cs

@ -8,10 +8,11 @@
using MongoDB.Bson; using MongoDB.Bson;
using MongoDB.Driver; using MongoDB.Driver;
using Squidex.Infrastructure.Migrations; using Squidex.Infrastructure.Migrations;
using Squidex.Infrastructure.MongoDb;
namespace Migrations.Migrations.MongoDb namespace Migrations.Migrations.MongoDb
{ {
public sealed class ConvertOldSnapshotStores : IMigration public sealed class ConvertOldSnapshotStores : MongoBase<BsonDocument>, IMigration
{ {
private readonly IMongoDatabase database; private readonly IMongoDatabase database;
@ -23,21 +24,17 @@ namespace Migrations.Migrations.MongoDb
public Task UpdateAsync( public Task UpdateAsync(
CancellationToken ct) CancellationToken ct)
{ {
// Do not resolve in constructor, because most of the time it is not executed anyway.
var collections = new[] var collections = new[]
{ {
"States_Apps", "States_Apps",
"States_Rules", "States_Rules",
"States_Schemas" "States_Schemas"
}; }.Select(x => database.GetCollection<BsonDocument>(x));
var update = Builders<BsonDocument>.Update.Rename("State", "Doc"); var update = Update.Rename("State", "Doc");
var filter = new BsonDocument(); return Task.WhenAll(collections.Select(x => x.UpdateManyAsync(FindAll, update, cancellationToken: ct)));
return Task.WhenAll(
collections
.Select(x => database.GetCollection<BsonDocument>(x))
.Select(x => x.UpdateManyAsync(filter, update, cancellationToken: ct)));
} }
} }
} }

7
backend/src/Migrations/Migrations/MongoDb/ConvertRuleEventsJson.cs

@ -8,10 +8,11 @@
using MongoDB.Bson; using MongoDB.Bson;
using MongoDB.Driver; using MongoDB.Driver;
using Squidex.Infrastructure.Migrations; using Squidex.Infrastructure.Migrations;
using Squidex.Infrastructure.MongoDb;
namespace Migrations.Migrations.MongoDb namespace Migrations.Migrations.MongoDb
{ {
public sealed class ConvertRuleEventsJson : IMigration public sealed class ConvertRuleEventsJson : MongoBase<BsonDocument>, IMigration
{ {
private readonly IMongoCollection<BsonDocument> collection; private readonly IMongoCollection<BsonDocument> collection;
@ -23,13 +24,13 @@ namespace Migrations.Migrations.MongoDb
public async Task UpdateAsync( public async Task UpdateAsync(
CancellationToken ct) CancellationToken ct)
{ {
foreach (var document in collection.Find(new BsonDocument()).ToEnumerable(ct)) foreach (var document in collection.Find(FindAll).ToEnumerable(ct))
{ {
try try
{ {
document["Job"]["actionData"] = document["Job"]["actionData"].ToBsonDocument().ToJson(); document["Job"]["actionData"] = document["Job"]["actionData"].ToBsonDocument().ToJson();
var filter = Builders<BsonDocument>.Filter.Eq("_id", document["_id"].ToString()); var filter = Filter.Eq("_id", document["_id"].ToString());
await collection.ReplaceOneAsync(filter, document, cancellationToken: ct); await collection.ReplaceOneAsync(filter, document, cancellationToken: ct);
} }

70
backend/src/Migrations/Migrations/MongoDb/RenameAssetMetadata.cs

@ -8,10 +8,11 @@
using MongoDB.Bson; using MongoDB.Bson;
using MongoDB.Driver; using MongoDB.Driver;
using Squidex.Infrastructure.Migrations; using Squidex.Infrastructure.Migrations;
using Squidex.Infrastructure.MongoDb;
namespace Migrations.Migrations.MongoDb namespace Migrations.Migrations.MongoDb
{ {
public sealed class RenameAssetMetadata : IMigration public sealed class RenameAssetMetadata : MongoBase<BsonDocument>, IMigration
{ {
private readonly IMongoDatabase database; private readonly IMongoDatabase database;
@ -23,45 +24,38 @@ namespace Migrations.Migrations.MongoDb
public async Task UpdateAsync( public async Task UpdateAsync(
CancellationToken ct) CancellationToken ct)
{ {
// Do not resolve in constructor, because most of the time it is not executed anyway.
var collection = database.GetCollection<BsonDocument>("States_Assets"); var collection = database.GetCollection<BsonDocument>("States_Assets");
var createMetadata = // Create metadata.
Builders<BsonDocument>.Update await collection.UpdateManyAsync(FindAll,
.Set("md", new BsonDocument()); Update.Set("md", new BsonDocument()),
cancellationToken: ct);
await collection.UpdateManyAsync(new BsonDocument(), createMetadata, cancellationToken: ct);
// Remove null pixel infos.
var removeNullPixelInfos = await collection.UpdateManyAsync(new BsonDocument("ph", BsonValue.Create(null)),
Builders<BsonDocument>.Update Update.Unset("ph").Unset("pw"),
.Unset("ph") cancellationToken: ct);
.Unset("pw");
// Set pixel metadata.
await collection.UpdateManyAsync(new BsonDocument("ph", BsonValue.Create(null)), removeNullPixelInfos, cancellationToken: ct); await collection.UpdateManyAsync(FindAll,
Update.Rename("ph", "md.pixelHeight").Rename("pw", "md.pixelWidth"),
var setPixelDimensions = cancellationToken: ct);
Builders<BsonDocument>.Update
.Rename("ph", "md.pixelHeight") // Set type to image.
.Rename("pw", "md.pixelWidth"); await collection.UpdateManyAsync(new BsonDocument("im", true),
Update.Set("at", "Image"),
await collection.UpdateManyAsync(new BsonDocument(), setPixelDimensions, cancellationToken: ct); cancellationToken: ct);
var setTypeToImage = // Set type to unknown.
Builders<BsonDocument>.Update await collection.UpdateManyAsync(new BsonDocument("im", false),
.Set("at", "Image"); Update.Set("at", "Unknown"),
cancellationToken: ct);
await collection.UpdateManyAsync(new BsonDocument("im", true), setTypeToImage, cancellationToken: ct);
// Remove IsImage.
var setTypeToUnknown = await collection.UpdateManyAsync(FindAll,
Builders<BsonDocument>.Update Update.Unset("im"),
.Set("at", "Unknown"); cancellationToken: ct);
await collection.UpdateManyAsync(new BsonDocument("im", false), setTypeToUnknown, cancellationToken: ct);
var removeIsImage =
Builders<BsonDocument>.Update
.Unset("im");
await collection.UpdateManyAsync(new BsonDocument(), removeIsImage, cancellationToken: ct);
} }
} }
} }

6
backend/src/Migrations/Migrations/MongoDb/RenameAssetSlugField.cs

@ -8,10 +8,11 @@
using MongoDB.Bson; using MongoDB.Bson;
using MongoDB.Driver; using MongoDB.Driver;
using Squidex.Infrastructure.Migrations; using Squidex.Infrastructure.Migrations;
using Squidex.Infrastructure.MongoDb;
namespace Migrations.Migrations.MongoDb namespace Migrations.Migrations.MongoDb
{ {
public sealed class RenameAssetSlugField : IMigration public sealed class RenameAssetSlugField : MongoBase<BsonDocument>, IMigration
{ {
private readonly IMongoDatabase database; private readonly IMongoDatabase database;
@ -23,11 +24,12 @@ namespace Migrations.Migrations.MongoDb
public Task UpdateAsync( public Task UpdateAsync(
CancellationToken ct) CancellationToken ct)
{ {
// Do not resolve in constructor, because most of the time it is not executed anyway.
var collection = database.GetCollection<BsonDocument>("States_Assets"); var collection = database.GetCollection<BsonDocument>("States_Assets");
var update = Builders<BsonDocument>.Update.Rename("FileNameSlug", "Slug"); var update = Builders<BsonDocument>.Update.Rename("FileNameSlug", "Slug");
return collection.UpdateManyAsync(new BsonDocument(), update, cancellationToken: ct); return collection.UpdateManyAsync(FindAll, update, cancellationToken: ct);
} }
} }
} }

5
backend/src/Migrations/Migrations/MongoDb/RestructureContentCollection.cs

@ -12,7 +12,7 @@ using Squidex.Infrastructure.MongoDb;
namespace Migrations.Migrations.MongoDb namespace Migrations.Migrations.MongoDb
{ {
public sealed class RestructureContentCollection : IMigration public sealed class RestructureContentCollection : MongoBase<BsonDocument>, IMigration
{ {
private readonly IMongoDatabase contentDatabase; private readonly IMongoDatabase contentDatabase;
@ -28,6 +28,7 @@ namespace Migrations.Migrations.MongoDb
{ {
await contentDatabase.DropCollectionAsync("State_Contents", ct); await contentDatabase.DropCollectionAsync("State_Contents", ct);
await contentDatabase.DropCollectionAsync("State_Content_Published", ct); await contentDatabase.DropCollectionAsync("State_Content_Published", ct);
await contentDatabase.RenameCollectionAsync("State_Content_Draft", "State_Contents", cancellationToken: ct); await contentDatabase.RenameCollectionAsync("State_Content_Draft", "State_Contents", cancellationToken: ct);
} }
@ -35,7 +36,7 @@ namespace Migrations.Migrations.MongoDb
{ {
var collection = contentDatabase.GetCollection<BsonDocument>("State_Contents"); var collection = contentDatabase.GetCollection<BsonDocument>("State_Contents");
await collection.UpdateManyAsync(new BsonDocument(), Builders<BsonDocument>.Update.Unset("dt"), cancellationToken: ct); await collection.UpdateManyAsync(FindAll, Update.Unset("dt"), cancellationToken: ct);
} }
} }
} }

11
backend/src/Squidex.Domain.Apps.Core.Model/Contents/GeoJsonValue.cs

@ -5,8 +5,7 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using GeoJSON.Net; using NetTopologySuite.Geometries;
using GeoJSON.Net.Geometry;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Json;
using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Json.Objects;
@ -17,7 +16,7 @@ namespace Squidex.Domain.Apps.Core.Contents
{ {
public static class GeoJsonValue 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(serializer);
Guard.NotNull(value); Guard.NotNull(value);
@ -41,7 +40,7 @@ namespace Squidex.Domain.Apps.Core.Contents
return GeoJsonParseResult.InvalidLongitude; return GeoJsonParseResult.InvalidLongitude;
} }
geoJSON = new Point(new Position(lat, lon)); geoJSON = new Point(new Coordinate(lat, lon));
return GeoJsonParseResult.Success; return GeoJsonParseResult.Success;
} }
@ -49,7 +48,7 @@ namespace Squidex.Domain.Apps.Core.Contents
return GeoJsonParseResult.InvalidValue; 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; geoJSON = null;
@ -66,7 +65,7 @@ namespace Squidex.Domain.Apps.Core.Contents
stream.Position = 0; stream.Position = 0;
geoJSON = serializer.Deserialize<GeoJSONObject>(stream, null, true); geoJSON = serializer.Deserialize<Geometry>(stream, null, true);
return true; return true;
} }

48
backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/ContentFieldDataConverter.cs

@ -5,30 +5,16 @@
// All rights reserved. Licensed under the MIT license. // 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;
using Squidex.Infrastructure.Json.Newtonsoft;
using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Domain.Apps.Core.Contents.Json namespace Squidex.Domain.Apps.Core.Contents.Json
{ {
public sealed class ContentFieldDataConverter : JsonClassConverter<ContentFieldData> public sealed class ContentFieldDataConverter : JsonConverter<ContentFieldData>
{ {
protected override void WriteValue(JsonWriter writer, ContentFieldData value, JsonSerializer serializer) public override ContentFieldData? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
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)
{ {
var result = new ContentFieldData(); var result = new ContentFieldData();
@ -36,15 +22,15 @@ namespace Squidex.Domain.Apps.Core.Contents.Json
{ {
switch (reader.TokenType) switch (reader.TokenType)
{ {
case JsonToken.PropertyName: case JsonTokenType.PropertyName:
var propertyName = reader.Value!.ToString()!; var propertyName = reader.GetString()!;
if (!reader.Read()) if (!reader.Read())
{ {
throw new JsonSerializationException("Unexpected end when reading Object."); throw new JsonException("Unexpected end when reading Object.");
} }
var value = serializer.Deserialize<JsonValue>(reader)!; var value = JsonSerializer.Deserialize<JsonValue>(ref reader, options)!;
if (propertyName == InvariantPartitioning.Key) if (propertyName == InvariantPartitioning.Key)
{ {
@ -57,12 +43,26 @@ namespace Squidex.Domain.Apps.Core.Contents.Json
result[propertyName] = value; result[propertyName] = value;
break; break;
case JsonToken.EndObject: case JsonTokenType.EndObject:
return result; 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();
} }
} }
} }

6
backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/WorkflowStepSurrogate.cs

@ -5,7 +5,7 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using Newtonsoft.Json; using System.Text.Json.Serialization;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Collections; using Squidex.Infrastructure.Collections;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
@ -16,10 +16,10 @@ namespace Squidex.Domain.Apps.Core.Contents.Json
{ {
public Dictionary<Status, WorkflowTransitionSurrogate> Transitions { get; set; } public Dictionary<Status, WorkflowTransitionSurrogate> Transitions { get; set; }
[JsonProperty("noUpdate")] [JsonPropertyName("noUpdate")]
public bool NoUpdateFlag { get; set; } public bool NoUpdateFlag { get; set; }
[JsonProperty("noUpdateRules")] [JsonPropertyName("noUpdateRules")]
public NoUpdate? NoUpdate { get; set; } public NoUpdate? NoUpdate { get; set; }
public string? Color { get; set; } public string? Color { get; set; }

6
backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedCommentEvent.cs

@ -5,7 +5,7 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Runtime.Serialization; using System.Text.Json.Serialization;
using Squidex.Shared.Users; using Squidex.Shared.Users;
#pragma warning disable CA1822 // Mark members as static #pragma warning disable CA1822 // Mark members as static
@ -21,10 +21,10 @@ namespace Squidex.Domain.Apps.Core.Rules.EnrichedEvents
[FieldDescription(nameof(FieldDescriptions.CommentUrl))] [FieldDescription(nameof(FieldDescriptions.CommentUrl))]
public Uri? Url { get; set; } public Uri? Url { get; set; }
[FieldDescription(nameof(FieldDescriptions.CommentMentionedUser)), IgnoreDataMember] [FieldDescription(nameof(FieldDescriptions.CommentMentionedUser)), JsonIgnore]
public IUser MentionedUser { get; set; } public IUser MentionedUser { get; set; }
[IgnoreDataMember] [JsonIgnore]
public override long Partition public override long Partition
{ {
get => MentionedUser?.Id.GetHashCode(StringComparison.Ordinal) ?? 0; get => MentionedUser?.Id.GetHashCode(StringComparison.Ordinal) ?? 0;

4
backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedUserEventBase.cs

@ -5,7 +5,7 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Runtime.Serialization; using System.Text.Json.Serialization;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Shared.Users; using Squidex.Shared.Users;
@ -19,7 +19,7 @@ namespace Squidex.Domain.Apps.Core.Rules.EnrichedEvents
[FieldDescription(nameof(FieldDescriptions.Actor))] [FieldDescription(nameof(FieldDescriptions.Actor))]
public RefToken Actor { get; set; } public RefToken Actor { get; set; }
[FieldDescription(nameof(FieldDescriptions.User)), IgnoreDataMember] [FieldDescription(nameof(FieldDescriptions.User)), JsonIgnore]
public IUser? User { get; set; } public IUser? User { get; set; }
public bool ShouldSerializeUser() public bool ShouldSerializeUser()

1
backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj

@ -17,6 +17,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="6.0.1" /> <PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="6.0.1" />
<PackageReference Include="NetTopologySuite" Version="2.5.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Collections.Immutable" Version="6.0.0" /> <PackageReference Include="System.Collections.Immutable" Version="6.0.0" />

20
backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs

@ -9,11 +9,12 @@ using System.Globalization;
using System.Security.Claims; using System.Security.Claims;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Newtonsoft.Json; using NodaTime;
using NodaTime.Text; using NodaTime.Text;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Core.Templates; using Squidex.Domain.Apps.Core.Templates;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Json;
using Squidex.Shared; using Squidex.Shared;
using Squidex.Shared.Identity; using Squidex.Shared.Identity;
@ -79,16 +80,20 @@ namespace Squidex.Domain.Apps.Core.HandleRules
public virtual string ToPayload<T>(T @event) public virtual string ToPayload<T>(T @event)
{ {
var payload = @event; // Just serialize the payload.
return serializer.Serialize(@event, true);
return serializer.Serialize(payload);
} }
public virtual string ToEnvelope(EnrichedEvent @event) 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<string?> FormatAsync(string text, EnrichedEvent @event) public async ValueTask<string?> FormatAsync(string text, EnrichedEvent @event)
@ -288,8 +293,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules
text = text.ToUpperInvariant(); text = text.ToUpperInvariant();
break; break;
case "escape": case "escape":
text = JsonConvert.ToString(text); text = text.JsonEscape();
text = text[1..^1];
break; break;
case "slugify": case "slugify":
text = text.Slugify(); text = text.Slugify();

8
backend/src/Squidex.Domain.Apps.Core.Operations/Templates/Extensions/StringFluidExtension.cs

@ -7,7 +7,6 @@
using Fluid; using Fluid;
using Fluid.Values; using Fluid.Values;
using Newtonsoft.Json;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Text; using Squidex.Text;
@ -29,12 +28,7 @@ namespace Squidex.Domain.Apps.Core.Templates.Extensions
private static readonly FilterDelegate Escape = (input, arguments, context) => private static readonly FilterDelegate Escape = (input, arguments, context) =>
{ {
var result = input.ToStringValue(); return FluidValue.Create(input.ToStringValue().JsonEscape());
result = JsonConvert.ToString(result);
result = result[1..^1];
return FluidValue.Create(result);
}; };
private static readonly FilterDelegate Markdown2Text = (input, arguments, context) => private static readonly FilterDelegate Markdown2Text = (input, arguments, context) =>

5
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs

@ -6,7 +6,6 @@
// ========================================================================== // ==========================================================================
using MongoDB.Driver; using MongoDB.Driver;
using Newtonsoft.Json;
using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Apps.DomainObject; using Squidex.Domain.Apps.Entities.Apps.DomainObject;
using Squidex.Domain.Apps.Entities.Apps.Repositories; using Squidex.Domain.Apps.Entities.Apps.Repositories;
@ -18,8 +17,8 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Apps
{ {
public sealed class MongoAppRepository : MongoSnapshotStoreBase<AppDomainObject.State, MongoAppEntity>, IAppRepository, IDeleter public sealed class MongoAppRepository : MongoSnapshotStoreBase<AppDomainObject.State, MongoAppEntity>, IAppRepository, IDeleter
{ {
public MongoAppRepository(IMongoDatabase database, JsonSerializer serializer) public MongoAppRepository(IMongoDatabase database)
: base(database, serializer) : base(database)
{ {
} }

1
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs

@ -103,7 +103,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
[BsonElement("dl")] [BsonElement("dl")]
public bool IsDeleted { get; set; } public bool IsDeleted { get; set; }
[BsonJson]
[BsonRequired] [BsonRequired]
[BsonElement("md")] [BsonElement("md")]
public AssetMetadata Metadata { get; set; } public AssetMetadata Metadata { get; set; }

2
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderRepository_SnapshotStore.cs

@ -28,7 +28,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
IAsyncEnumerable<SnapshotResult<AssetFolderDomainObject.State>> ISnapshotStore<AssetFolderDomainObject.State>.ReadAllAsync( IAsyncEnumerable<SnapshotResult<AssetFolderDomainObject.State>> ISnapshotStore<AssetFolderDomainObject.State>.ReadAllAsync(
CancellationToken ct) CancellationToken ct)
{ {
return Collection.Find(new BsonDocument(), Batching.Options).ToAsyncEnumerable(ct) return Collection.Find(FindAll, Batching.Options).ToAsyncEnumerable(ct)
.Select(x => new SnapshotResult<AssetFolderDomainObject.State>(x.DocumentId, x.ToState(), x.Version, true)); .Select(x => new SnapshotResult<AssetFolderDomainObject.State>(x.DocumentId, x.ToState(), x.Version, true));
} }

2
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs

@ -28,7 +28,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
IAsyncEnumerable<SnapshotResult<AssetDomainObject.State>> ISnapshotStore<AssetDomainObject.State>.ReadAllAsync( IAsyncEnumerable<SnapshotResult<AssetDomainObject.State>> ISnapshotStore<AssetDomainObject.State>.ReadAllAsync(
CancellationToken ct) CancellationToken ct)
{ {
return Collection.Find(new BsonDocument(), Batching.Options).ToAsyncEnumerable(ct) return Collection.Find(FindAll, Batching.Options).ToAsyncEnumerable(ct)
.Select(x => new SnapshotResult<AssetDomainObject.State>(x.DocumentId, x.ToState(), x.Version)); .Select(x => new SnapshotResult<AssetDomainObject.State>(x.DocumentId, x.ToState(), x.Version));
} }

2
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs

@ -248,7 +248,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
public IAsyncEnumerable<MongoContentEntity> StreamAll( public IAsyncEnumerable<MongoContentEntity> StreamAll(
CancellationToken ct) 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, public async Task UpsertVersionedAsync(DomainId documentId, long oldVersion, MongoContentEntity value,

2
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs

@ -59,12 +59,10 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
[BsonIgnoreIfNull] [BsonIgnoreIfNull]
[BsonElement("do")] [BsonElement("do")]
[BsonJson]
public ContentData Data { get; set; } public ContentData Data { get; set; }
[BsonIgnoreIfNull] [BsonIgnoreIfNull]
[BsonElement("dd")] [BsonElement("dd")]
[BsonJson]
public ContentData? DraftData { get; set; } public ContentData? DraftData { get; set; }
[BsonIgnoreIfNull] [BsonIgnoreIfNull]

2
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs

@ -28,7 +28,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
static MongoContentRepository() static MongoContentRepository()
{ {
TypeConverterStringSerializer<Status>.Register(); BsonStringSerializer<Status>.Register();
} }
public MongoContentRepository(IMongoDatabase database, IAppProvider appProvider, public MongoContentRepository(IMongoDatabase database, IAppProvider appProvider,

2
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++) for (var i = 1; i < path.Count; i++)
{ {
result[i] = result[i].UnescapeEdmField().EscapeJson(); result[i] = result[i].UnescapeEdmField().JsonEscape();
} }
return result; return result;

4
backend/src/Squidex.Domain.Apps.Entities.MongoDb/MongoCountCollection.cs

@ -20,8 +20,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb
: base(database) : base(database)
{ {
this.name = $"{name}_Count"; this.name = $"{name}_Count";
InitializeAsync(default).Wait();
} }
protected override string CollectionName() protected override string CollectionName()
@ -34,8 +32,10 @@ namespace Squidex.Domain.Apps.Entities.MongoDb
{ {
var (cachedTotal, isOutdated) = await CountAsync(key, ct); 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) if (cachedTotal < 5_000)
{ {
// We always want to have up to date collection sizes for smaller schemas.
return await RefreshTotalAsync(key, cachedTotal, provider, ct); return await RefreshTotalAsync(key, cachedTotal, provider, ct);
} }

5
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs

@ -6,7 +6,6 @@
// ========================================================================== // ==========================================================================
using MongoDB.Driver; using MongoDB.Driver;
using Newtonsoft.Json;
using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Rules; using Squidex.Domain.Apps.Entities.Rules;
using Squidex.Domain.Apps.Entities.Rules.DomainObject; using Squidex.Domain.Apps.Entities.Rules.DomainObject;
@ -18,8 +17,8 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Rules
{ {
public sealed class MongoRuleRepository : MongoSnapshotStoreBase<RuleDomainObject.State, MongoRuleEntity>, IRuleRepository, IDeleter public sealed class MongoRuleRepository : MongoSnapshotStoreBase<RuleDomainObject.State, MongoRuleEntity>, IRuleRepository, IDeleter
{ {
public MongoRuleRepository(IMongoDatabase database, JsonSerializer serializer) public MongoRuleRepository(IMongoDatabase database)
: base(database, serializer) : base(database)
{ {
} }

5
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository.cs

@ -6,7 +6,6 @@
// ========================================================================== // ==========================================================================
using MongoDB.Driver; using MongoDB.Driver;
using Newtonsoft.Json;
using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Domain.Apps.Entities.Schemas.DomainObject; using Squidex.Domain.Apps.Entities.Schemas.DomainObject;
@ -18,8 +17,8 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Schemas
{ {
public sealed class MongoSchemaRepository : MongoSnapshotStoreBase<SchemaDomainObject.State, MongoSchemaEntity>, ISchemaRepository, IDeleter public sealed class MongoSchemaRepository : MongoSnapshotStoreBase<SchemaDomainObject.State, MongoSchemaEntity>, ISchemaRepository, IDeleter
{ {
public MongoSchemaRepository(IMongoDatabase database, JsonSerializer serializer) public MongoSchemaRepository(IMongoDatabase database)
: base(database, serializer) : base(database)
{ {
} }

4
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemasHash.cs

@ -39,8 +39,8 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Schemas
get => "^schema-"; get => "^schema-";
} }
public MongoSchemasHash(IMongoDatabase database, bool setup = false) public MongoSchemasHash(IMongoDatabase database)
: base(database, setup) : base(database)
{ {
} }

4
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 readonly AtlasOptions options;
private string index; private string index;
public AtlasTextIndex(IMongoDatabase database, IOptions<AtlasOptions> options, bool setup = false) public AtlasTextIndex(IMongoDatabase database, IOptions<AtlasOptions> options)
: base(database, setup) : base(database)
{ {
this.options = options.Value; this.options = options.Value;
} }

8
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/CommandFactory.cs

@ -7,16 +7,12 @@
using MongoDB.Driver; using MongoDB.Driver;
using Squidex.Domain.Apps.Entities.Contents.Text; using Squidex.Domain.Apps.Entities.Contents.Text;
using Squidex.Infrastructure.MongoDb;
namespace Squidex.Domain.Apps.Entities.MongoDb.Text namespace Squidex.Domain.Apps.Entities.MongoDb.Text
{ {
public sealed class CommandFactory<T> where T : class public sealed class CommandFactory<T> : MongoBase<MongoTextIndexEntity<T>> where T : class
{ {
#pragma warning disable RECS0108 // Warns about static fields in generic types
private static readonly FilterDefinitionBuilder<MongoTextIndexEntity<T>> Filter = Builders<MongoTextIndexEntity<T>>.Filter;
private static readonly UpdateDefinitionBuilder<MongoTextIndexEntity<T>> Update = Builders<MongoTextIndexEntity<T>>.Update;
#pragma warning restore RECS0108 // Warns about static fields in generic types
private readonly Func<Dictionary<string, string>, T> textBuilder; private readonly Func<Dictionary<string, string>, T> textBuilder;
public CommandFactory(Func<Dictionary<string, string>, T> textBuilder) public CommandFactory(Func<Dictionary<string, string>, T> textBuilder)

4
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<List<MongoTextIndexEntityText>> public sealed class MongoTextIndex : MongoTextIndexBase<List<MongoTextIndexEntityText>>
{ {
public MongoTextIndex(IMongoDatabase database, bool setup = false) public MongoTextIndex(IMongoDatabase database)
: base(database, setup) : base(database)
{ {
} }

25
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<T> : MongoRepositoryBase<MongoTextIndexEntity<T>>, ITextIndex, IDeleter where T : class public abstract class MongoTextIndexBase<T> : MongoRepositoryBase<MongoTextIndexEntity<T>>, ITextIndex, IDeleter where T : class
{ {
private readonly ProjectionDefinition<MongoTextIndexEntity<T>> searchTextProjection;
private readonly ProjectionDefinition<MongoTextIndexEntity<T>> searchGeoProjection;
private readonly CommandFactory<T> commandFactory; private readonly CommandFactory<T> commandFactory;
protected sealed class MongoTextResult protected sealed class MongoTextResult
@ -39,12 +37,9 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Text
public double Score { get; set; } public double Score { get; set; }
} }
protected MongoTextIndexBase(IMongoDatabase database, bool setup = false) protected MongoTextIndexBase(IMongoDatabase database)
: base(database, setup) : 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 #pragma warning disable MA0056 // Do not call overridable members in constructor
commandFactory = new CommandFactory<T>(BuildTexts); commandFactory = new CommandFactory<T>(BuildTexts);
#pragma warning restore MA0056 // Do not call overridable members in constructor #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)); Filter.GeoWithinCenterSphere(x => x.GeoObject, query.Longitude, query.Latitude, query.Radius / 6378100));
var byGeo = var byGeo =
await GetCollection(scope).Find(findFilter).Limit(query.Take).Project<MongoTextResult>(searchGeoProjection) await GetCollection(scope).Find(findFilter).Limit(query.Take)
.Project<MongoTextResult>(Projection.Include(x => x.ContentId))
.ToListAsync(ct); .ToListAsync(ct);
return byGeo.Select(x => x.ContentId).ToList(); 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<MongoTextIndexEntity<T>> filter, SearchScope scope, int take, double factor, private async Task SearchAsync(List<(DomainId, double)> result, FilterDefinition<MongoTextIndexEntity<T>> filter, SearchScope scope, int take, double factor,
CancellationToken ct = default) CancellationToken ct = default)
{ {
var collection = GetCollection(scope); var byText =
await GetCollection(scope).Find(filter).Limit(take)
var find = .Project<MongoTextResult>(Projection.Include(x => x.ContentId).MetaTextScore("score")).Sort(Sort.MetaTextScore("score"))
collection.Find(filter).Limit(take) .ToListAsync(ct);
.Project<MongoTextResult>(searchTextProjection).Sort(Sort.MetaTextScore("score"));
var documents = await find.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<MongoTextIndexEntity<T>> Filter_ByScope(SearchScope scope) private static FilterDefinition<MongoTextIndexEntity<T>> Filter_ByScope(SearchScope scope)

4
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/MongoTextIndexEntity.cs

@ -5,9 +5,9 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using GeoJSON.Net;
using MongoDB.Bson; using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes; using MongoDB.Bson.Serialization.Attributes;
using NetTopologySuite.Geometries;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.MongoDb;
@ -58,6 +58,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Text
[BsonIgnoreIfNull] [BsonIgnoreIfNull]
[BsonElement("go")] [BsonElement("go")]
[BsonJson] [BsonJson]
public GeoJSONObject GeoObject { get; set; } public Geometry GeoObject { get; set; }
} }
} }

4
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) public MongoTextIndexerState(IMongoDatabase database)
: base(database, setup) : base(database)
{ {
} }

2
backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AppCommand.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Runtime.Serialization;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
@ -13,7 +12,6 @@ namespace Squidex.Domain.Apps.Entities.Apps.Commands
{ {
public abstract class AppCommand : SquidexCommand, IAggregateCommand public abstract class AppCommand : SquidexCommand, IAggregateCommand
{ {
[IgnoreDataMember]
public abstract DomainId AggregateId { get; } public abstract DomainId AggregateId { get; }
} }
} }

2
backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AppUpdateCommand.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Runtime.Serialization;
using Squidex.Infrastructure; using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Apps.Commands namespace Squidex.Domain.Apps.Entities.Apps.Commands
@ -14,7 +13,6 @@ namespace Squidex.Domain.Apps.Entities.Apps.Commands
{ {
public NamedId<DomainId> AppId { get; set; } public NamedId<DomainId> AppId { get; set; }
[IgnoreDataMember]
public override DomainId AggregateId public override DomainId AggregateId
{ {
get => AppId.Id; get => AppId.Id;

2
backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/CreateApp.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Runtime.Serialization;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
@ -19,7 +18,6 @@ namespace Squidex.Domain.Apps.Entities.Apps.Commands
public string? Template { get; set; } public string? Template { get; set; }
[IgnoreDataMember]
public override DomainId AggregateId public override DomainId AggregateId
{ {
get => AppId; get => AppId;

4
backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppDomainObject.State.cs

@ -5,7 +5,7 @@
// All rights reserved. Licensed under the MIT license. // 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.Apps;
using Squidex.Domain.Apps.Core.Assets; using Squidex.Domain.Apps.Core.Assets;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
@ -49,7 +49,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject
public bool IsDeleted { get; set; } public bool IsDeleted { get; set; }
[IgnoreDataMember] [JsonIgnore]
public DomainId UniqueId public DomainId UniqueId
{ {
get => Id; get => Id;

2
backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/UsageNotifierWorker.cs

@ -25,7 +25,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans
[CollectionName("UsageNotifications")] [CollectionName("UsageNotifications")]
public sealed class State public sealed class State
{ {
public Dictionary<DomainId, DateTime> NotificationsSent { get; } = new Dictionary<DomainId, DateTime>(); public Dictionary<DomainId, DateTime> NotificationsSent { get; set; } = new Dictionary<DomainId, DateTime>();
} }
public IClock Clock { get; set; } = SystemClock.Instance; public IClock Clock { get; set; } = SystemClock.Instance;

2
backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/AssetCommand.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Runtime.Serialization;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
@ -19,7 +18,6 @@ namespace Squidex.Domain.Apps.Entities.Assets.Commands
public bool DoNotScript { get; set; } public bool DoNotScript { get; set; }
[IgnoreDataMember]
public DomainId AggregateId public DomainId AggregateId
{ {
get => DomainId.Combine(AppId, AssetId); get => DomainId.Combine(AppId, AssetId);

2
backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/AssetFolderCommand.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Runtime.Serialization;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
@ -17,7 +16,6 @@ namespace Squidex.Domain.Apps.Entities.Assets.Commands
public DomainId AssetFolderId { get; set; } public DomainId AssetFolderId { get; set; }
[IgnoreDataMember]
public DomainId AggregateId public DomainId AggregateId
{ {
get => DomainId.Combine(AppId, AssetFolderId); get => DomainId.Combine(AppId, AssetFolderId);

6
backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetDomainObject.State.cs

@ -5,7 +5,7 @@
// All rights reserved. Licensed under the MIT license. // 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.Core.Assets;
using Squidex.Domain.Apps.Events.Assets; using Squidex.Domain.Apps.Events.Assets;
using Squidex.Infrastructure; using Squidex.Infrastructure;
@ -47,13 +47,13 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
public AssetType Type { get; set; } public AssetType Type { get; set; }
[IgnoreDataMember] [JsonIgnore]
public DomainId AssetId public DomainId AssetId
{ {
get => Id; get => Id;
} }
[IgnoreDataMember] [JsonIgnore]
public DomainId UniqueId public DomainId UniqueId
{ {
get => DomainId.Combine(AppId, Id); get => DomainId.Combine(AppId, Id);

4
backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetFolderDomainObject.State.cs

@ -5,7 +5,7 @@
// All rights reserved. Licensed under the MIT license. // 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.Domain.Apps.Events.Assets;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
@ -26,7 +26,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
public bool IsDeleted { get; set; } public bool IsDeleted { get; set; }
[IgnoreDataMember] [JsonIgnore]
public DomainId UniqueId public DomainId UniqueId
{ {
get => DomainId.Combine(AppId, Id); get => DomainId.Combine(AppId, Id);

66
backend/src/Squidex.Domain.Apps.Entities/Backup/Model/CompatibleStoredEvent.cs

@ -5,32 +5,30 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Globalization; using System.Text.Json.Serialization;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Squidex.Infrastructure.EventSourcing; 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 #pragma warning disable MA0048 // File name must match type name
namespace Squidex.Domain.Apps.Entities.Backup.Model namespace Squidex.Domain.Apps.Entities.Backup.Model
{ {
public sealed class CompatibleStoredEvent public sealed class CompatibleStoredEvent
{ {
[JsonProperty("n")] [JsonPropertyName("n")]
public NewEvent NewEvent; public NewEvent NewEvent { get; set; }
[JsonProperty] [JsonPropertyName("streamName")]
public string StreamName; public string StreamName { get; set; }
[JsonProperty] [JsonPropertyName("eventPosition")]
public string EventPosition; public string EventPosition { get; set; }
[JsonProperty] [JsonPropertyName("eventStreamNumber")]
public long EventStreamNumber; public long EventStreamNumber { get; set; }
[JsonProperty] [JsonPropertyName("data")]
public CompatibleEventData Data; public CompatibleEventData Data { get; set; }
public static CompatibleStoredEvent V1(StoredEvent stored) public static CompatibleStoredEvent V1(StoredEvent stored)
{ {
@ -65,41 +63,45 @@ namespace Squidex.Domain.Apps.Entities.Backup.Model
public sealed class CompatibleEventData public sealed class CompatibleEventData
{ {
[JsonProperty] [JsonPropertyName("type")]
public string Type; public string Type { get; set; }
[JsonProperty] [JsonPropertyName("metadata")]
public JRaw Payload; public EnvelopeHeaders EventHeaders { get; set; }
[JsonProperty] [JsonPropertyName("payload")]
public EnvelopeHeaders Metadata; [JsonConverter(typeof(UnsafeRawJsonConverter))]
public string EventPayload { get; set; }
public static CompatibleEventData V1(EventData data) public static CompatibleEventData V1(EventData data)
{ {
var payload = new JRaw(data.Payload); return new CompatibleEventData
{
return new CompatibleEventData { Type = data.Type, Payload = payload, Metadata = data.Headers }; Type = data.Type,
EventPayload = data.Payload,
EventHeaders = data.Headers
};
} }
public EventData ToData() public EventData ToData()
{ {
return new EventData(Type, Metadata, Payload.ToString(CultureInfo.InvariantCulture)); return new EventData(Type, EventHeaders, EventPayload);
} }
} }
public sealed class NewEvent public sealed class NewEvent
{ {
[JsonProperty("t")] [JsonPropertyName("t")]
public string EventType; public string EventType { get; set; }
[JsonProperty("s")] [JsonPropertyName("s")]
public string StreamName; public string StreamName { get; set; }
[JsonProperty("p")] [JsonPropertyName("p")]
public string EventPayload; public string EventPayload { get; set; }
[JsonProperty("h")] [JsonPropertyName("h")]
public EnvelopeHeaders EventHeaders; public EnvelopeHeaders EventHeaders { get; set; }
public static NewEvent V2(StoredEvent stored) public static NewEvent V2(StoredEvent stored)
{ {

2
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 sealed class BackupState
{ {
public List<BackupJob> Jobs { get; } = new List<BackupJob>(); public List<BackupJob> Jobs { get; set; } = new List<BackupJob>();
public void EnsureCanStart() public void EnsureCanStart()
{ {

2
backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentCommand.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Runtime.Serialization;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
@ -21,7 +20,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Commands
public bool DoNotScript { get; set; } public bool DoNotScript { get; set; }
[IgnoreDataMember]
public DomainId AggregateId public DomainId AggregateId
{ {
get => DomainId.Combine(AppId, ContentId); get => DomainId.Combine(AppId, ContentId);

12
backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.State.cs

@ -5,7 +5,7 @@
// All rights reserved. Licensed under the MIT license. // 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.Core.Contents;
using Squidex.Domain.Apps.Events.Contents; using Squidex.Domain.Apps.Events.Contents;
using Squidex.Infrastructure; using Squidex.Infrastructure;
@ -30,31 +30,31 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
public bool IsDeleted { get; set; } public bool IsDeleted { get; set; }
[IgnoreDataMember] [JsonIgnore]
public DomainId UniqueId public DomainId UniqueId
{ {
get => DomainId.Combine(AppId, Id); get => DomainId.Combine(AppId, Id);
} }
[IgnoreDataMember] [JsonIgnore]
public ContentData Data public ContentData Data
{ {
get => NewVersion?.Data ?? CurrentData; get => NewVersion?.Data ?? CurrentData;
} }
[IgnoreDataMember] [JsonIgnore]
public ContentData CurrentData public ContentData CurrentData
{ {
get => CurrentVersion.Data; get => CurrentVersion.Data;
} }
[IgnoreDataMember] [JsonIgnore]
public Status? NewStatus public Status? NewStatus
{ {
get => NewVersion?.Status; get => NewVersion?.Status;
} }
[IgnoreDataMember] [JsonIgnore]
public Status Status public Status Status
{ {
get => CurrentVersion?.Status ?? default; get => CurrentVersion?.Status ?? default;

8
backend/src/Squidex.Domain.Apps.Entities/Contents/Text/Extensions.cs

@ -6,7 +6,7 @@
// ========================================================================== // ==========================================================================
using System.Text; using System.Text;
using GeoJSON.Net; using NetTopologySuite.Geometries;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Json;
using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Json.Objects;
@ -16,9 +16,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Text
{ {
public static class Extensions public static class Extensions
{ {
public static Dictionary<string, GeoJSONObject>? ToGeo(this ContentData data, IJsonSerializer serializer) public static Dictionary<string, Geometry>? ToGeo(this ContentData data, IJsonSerializer serializer)
{ {
Dictionary<string, GeoJSONObject>? result = null; Dictionary<string, Geometry>? result = null;
foreach (var (field, value) in data) foreach (var (field, value) in data)
{ {
@ -30,7 +30,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Text
if (geoJson != null) if (geoJson != null)
{ {
result ??= new Dictionary<string, GeoJSONObject>(); result ??= new Dictionary<string, Geometry>();
result[$"{field}.{key}"] = geoJson; result[$"{field}.{key}"] = geoJson;
} }
} }

4
backend/src/Squidex.Domain.Apps.Entities/Contents/Text/UpsertIndexEntry.cs

@ -5,14 +5,14 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using GeoJSON.Net; using NetTopologySuite.Geometries;
using Squidex.Infrastructure; using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Contents.Text namespace Squidex.Domain.Apps.Entities.Contents.Text
{ {
public sealed class UpsertIndexEntry : IndexCommand public sealed class UpsertIndexEntry : IndexCommand
{ {
public Dictionary<string, GeoJSONObject>? GeoObjects { get; set; } public Dictionary<string, Geometry>? GeoObjects { get; set; }
public Dictionary<string, string>? Texts { get; set; } public Dictionary<string, string>? Texts { get; set; }

4
backend/src/Squidex.Domain.Apps.Entities/Rules/DomainObject/RuleDomainObject.State.cs

@ -5,7 +5,7 @@
// All rights reserved. Licensed under the MIT license. // 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.Core.Rules;
using Squidex.Domain.Apps.Events.Rules; using Squidex.Domain.Apps.Events.Rules;
using Squidex.Infrastructure; using Squidex.Infrastructure;
@ -25,7 +25,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.DomainObject
public bool IsDeleted { get; set; } public bool IsDeleted { get; set; }
[IgnoreDataMember] [JsonIgnore]
public DomainId UniqueId public DomainId UniqueId
{ {
get => DomainId.Combine(AppId, Id); get => DomainId.Combine(AppId, Id);

1
backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/CreateSchema.cs

@ -40,7 +40,6 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Commands
public ReadonlyDictionary<string, string>? PreviewUrls { get; set; } public ReadonlyDictionary<string, string>? PreviewUrls { get; set; }
[IgnoreDataMember]
public override DomainId AggregateId public override DomainId AggregateId
{ {
get => DomainId.Combine(AppId, SchemaId); get => DomainId.Combine(AppId, SchemaId);

1
backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/SchemaUpdateCommand.cs

@ -14,7 +14,6 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Commands
{ {
public NamedId<DomainId> SchemaId { get; set; } public NamedId<DomainId> SchemaId { get; set; }
[IgnoreDataMember]
public override DomainId AggregateId public override DomainId AggregateId
{ {
get => DomainId.Combine(AppId, SchemaId.Id); get => DomainId.Combine(AppId, SchemaId.Id);

4
backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/SchemaDomainObject.State.cs

@ -5,7 +5,7 @@
// All rights reserved. Licensed under the MIT license. // 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;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Events.Schemas; using Squidex.Domain.Apps.Events.Schemas;
@ -28,7 +28,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject
public bool IsDeleted { get; set; } public bool IsDeleted { get; set; }
[IgnoreDataMember] [JsonIgnore]
public DomainId UniqueId public DomainId UniqueId
{ {
get => DomainId.Combine(AppId, Id); get => DomainId.Combine(AppId, Id);

4
backend/src/Squidex.Infrastructure.MongoDb/Caching/MongoDistributedCache.cs

@ -13,8 +13,8 @@ namespace Squidex.Infrastructure.Caching
{ {
public sealed class MongoDistributedCache : MongoRepositoryBase<MongoCacheEntry>, IDistributedCache public sealed class MongoDistributedCache : MongoRepositoryBase<MongoCacheEntry>, IDistributedCache
{ {
public MongoDistributedCache(IMongoDatabase database, bool setup = false) public MongoDistributedCache(IMongoDatabase database)
: base(database, setup) : base(database)
{ {
} }

2
backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEvent.cs

@ -16,13 +16,11 @@ namespace Squidex.Infrastructure.EventSourcing
[BsonRequired] [BsonRequired]
public string Type { get; set; } public string Type { get; set; }
[BsonJson]
[BsonRequired] [BsonRequired]
public string Payload { get; set; } public string Payload { get; set; }
[BsonElement("Metadata")] [BsonElement("Metadata")]
[BsonRequired] [BsonRequired]
[BsonJson]
public EnvelopeHeaders Headers { get; set; } public EnvelopeHeaders Headers { get; set; }
public static MongoEvent FromEventData(EventData data) public static MongoEvent FromEventData(EventData data)

4
backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs

@ -14,10 +14,6 @@ namespace Squidex.Infrastructure.EventSourcing
{ {
public partial class MongoEventStore : MongoRepositoryBase<MongoEventCommit>, IEventStore public partial class MongoEventStore : MongoRepositoryBase<MongoEventCommit>, IEventStore
{ {
private static readonly FieldDefinition<MongoEventCommit, BsonTimestamp> TimestampField = FieldBuilder.Build(x => x.Timestamp);
private static readonly FieldDefinition<MongoEventCommit, long> EventsCountField = FieldBuilder.Build(x => x.EventsCount);
private static readonly FieldDefinition<MongoEventCommit, long> EventStreamOffsetField = FieldBuilder.Build(x => x.EventStreamOffset);
private static readonly FieldDefinition<MongoEventCommit, string> EventStreamField = FieldBuilder.Build(x => x.EventStream);
private readonly IEventNotifier notifier; private readonly IEventNotifier notifier;
public IMongoCollection<BsonDocument> RawCollection public IMongoCollection<BsonDocument> RawCollection

20
backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Reader.cs

@ -47,10 +47,10 @@ namespace Squidex.Infrastructure.EventSourcing
using (Telemetry.Activities.StartActivity("MongoEventStore/QueryLatestAsync")) using (Telemetry.Activities.StartActivity("MongoEventStore/QueryLatestAsync"))
{ {
var filter = Filter.Eq(EventStreamField, streamName); var filter = Filter.Eq(x => x.EventStream, streamName);
var commits = 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); .ToListAsync(ct);
var result = commits.Select(x => x.Filtered()).Reverse().SelectMany(x => x).TakeLast(count).ToList(); var result = commits.Select(x => x.Filtered()).Reverse().SelectMany(x => x).TakeLast(count).ToList();
@ -68,11 +68,11 @@ namespace Squidex.Infrastructure.EventSourcing
{ {
var filter = var filter =
Filter.And( Filter.And(
Filter.Eq(EventStreamField, streamName), Filter.Eq(x => x.EventStream, streamName),
Filter.Gte(EventStreamOffsetField, streamPosition - MaxCommitSize)); Filter.Gte(x => x.EventStreamOffset, streamPosition - MaxCommitSize));
var commits = var commits =
await Collection.Find(filter).Sort(Sort.Ascending(TimestampField)) await Collection.Find(filter).Sort(Sort.Ascending(x => x.Timestamp))
.ToListAsync(ct); .ToListAsync(ct);
var result = commits.SelectMany(x => x.Filtered(streamPosition)).ToList(); var result = commits.SelectMany(x => x.Filtered(streamPosition)).ToList();
@ -92,11 +92,11 @@ namespace Squidex.Infrastructure.EventSourcing
var filter = var filter =
Filter.And( Filter.And(
Filter.In(EventStreamField, streamNames), Filter.In(x => x.EventStream, streamNames),
Filter.Gte(EventStreamOffsetField, position)); Filter.Gte(x => x.EventStreamOffset, position));
var commits = var commits =
await Collection.Find(filter).Sort(Sort.Ascending(TimestampField)) await Collection.Find(filter).Sort(Sort.Ascending(x => x.Timestamp))
.ToListAsync(ct); .ToListAsync(ct);
var result = commits.GroupBy(x => x.EventStream) var result = commits.GroupBy(x => x.EventStream)
@ -122,7 +122,7 @@ namespace Squidex.Infrastructure.EventSourcing
var find = var find =
Collection.Find(filterDefinition, Batching.Options) 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; var taken = 0;
@ -162,7 +162,7 @@ namespace Squidex.Infrastructure.EventSourcing
var find = var find =
Collection.Find(filterDefinition) 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; var taken = 0;

8
backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Writer.cs

@ -129,11 +129,11 @@ namespace Squidex.Infrastructure.EventSourcing
CancellationToken ct = default) CancellationToken ct = default)
{ {
var document = var document =
await Collection.Find(Filter.Eq(EventStreamField, streamName)) await Collection.Find(Filter.Eq(x => x.EventStream, streamName))
.Project<BsonDocument>(Projection .Project<BsonDocument>(Projection
.Include(EventStreamOffsetField) .Include(x => x.EventStreamOffset)
.Include(EventsCountField)) .Include(x => x.EventsCount))
.Sort(Sort.Descending(EventStreamOffsetField)).Limit(1) .Sort(Sort.Descending(x => x.EventStreamOffset)).Limit(1)
.FirstOrDefaultAsync(ct); .FirstOrDefaultAsync(ct);
if (document != null) if (document != null)

6
backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Batching.cs

@ -11,13 +11,9 @@ namespace Squidex.Infrastructure.MongoDb
{ {
public static class Batching public static class Batching
{ {
public const int BufferSize = 100;
public const int Size = BufferSize * 2;
public static readonly FindOptions Options = new FindOptions public static readonly FindOptions Options = new FindOptions
{ {
BatchSize = Size BatchSize = 200
}; };
} }
} }

6
backend/src/Squidex.Infrastructure.MongoDb/MongoDb/DomainIdSerializer.cs → backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonDomainIdSerializer.cs

@ -11,13 +11,13 @@ using MongoDB.Bson.Serialization.Serializers;
namespace Squidex.Infrastructure.MongoDb namespace Squidex.Infrastructure.MongoDb
{ {
public sealed class DomainIdSerializer : SerializerBase<DomainId>, IBsonPolymorphicSerializer, IRepresentationConfigurable<DomainIdSerializer> public sealed class BsonDomainIdSerializer : SerializerBase<DomainId>, IBsonPolymorphicSerializer, IRepresentationConfigurable<BsonDomainIdSerializer>
{ {
public static void Register() public static void Register()
{ {
try try
{ {
BsonSerializer.RegisterSerializer(new DomainIdSerializer()); BsonSerializer.RegisterSerializer(new BsonDomainIdSerializer());
} }
catch (BsonSerializationException) catch (BsonSerializationException)
{ {
@ -58,7 +58,7 @@ namespace Squidex.Infrastructure.MongoDb
context.Writer.WriteString(value.ToString()); context.Writer.WriteString(value.ToString());
} }
public DomainIdSerializer WithRepresentation(BsonType representation) public BsonDomainIdSerializer WithRepresentation(BsonType representation)
{ {
if (representation != BsonType.String) if (representation != BsonType.String)
{ {

26
backend/src/Squidex.Infrastructure.MongoDb/MongoDb/InstantSerializer.cs → backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonInstantSerializer.cs

@ -13,13 +13,13 @@ using NodaTime.Text;
namespace Squidex.Infrastructure.MongoDb namespace Squidex.Infrastructure.MongoDb
{ {
public sealed class InstantSerializer : SerializerBase<Instant>, IBsonPolymorphicSerializer, IRepresentationConfigurable<InstantSerializer> public sealed class BsonInstantSerializer : SerializerBase<Instant>, IBsonPolymorphicSerializer, IRepresentationConfigurable<BsonInstantSerializer>
{ {
public static void Register() public static void Register()
{ {
try try
{ {
BsonSerializer.RegisterSerializer(new InstantSerializer()); BsonSerializer.RegisterSerializer(new BsonInstantSerializer());
} }
catch (BsonSerializationException) catch (BsonSerializationException)
{ {
@ -34,7 +34,7 @@ namespace Squidex.Infrastructure.MongoDb
public BsonType Representation { get; } 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) if (representation != BsonType.DateTime && representation != BsonType.Int64 && representation != BsonType.String)
{ {
@ -56,9 +56,10 @@ namespace Squidex.Infrastructure.MongoDb
return Instant.FromUnixTimeMilliseconds(context.Reader.ReadInt64()); return Instant.FromUnixTimeMilliseconds(context.Reader.ReadInt64());
case BsonType.String: case BsonType.String:
return InstantPattern.ExtendedIso.Parse(context.Reader.ReadString()).Value; 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) public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, Instant value)
@ -67,21 +68,22 @@ namespace Squidex.Infrastructure.MongoDb
{ {
case BsonType.DateTime: case BsonType.DateTime:
context.Writer.WriteDateTime(value.ToUnixTimeMilliseconds()); context.Writer.WriteDateTime(value.ToUnixTimeMilliseconds());
return; break;
case BsonType.Int64: case BsonType.Int64:
context.Writer.WriteInt64(value.ToUnixTimeMilliseconds()); context.Writer.WriteInt64(value.ToUnixTimeMilliseconds());
return; break;
case BsonType.String: case BsonType.String:
context.Writer.WriteString(InstantPattern.ExtendedIso.Format(value)); 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) IBsonSerializer IRepresentationConfigurable.WithRepresentation(BsonType representation)

13
backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConvention.cs

@ -6,19 +6,26 @@
// ========================================================================== // ==========================================================================
using System.Reflection; using System.Reflection;
using System.Text.Json;
using MongoDB.Bson; using MongoDB.Bson;
using MongoDB.Bson.Serialization; using MongoDB.Bson.Serialization;
using MongoDB.Bson.Serialization.Conventions; using MongoDB.Bson.Serialization.Conventions;
using Newtonsoft.Json;
namespace Squidex.Infrastructure.MongoDb namespace Squidex.Infrastructure.MongoDb
{ {
public static class BsonJsonConvention 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 try
{ {
if (options != null)
{
Options = options;
}
var pack = new ConventionPack(); var pack = new ConventionPack();
pack.AddMemberMapConvention("JsonBson", memberMap => pack.AddMemberMapConvention("JsonBson", memberMap =>
@ -28,7 +35,7 @@ namespace Squidex.Infrastructure.MongoDb
if (attributes.OfType<BsonJsonAttribute>().Any()) if (attributes.OfType<BsonJsonAttribute>().Any())
{ {
var bsonSerializerType = typeof(BsonJsonSerializer<>).MakeGenericType(memberMap.MemberType); var bsonSerializerType = typeof(BsonJsonSerializer<>).MakeGenericType(memberMap.MemberType);
var bsonSerializer = Activator.CreateInstance(bsonSerializerType, serializer); var bsonSerializer = Activator.CreateInstance(bsonSerializerType);
memberMap.SetSerializer((IBsonSerializer)bsonSerializer!); memberMap.SetSerializer((IBsonSerializer)bsonSerializer!);
} }

105
backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonReader.cs

@ -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();
}
}
}

168
backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonSerializer.cs

@ -5,55 +5,187 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Text.Json;
using MongoDB.Bson; using MongoDB.Bson;
using MongoDB.Bson.IO;
using MongoDB.Bson.Serialization; using MongoDB.Bson.Serialization;
using MongoDB.Bson.Serialization.Serializers; using MongoDB.Bson.Serialization.Serializers;
using Newtonsoft.Json; using Squidex.Infrastructure.ObjectPool;
namespace Squidex.Infrastructure.MongoDb namespace Squidex.Infrastructure.MongoDb
{ {
public sealed class BsonJsonSerializer<T> : ClassSerializerBase<T?> where T : class public sealed class BsonJsonSerializer<T> : ClassSerializerBase<T?> where T : class
{ {
private readonly JsonSerializer serializer; public override T? Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
{
var bsonReader = context.Reader;
public BsonJsonSerializer(JsonSerializer serializer) if (bsonReader.GetCurrentBsonType() == BsonType.Null)
{ {
Guard.NotNull(serializer); bsonReader.ReadNull();
return null;
}
using var stream = DefaultPools.MemoryStream.GetStream();
this.serializer = serializer; using (var writer = new Utf8JsonWriter(stream))
{
FromBson(bsonReader, writer);
} }
public override T? Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) stream.Position = 0;
return JsonSerializer.Deserialize<T>(stream, BsonJsonConvention.Options);
}
private static void FromBson(IBsonReader reader, Utf8JsonWriter writer)
{ {
var bsonReader = context.Reader; void ReadDocument()
{
reader.ReadStartDocument();
writer.WriteStartObject();
if (bsonReader.GetCurrentBsonType() == BsonType.Null) while (reader.ReadBsonType() != BsonType.EndOfDocument)
{ {
bsonReader.ReadNull(); Read();
}
return null; writer.WriteEndObject();
reader.ReadEndDocument();
}
void ReadArray()
{
reader.ReadStartArray();
writer.WriteStartArray();
while (reader.ReadBsonType() != BsonType.EndOfDocument)
{
Read();
} }
else
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<T>(jsonReader); break;
case BsonReaderState.Done:
break;
case BsonReaderState.Closed:
break;
} }
} }
Read();
}
public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, T? value) public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, T? value)
{ {
var bsonWriter = context.Writer; 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)
{
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())
{ {
var jsonWriter = new BsonJsonWriter(bsonWriter); 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;
} }
} }
} }

91
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<JsonValue>
{
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<JsonArray>(reader);
case BsonType.Document:
return BsonSerializer.Deserialize<JsonObject>(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;
}
}
}
}

190
backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonWriter.cs

@ -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();
}
}
}

4
backend/src/Squidex.Infrastructure.MongoDb/MongoDb/TypeConverterStringSerializer.cs → backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonStringSerializer.cs

@ -12,7 +12,7 @@ using MongoDB.Bson.Serialization.Serializers;
namespace Squidex.Infrastructure.MongoDb namespace Squidex.Infrastructure.MongoDb
{ {
public sealed class TypeConverterStringSerializer<T> : SerializerBase<T> public sealed class BsonStringSerializer<T> : SerializerBase<T>
{ {
private readonly TypeConverter typeConverter = TypeDescriptor.GetConverter(typeof(T)); private readonly TypeConverter typeConverter = TypeDescriptor.GetConverter(typeof(T));
@ -20,7 +20,7 @@ namespace Squidex.Infrastructure.MongoDb
{ {
try try
{ {
BsonSerializer.RegisterSerializer(new TypeConverterStringSerializer<T>()); BsonSerializer.RegisterSerializer(new BsonStringSerializer<T>());
} }
catch (BsonSerializationException) catch (BsonSerializationException)
{ {

31
backend/src/Squidex.Infrastructure.MongoDb/MongoDb/FieldDefinitionBuilder.cs

@ -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<T>
{
public static readonly FieldDefinitionBuilder<T> Instance = new FieldDefinitionBuilder<T>();
private FieldDefinitionBuilder()
{
}
public FieldDefinition<T, TResult> Build<TResult>(Expression<Func<T, TResult>> expression)
{
return new ExpressionFieldDefinition<T, TResult>(expression);
}
public FieldDefinition<T, TResult> Build<TResult>(string name)
{
return new StringFieldDefinition<T, TResult>(name);
}
}
}

20
backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoBase.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using MongoDB.Bson;
using MongoDB.Driver; using MongoDB.Driver;
#pragma warning disable RECS0108 // Warns about static fields in generic types #pragma warning disable RECS0108 // Warns about static fields in generic types
@ -13,9 +14,6 @@ namespace Squidex.Infrastructure.MongoDb
{ {
public abstract class MongoBase<TEntity> public abstract class MongoBase<TEntity>
{ {
protected static readonly FieldDefinitionBuilder<TEntity> FieldBuilder =
FieldDefinitionBuilder<TEntity>.Instance;
protected static readonly FilterDefinitionBuilder<TEntity> Filter = protected static readonly FilterDefinitionBuilder<TEntity> Filter =
Builders<TEntity>.Filter; Builders<TEntity>.Filter;
@ -43,13 +41,19 @@ namespace Squidex.Infrastructure.MongoDb
protected static readonly UpdateOptions Upsert = protected static readonly UpdateOptions Upsert =
new UpdateOptions { IsUpsert = true }; new UpdateOptions { IsUpsert = true };
protected static readonly BsonDocument FindAll =
new BsonDocument();
static MongoBase() static MongoBase()
{ {
TypeConverterStringSerializer<RefToken>.Register(); BsonDomainIdSerializer.Register();
BsonInstantSerializer.Register();
InstantSerializer.Register(); BsonJsonConvention.Register();
BsonJsonValueSerializer.Register();
DomainIdSerializer.Register(); BsonStringSerializer<RefToken>.Register();
BsonStringSerializer<NamedId<DomainId>>.Register();
BsonStringSerializer<NamedId<Guid>>.Register();
BsonStringSerializer<NamedId<string>>.Register();
} }
} }
} }

28
backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs

@ -5,21 +5,27 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Globalization;
using MongoDB.Driver; using MongoDB.Driver;
using Squidex.Hosting; using Squidex.Hosting;
using Squidex.Hosting.Configuration; using Squidex.Hosting.Configuration;
namespace Squidex.Infrastructure.MongoDb namespace Squidex.Infrastructure.MongoDb
{ {
public abstract class MongoRepositoryBase<TEntity> : MongoBase<TEntity>, IInitializable public abstract class MongoRepositoryBase<T> : MongoBase<T>, IInitializable
{ {
private readonly IMongoDatabase mongoDatabase; private readonly IMongoDatabase mongoDatabase;
private IMongoCollection<TEntity> mongoCollection; private IMongoCollection<T> mongoCollection;
protected IMongoCollection<TEntity> Collection protected IMongoCollection<T> Collection
{ {
get get
{ {
if (mongoCollection == null)
{
InitializeAsync(default).Wait();
}
if (mongoCollection == null) if (mongoCollection == null)
{ {
ThrowHelper.InvalidOperationException("Collection has not been initialized yet."); ThrowHelper.InvalidOperationException("Collection has not been initialized yet.");
@ -35,16 +41,11 @@ namespace Squidex.Infrastructure.MongoDb
get => mongoDatabase; get => mongoDatabase;
} }
protected MongoRepositoryBase(IMongoDatabase database, bool setup = false) protected MongoRepositoryBase(IMongoDatabase database)
{ {
Guard.NotNull(database); Guard.NotNull(database);
mongoDatabase = database; mongoDatabase = database;
if (setup)
{
CreateCollection();
}
} }
protected virtual MongoCollectionSettings CollectionSettings() protected virtual MongoCollectionSettings CollectionSettings()
@ -52,9 +53,12 @@ namespace Squidex.Infrastructure.MongoDb
return new MongoCollectionSettings(); 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<TEntity> collection, protected virtual Task SetupCollectionAsync(IMongoCollection<T> collection,
CancellationToken ct) CancellationToken ct)
{ {
return Task.CompletedTask; return Task.CompletedTask;
@ -99,7 +103,7 @@ namespace Squidex.Infrastructure.MongoDb
private void CreateCollection() private void CreateCollection()
{ {
mongoCollection = mongoDatabase.GetCollection<TEntity>( mongoCollection = mongoDatabase.GetCollection<T>(
CollectionName(), CollectionName(),
CollectionSettings() ?? new MongoCollectionSettings()); CollectionSettings() ?? new MongoCollectionSettings());
} }

5
backend/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs

@ -6,14 +6,13 @@
// ========================================================================== // ==========================================================================
using MongoDB.Driver; using MongoDB.Driver;
using Newtonsoft.Json;
namespace Squidex.Infrastructure.States namespace Squidex.Infrastructure.States
{ {
public sealed class MongoSnapshotStore<T> : MongoSnapshotStoreBase<T, MongoState<T>> public sealed class MongoSnapshotStore<T> : MongoSnapshotStoreBase<T, MongoState<T>>
{ {
public MongoSnapshotStore(IMongoDatabase database, JsonSerializer serializer) public MongoSnapshotStore(IMongoDatabase database)
: base(database, serializer) : base(database)
{ {
} }
} }

16
backend/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStoreBase.cs

@ -8,27 +8,17 @@
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using MongoDB.Bson; using MongoDB.Bson;
using MongoDB.Driver; using MongoDB.Driver;
using Newtonsoft.Json;
using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.MongoDb;
namespace Squidex.Infrastructure.States namespace Squidex.Infrastructure.States
{ {
public abstract class MongoSnapshotStoreBase<T, TState> : MongoRepositoryBase<TState>, ISnapshotStore<T> where TState : MongoState<T>, new() public abstract class MongoSnapshotStoreBase<T, TState> : MongoRepositoryBase<TState>, ISnapshotStore<T> where TState : MongoState<T>, new()
{ {
protected MongoSnapshotStoreBase(IMongoDatabase database, JsonSerializer serializer) protected MongoSnapshotStoreBase(IMongoDatabase database)
: base(database, Register(serializer)) : base(database)
{ {
} }
private static bool Register(JsonSerializer serializer)
{
Guard.NotNull(serializer);
BsonJsonConvention.Register(serializer);
return true;
}
protected override string CollectionName() protected override string CollectionName()
{ {
var attribute = typeof(T).GetCustomAttributes(true).OfType<CollectionNameAttribute>().FirstOrDefault(); var attribute = typeof(T).GetCustomAttributes(true).OfType<CollectionNameAttribute>().FirstOrDefault();
@ -101,7 +91,7 @@ namespace Squidex.Infrastructure.States
{ {
using (Telemetry.Activities.StartActivity("MongoSnapshotStoreBase/ReadAllAsync")) 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)) await foreach (var document in find.ToAsyncEnumerable(ct))
{ {

2
backend/src/Squidex.Infrastructure/Collections/ReadonlyDictionary.cs

@ -11,9 +11,7 @@ namespace Squidex.Infrastructure.Collections
{ {
private static class Empties<TKey, TValue> where TKey : notnull private static class Empties<TKey, TValue> where TKey : notnull
{ {
#pragma warning disable SA1401 // Fields should be private
public static readonly ReadonlyDictionary<TKey, TValue> Instance = new ReadonlyDictionary<TKey, TValue>(); public static readonly ReadonlyDictionary<TKey, TValue> Instance = new ReadonlyDictionary<TKey, TValue>();
#pragma warning restore SA1401 // Fields should be private
} }
public static ReadonlyDictionary<TKey, TValue> Empty<TKey, TValue>() where TKey : notnull public static ReadonlyDictionary<TKey, TValue> Empty<TKey, TValue>() where TKey : notnull

2
backend/src/Squidex.Infrastructure/EventSourcing/DefaultEventFormatter.cs

@ -75,7 +75,7 @@ namespace Squidex.Infrastructure.EventSourcing
} }
var payloadType = typeNameRegistry.GetName(payloadValue.GetType()); var payloadType = typeNameRegistry.GetName(payloadValue.GetType());
var payloadJson = serializer.Serialize(envelope.Payload); var payloadJson = serializer.Serialize(envelope.Payload, envelope.Payload.GetType());
envelope.SetCommitId(commitId); envelope.SetCommitId(commitId);

6
backend/src/Squidex.Infrastructure/Json/IJsonSerializer.cs

@ -9,10 +9,14 @@ namespace Squidex.Infrastructure.Json
{ {
public interface IJsonSerializer public interface IJsonSerializer
{ {
string Serialize<T>(T value, bool intented = false); string Serialize<T>(T value, bool indented = false);
string Serialize(object? value, Type type, bool indented = false);
void Serialize<T>(T value, Stream stream, bool leaveOpen = false); void Serialize<T>(T value, Stream stream, bool leaveOpen = false);
void Serialize(object? value, Type type, Stream stream, bool leaveOpen = false);
T Deserialize<T>(string value, Type? actualType = null); T Deserialize<T>(string value, Type? actualType = null);
T Deserialize<T>(Stream stream, Type? actualType = null, bool leaveOpen = false); T Deserialize<T>(Stream stream, Type? actualType = null, bool leaveOpen = false);

98
backend/src/Squidex.Infrastructure/Json/Newtonsoft/ConverterContractResolver.cs

@ -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<Type, JsonConverter?> converterCache = new Dictionary<Type, JsonConverter?>();
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<Type, JsonConverter?>(cache)
: new Dictionary<Type, JsonConverter?>();
updatedCache[objectType] = result;
converterCache = updatedCache;
}
}
return result;
}
}
}

49
backend/src/Squidex.Infrastructure/Json/Newtonsoft/JsonClassConverter.cs

@ -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<T> : JsonConverter, ISupportedTypes where T : class
{
public virtual IEnumerable<Type> 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);
}
}
}

207
backend/src/Squidex.Infrastructure/Json/Newtonsoft/JsonValueConverter.cs

@ -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<Type> supportedTypes = new HashSet<Type>
{
typeof(JsonValue)
};
public virtual IEnumerable<Type> 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<string, JsonValue> 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);
}
}
}

98
backend/src/Squidex.Infrastructure/Json/Newtonsoft/NewtonsoftJsonSerializer.cs

@ -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>(T value, bool intented = false)
{
var formatting = intented ? Formatting.Indented : Formatting.None;
return JsonConvert.SerializeObject(value, formatting, settings);
}
public void Serialize<T>(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<T>(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<T>(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);
}
}
}

30
backend/src/Squidex.Infrastructure/Json/Newtonsoft/SurrogateConverter.cs

@ -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<T, TSurrogate> : JsonClassConverter<T> where T : class where TSurrogate : ISurrogate<T>, new()
{
protected override T? ReadValue(JsonReader reader, Type objectType, JsonSerializer serializer)
{
var surrogate = serializer.Deserialize<TSurrogate>(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);
}
}
}

50
backend/src/Squidex.Infrastructure/Json/Newtonsoft/TypeConverterJsonConverter.cs

@ -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<T> : JsonConverter, ISupportedTypes
{
private readonly TypeConverter typeConverter = TypeDescriptor.GetConverter(typeof(T));
public IEnumerable<Type> 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);
}
}
}

45
backend/src/Squidex.Infrastructure/Json/Newtonsoft/TypeNameSerializationBinder.cs

@ -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);
}
}
}
}

16
backend/src/Squidex.Infrastructure/Json/Newtonsoft/WriteonlyGeoJsonConverter.cs

@ -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;
}
}

54
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<T> : InheritanceConverterBase<T> 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;
}
}
}

95
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<T> : JsonConverter<T> 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();
}
}
}

72
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<SquidexJsonValue>
{
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<SquidexJsonObject>(ref reader, options);
case JsonTokenType.StartArray:
return JsonSerializer.Deserialize<SquidexJsonArray>(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;
}
}
}
}

69
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<TKey, TValue, TInstance> : JsonConverter<TInstance> where TKey : notnull
{
private readonly Type innerType = typeof(IReadOnlyDictionary<TKey, TValue>);
private readonly Func<IDictionary<TKey, TValue>, TInstance> creator;
public Converter()
{
creator = ReflectionHelper.CreateParameterizedConstructor<TInstance, IDictionary<TKey, TValue>>();
}
public override TInstance Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var inner = JsonSerializer.Deserialize<Dictionary<TKey, TValue>>(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<,>);
}
}
}

65
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<T, TInstance> : JsonConverter<TInstance>
{
private readonly Type innerType = typeof(IReadOnlyList<T>);
private readonly Func<IList<T>, TInstance> creator;
public Converter()
{
creator = ReflectionHelper.CreateParameterizedConstructor<TInstance, IList<T>>();
}
public override TInstance Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var inner = JsonSerializer.Deserialize<List<T>>(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<>);
}
}
}

46
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<TInput, TInstance> CreateParameterizedConstructor<TInstance, TInput>()
{
var method = CreateParameterizedConstructor(typeof(TInstance), typeof(TInput));
return method.CreateDelegate<Func<TInput, TInstance>>();
}
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;
}
}
}

62
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<T> : JsonConverter<T> where T : notnull
{
private readonly Func<string, T> convertFromString;
private readonly Func<T, string?> convertToString;
public StringConverter(Func<string, T> convertFromString, Func<T, string?>? 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)!);
}
}
}

31
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<T, TSurrogate> : JsonConverter<T> where T : class where TSurrogate : ISurrogate<T>, new()
{
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var surrogate = JsonSerializer.Deserialize<TSurrogate>(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);
}
}
}

108
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<T>(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<T>(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>(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>(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();
}
}
}
}
}

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save