From a805a26da6f6202ce3637647c5899489d11fc8f8 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Mon, 20 Jun 2022 18:42:37 +0200 Subject: [PATCH] Feature/performance round (#888) * Json improvements. * Performance improvements. * Fixes json handling performance. * Telemetry improvements. * Add missing files. --- .../Actions/Kafka/KafkaProducer.cs | 44 ++--- .../Validation/CompositeUniqueValidator.cs | 67 ++++---- .../CompositeUniqueValidatorFactory.cs | 4 +- .../Apps/Json/RolesSurrogate.cs | 18 +- .../Assets/AssetMetadata.cs | 16 +- .../Contents/Component.cs | 10 +- .../Contents/ContentFieldData.cs | 5 +- .../Contents/GeoJsonValue.cs | 16 +- .../Json/ContentFieldDataConverter.cs | 8 +- .../ConvertContent/FieldConverters.cs | 34 ++-- .../ConvertContent/StringFormatter.cs | 26 ++- .../ConvertContent/ValueConverters.cs | 12 +- .../DefaultValues/DefaultValueExtensions.cs | 2 +- .../ContentReferencesExtensions.cs | 51 ++---- .../ExtractReferenceIds/ReferencesCleaner.cs | 6 +- .../ReferencesExtractor.cs | 160 ++++++++---------- .../ValueReferencesConverter.cs | 2 +- .../HandleRules/RuleTypeProvider.cs | 13 +- .../Scripting/ContentWrapper/JsonMapper.cs | 28 +-- .../Tags/TagNormalizer.cs | 16 +- .../Extensions/ContentFluidExtension.cs | 28 +-- .../ValidateContent/ContentValidator.cs | 63 ++++--- .../DefaultFieldValueValidatorsFactory.cs | 6 +- .../DefaultValidatorsFactory.cs | 4 +- .../ValidateContent/IValidator.cs | 6 +- .../ValidateContent/IValidatorsFactory.cs | 6 +- .../ValidateContent/JsonValueConverter.cs | 88 ++++------ .../ValidateContent/JsonValueValidator.cs | 26 +-- .../ValidateContent/RootContext.cs | 79 +++++++++ .../ValidateContent/ValidationContext.cs | 46 ++--- .../ValidateContent/ValidatorContext.cs | 36 ---- .../Validators/AggregateValidator.cs | 25 ++- .../Validators/AllowedValuesValidator.cs | 6 +- .../Validators/AssetsValidator.cs | 72 ++++---- .../Validators/CollectionItemValidator.cs | 19 +-- .../Validators/CollectionValidator.cs | 16 +- .../Validators/ComponentValidator.cs | 4 +- .../Validators/FieldValidator.cs | 14 +- .../Validators/NoValueValidator.cs | 6 +- .../Validators/ObjectValidator.cs | 18 +- .../Validators/PatternValidator.cs | 10 +- .../Validators/RangeValidator.cs | 12 +- .../Validators/ReferencesValidator.cs | 23 ++- .../Validators/RequiredStringValidator.cs | 8 +- .../Validators/RequiredValidator.cs | 6 +- .../Validators/StringLengthValidator.cs | 12 +- .../Validators/StringTextValidator.cs | 20 +-- .../Validators/UniqueObjectValuesValidator.cs | 12 +- .../Validators/UniqueValidator.cs | 19 ++- .../Validators/UniqueValuesValidator.cs | 6 +- .../Assets/MongoAssetRepository.cs | 14 +- .../MongoContentRepository_SnapshotStore.cs | 5 +- .../Rules/MongoRuleRepository.cs | 2 +- .../Apps/AppUISettingsGrain.cs | 10 +- .../Apps/Indexes/AppsIndex.cs | 12 +- .../Assets/Queries/AssetEnricher.cs | 2 +- .../Contents/BackupContents.cs | 39 ++--- .../Guards/ValidationExtensions.cs | 18 +- .../GraphQL/GraphQLExecutionContext.cs | 8 +- .../Types/Contents/DataInputGraphType.cs | 2 +- .../GraphQL/Types/Contents/FieldVisitor.cs | 36 ++-- .../Contents/Queries/ContentEnricher.cs | 5 +- .../Contents/Queries/Steps/ConvertData.cs | 7 +- .../Contents/Queries/Steps/ResolveAssets.cs | 9 +- .../Queries/Steps/ResolveReferences.cs | 9 +- .../Contents/Queries/Steps/ScriptContent.cs | 5 +- .../Contents/Text/Extensions.cs | 14 +- .../Validation/DependencyValidatorsFactory.cs | 12 +- .../Rules/RuleDequeuerGrain.cs | 5 +- .../EventSourcing/MongoEventStore_Writer.cs | 4 +- .../States/MongoSnapshotStoreBase.cs | 10 +- .../CollectionExtensions.cs | 46 ++--- .../Collections/ListDictionary.cs | 7 +- .../DefaultEventDataFormatter.cs | 50 +++--- .../EventSourcing/EnvelopeExtensions.cs | 20 +-- .../Json/Newtonsoft/JsonValueConverter.cs | 122 +++++++------ .../Json/Objects/JsonObject.cs | 2 +- .../Json/Objects/JsonValue.cs | 34 +--- .../ActivityPropagationGrainCallFilter.cs | 118 +++++++++++++ .../Orleans/LoggingFilter.cs | 2 +- .../Queries/Json/ValueConverter.cs | 80 +++++---- .../Reflection/TypeNameRegistry.cs | 2 +- .../Squidex.Infrastructure.csproj | 3 - .../Squidex.Infrastructure/Tasks/Scheduler.cs | 123 ++++++++++++++ .../Models/BulkUpdateContentsJobDto.cs | 2 +- .../{JsonQueryDto.cs => QueryJsonDto.cs} | 2 +- .../Squidex/Config/Domain/AssetServices.cs | 2 +- .../Config/Domain/EventSourcingServices.cs | 2 +- .../Squidex/Config/Domain/StoreServices.cs | 17 +- .../Config/Domain/TelemetryServices.cs | 1 + .../Squidex/Config/Orleans/OrleansServices.cs | 4 +- backend/src/Squidex/Squidex.csproj | 3 + .../HandleRules/RuleEventFormatterTests.cs | 2 +- .../ValidateContent/AssetsFieldTests.cs | 2 +- .../ValidateContent/ContentValidationTests.cs | 4 +- .../ValidateContent/ReferencesFieldTests.cs | 2 +- .../ValidationTestExtensions.cs | 80 +++++---- .../Validators/ComponentValidatorTests.cs | 2 +- .../Validators/UniqueValidatorTests.cs | 17 +- .../Collections/ListDictionaryTests.cs | 12 ++ .../Tasks/SchedulerTests.cs | 85 ++++++++++ .../shared/forms/field-editor.component.html | 4 +- .../actions/formattable-input.component.html | 4 +- .../actions/generic-action.component.html | 2 +- 104 files changed, 1302 insertions(+), 1018 deletions(-) create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/RootContext.cs delete mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidatorContext.cs create mode 100644 backend/src/Squidex.Infrastructure/Orleans/ActivityPropagationGrainCallFilter.cs create mode 100644 backend/src/Squidex.Infrastructure/Tasks/Scheduler.cs rename backend/src/Squidex/Areas/Api/Controllers/{JsonQueryDto.cs => QueryJsonDto.cs} (90%) create mode 100644 backend/tests/Squidex.Infrastructure.Tests/Tasks/SchedulerTests.cs diff --git a/backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaProducer.cs b/backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaProducer.cs index d770dc901..0987e5234 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaProducer.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaProducer.cs @@ -175,21 +175,21 @@ namespace Squidex.Extensions.Actions.Kafka private static object GetValue(JsonValue value, Schema schema) { - switch (value.Type) + switch (value.Value) { - case JsonValueType.String when IsTypeOrUnionWith(schema, Schema.Type.String): - return value.AsString; - case JsonValueType.Number when IsTypeOrUnionWith(schema, Schema.Type.Long): - return (long)value.AsNumber; - case JsonValueType.Number when IsTypeOrUnionWith(schema, Schema.Type.Float): - return (float)value.AsNumber; - case JsonValueType.Number when IsTypeOrUnionWith(schema, Schema.Type.Int): - return (int)value.AsNumber; - case JsonValueType.Number when IsTypeOrUnionWith(schema, Schema.Type.Double): - return value.AsNumber; - case JsonValueType.Boolean when IsTypeOrUnionWith(schema, Schema.Type.Boolean): - return value.AsBoolean; - case JsonValueType.Object when IsTypeOrUnionWith(schema, Schema.Type.Map): + case bool b when IsTypeOrUnionWith(schema, Schema.Type.Boolean): + return b; + case double d when IsTypeOrUnionWith(schema, Schema.Type.Long): + return (long)d; + case double d when IsTypeOrUnionWith(schema, Schema.Type.Float): + return (float)d; + case double d when IsTypeOrUnionWith(schema, Schema.Type.Int): + return (int)d; + case double d when IsTypeOrUnionWith(schema, Schema.Type.Double): + return d; + case string s when IsTypeOrUnionWith(schema, Schema.Type.String): + return s; + case JsonObject o when IsTypeOrUnionWith(schema, Schema.Type.Map): { var mapResult = new Dictionary(); @@ -197,14 +197,14 @@ namespace Squidex.Extensions.Actions.Kafka { var map = (MapSchema)union.Schemas.FirstOrDefault(x => x.Tag == Schema.Type.Map); - foreach (var (key, childValue) in value.AsObject) + foreach (var (key, childValue) in o) { mapResult.Add(key, GetValue(childValue, map?.ValueSchema)); } } else if (schema is MapSchema map) { - foreach (var (key, childValue) in value.AsObject) + foreach (var (key, childValue) in o) { mapResult.Add(key, GetValue(childValue, map?.ValueSchema)); } @@ -213,7 +213,7 @@ namespace Squidex.Extensions.Actions.Kafka return mapResult; } - case JsonValueType.Object when IsTypeOrUnionWith(schema, Schema.Type.Record): + case JsonObject o when IsTypeOrUnionWith(schema, Schema.Type.Record): { GenericRecord result = null; @@ -223,7 +223,7 @@ namespace Squidex.Extensions.Actions.Kafka result = new GenericRecord(record); - foreach (var (key, childValue) in value.AsObject) + foreach (var (key, childValue) in o) { if (record != null && record.TryGetField(key, out var field)) { @@ -235,7 +235,7 @@ namespace Squidex.Extensions.Actions.Kafka { result = new GenericRecord(record); - foreach (var (key, childValue) in value.AsObject) + foreach (var (key, childValue) in o) { if (record.TryGetField(key, out var field)) { @@ -247,7 +247,7 @@ namespace Squidex.Extensions.Actions.Kafka return result; } - case JsonValueType.Array when IsTypeOrUnionWith(schema, Schema.Type.Array): + case JsonArray a when IsTypeOrUnionWith(schema, Schema.Type.Array): { var result = new List(); @@ -255,14 +255,14 @@ namespace Squidex.Extensions.Actions.Kafka { var arraySchema = (ArraySchema)union.Schemas.FirstOrDefault(x => x.Tag == Schema.Type.Array); - foreach (var item in value.AsArray) + foreach (var item in a) { result.Add(GetValue(item, arraySchema?.ItemSchema)); } } else if (schema is ArraySchema array) { - foreach (var item in value.AsArray) + foreach (var item in a) { result.Add(GetValue(item, array.ItemSchema)); } diff --git a/backend/extensions/Squidex.Extensions/Validation/CompositeUniqueValidator.cs b/backend/extensions/Squidex.Extensions/Validation/CompositeUniqueValidator.cs index 0f11299fb..777a4d9d0 100644 --- a/backend/extensions/Squidex.Extensions/Validation/CompositeUniqueValidator.cs +++ b/backend/extensions/Squidex.Extensions/Validation/CompositeUniqueValidator.cs @@ -27,34 +27,39 @@ namespace Squidex.Extensions.Validation this.contentRepository = contentRepository; } - public async ValueTask ValidateAsync(object value, ValidationContext context, AddError addError) + public void Validate(object value, ValidationContext context) { if (value is ContentData data) { - var validateableFields = context.Schema.Fields.Where(IsValidateableField); + context.Root.AddTask(async ct => await ValidateAsync(data, context)); + } + } - var filters = new List>(); + private async Task ValidateAsync(ContentData data, ValidationContext context) + { + var validateableFields = context.Root.Schema.Fields.Where(IsValidateableField); - foreach (var field in validateableFields) - { - var fieldValue = TryGetValue(field, data); + var filters = new List>(); - if (fieldValue != null) - { - filters.Add(ClrFilter.Eq($"data.{field.Name}.iv", fieldValue)); - } - } + foreach (var field in validateableFields) + { + var fieldValue = TryGetValue(field, data); - if (filters.Count > 0) + if (fieldValue != null) { - var filter = ClrFilter.And(filters); + filters.Add(ClrFilter.Eq($"data.{field.Name}.iv", fieldValue)); + } + } - var found = await contentRepository.QueryIdsAsync(context.AppId.Id, context.SchemaId.Id, filter); + if (filters.Count > 0) + { + var filter = ClrFilter.And(filters); - if (found.Any(x => x.Id != context.ContentId)) - { - addError(Enumerable.Empty(), "A content with the same values already exist."); - } + var found = await contentRepository.QueryIdsAsync(context.Root.AppId.Id, context.Root.SchemaId.Id, filter); + + if (found.Any(x => x.Id != context.Root.ContentId)) + { + context.AddError(Enumerable.Empty(), "A content with the same values already exist."); } } } @@ -73,24 +78,22 @@ namespace Squidex.Extensions.Validation switch (field.RawProperties) { - case BooleanFieldProperties when value.Type == JsonValueType.Boolean: - return value.AsBoolean; - case BooleanFieldProperties when value.Type == JsonValueType.Null: + case BooleanFieldProperties when value.Value is bool b: + return b; + case BooleanFieldProperties when value.Value == default: return ClrValue.Null; - case NumberFieldProperties when value.Type == JsonValueType.Number: - return value.AsNumber; - case NumberFieldProperties when value.Type == JsonValueType.Null: + case NumberFieldProperties when value.Value is double n: + return n; + case NumberFieldProperties when value.Value == default: return ClrValue.Null; - case StringFieldProperties when value.Type == JsonValueType.String: - return value.AsString; - case StringFieldProperties when value.Type == JsonValueType.Null: + case StringFieldProperties when value.Value is string s: + return s; + case StringFieldProperties when value.Value == default: return ClrValue.Null; - case ReferencesFieldProperties when value.Type == JsonValueType.Array: - var first = value.AsArray.FirstOrDefault(); - - if (first.Type == JsonValueType.String) + case ReferencesFieldProperties when value.Value is JsonArray a: + if (a.FirstOrDefault().Value is string first) { - return first.AsString; + return first; } break; diff --git a/backend/extensions/Squidex.Extensions/Validation/CompositeUniqueValidatorFactory.cs b/backend/extensions/Squidex.Extensions/Validation/CompositeUniqueValidatorFactory.cs index efb3e6b51..7be4c1991 100644 --- a/backend/extensions/Squidex.Extensions/Validation/CompositeUniqueValidatorFactory.cs +++ b/backend/extensions/Squidex.Extensions/Validation/CompositeUniqueValidatorFactory.cs @@ -20,9 +20,9 @@ namespace Squidex.Extensions.Validation this.contentRepository = contentRepository; } - public IEnumerable CreateContentValidators(ValidatorContext context, ValidatorFactory createFieldValidator) + public IEnumerable CreateContentValidators(ValidationContext context, ValidatorFactory createFieldValidator) { - foreach (var validatorTag in ValidatorTags(context.Schema.Properties.Tags)) + foreach (var validatorTag in ValidatorTags(context.Root.Schema.Properties.Tags)) { yield return new CompositeUniqueValidator(validatorTag, contentRepository); } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/RolesSurrogate.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/RolesSurrogate.cs index ce790f842..9a5b7a76b 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/RolesSurrogate.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/RolesSurrogate.cs @@ -47,25 +47,23 @@ namespace Squidex.Domain.Apps.Core.Apps.Json var properties = new JsonObject(); var permissions = PermissionSet.Empty; - if (value.Type == JsonValueType.Array) + if (value.Value is JsonArray a) { - var array = value.AsArray; - - if (array.Count > 0) + if (a.Count > 0) { - permissions = new PermissionSet(array.Where(x => x.Type == JsonValueType.String).Select(x => x.AsString)); + permissions = new PermissionSet(a.Select(x => x.Value).OfType()); } } - else if (value.Type == JsonValueType.Object) + else if (value.Value is JsonObject o) { - if (value.TryGetValue(JsonValueType.Array, "permissions", out var array)) + if (o.TryGetValue("permissions", out var found) && found.Value is JsonArray permissionArray) { - permissions = new PermissionSet(array.AsArray.Where(x => x.Type == JsonValueType.String).Select(x => x.AsString)); + permissions = new PermissionSet(permissionArray.Select(x => x.Value).OfType()); } - if (value.TryGetValue(JsonValueType.Object, "properties", out var obj)) + if (o.TryGetValue("properties", out found) && found.Value is JsonObject propertiesObject) { - properties = obj.AsObject; + properties = propertiesObject; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Assets/AssetMetadata.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Assets/AssetMetadata.cs index 7b5d346f4..eb7193367 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Assets/AssetMetadata.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Assets/AssetMetadata.cs @@ -95,9 +95,9 @@ namespace Squidex.Domain.Apps.Core.Assets public int? GetIn32(string name) { - if (TryGetValue(name, out var n) && n.Type == JsonValueType.Number) + if (TryGetValue(name, out var value) && value.Value is double n) { - return (int)n.AsNumber; + return (int)n; } return null; @@ -105,9 +105,9 @@ namespace Squidex.Domain.Apps.Core.Assets public float? GetSingle(string name) { - if (TryGetValue(name, out var n) && n.Type == JsonValueType.Number) + if (TryGetValue(name, out var value) && value.Value is double n) { - return (float)n.AsNumber; + return (float)n; } return null; @@ -115,9 +115,9 @@ namespace Squidex.Domain.Apps.Core.Assets public bool TryGetNumber(string name, out double result) { - if (TryGetValue(name, out var n) && n.Type == JsonValueType.Number) + if (TryGetValue(name, out var value) && value.Value is double n) { - result = n.AsNumber; + result = n; return true; } @@ -129,9 +129,9 @@ namespace Squidex.Domain.Apps.Core.Assets public bool TryGetString(string name, [MaybeNullWhen(false)] out string result) { - if (TryGetValue(name, out var s) && s.Type == JsonValueType.String) + if (TryGetValue(name, out var value) && value.Value is string s) { - result = s.AsString; + result = s; return true; } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Component.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Component.cs index d5e96ef7c..4dbb3bb0b 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Component.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Component.cs @@ -28,24 +28,22 @@ namespace Squidex.Domain.Apps.Core.Contents { discriminator = null!; - if (value.Type != JsonValueType.Object) + if (value.Value is not JsonObject o) { return false; } - if (!value.AsObject.TryGetValue(Discriminator, out var type) || type.Type != JsonValueType.String) + if (!o.TryGetValue(Discriminator, out var found) || found.Value is not string s) { return false; } - var typed = type.AsString; - - if (string.IsNullOrWhiteSpace(typed)) + if (string.IsNullOrWhiteSpace(s)) { return false; } - discriminator = typed; + discriminator = s; return true; } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/ContentFieldData.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/ContentFieldData.cs index c4f661de9..080ed3f37 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/ContentFieldData.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/ContentFieldData.cs @@ -7,12 +7,11 @@ using System.Diagnostics.CodeAnalysis; using Squidex.Infrastructure; -using Squidex.Infrastructure.Collections; using Squidex.Infrastructure.Json.Objects; namespace Squidex.Domain.Apps.Core.Contents { - public sealed class ContentFieldData : ListDictionary, IEquatable + public sealed class ContentFieldData : Dictionary, IEquatable { public ContentFieldData() : base(0, StringComparer.OrdinalIgnoreCase) @@ -37,7 +36,7 @@ namespace Squidex.Domain.Apps.Core.Contents { result = JsonValue.Null; - if (TryGetValue(key, out var found) && found.Type != JsonValueType.Null) + if (TryGetValue(key, out var found) && found != default) { result = found; return true; diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/GeoJsonValue.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/GeoJsonValue.cs index 7dc6852df..e005d0b5c 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/GeoJsonValue.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/GeoJsonValue.cs @@ -25,26 +25,24 @@ namespace Squidex.Domain.Apps.Core.Contents geoJSON = null; - if (value.Type == JsonValueType.Object) + if (value.Value is JsonObject o) { - var obj = value.AsObject; - - if (TryParseGeoJson(obj, serializer, out geoJSON)) + if (TryParseGeoJson(o, serializer, out geoJSON)) { return GeoJsonParseResult.Success; } - if (!obj.TryGetValue("latitude", out var lat) || lat.Type != JsonValueType.Number || !lat.AsNumber.IsBetween(-90, 90)) + if (!o.TryGetValue("latitude", out var found) || found.Value is not double lat || !lat.IsBetween(-90, 90)) { return GeoJsonParseResult.InvalidLatitude; } - if (!obj.TryGetValue("longitude", out var lon) || lon.Type != JsonValueType.Number || !lon.AsNumber.IsBetween(-180, 180)) + if (!o.TryGetValue("longitude", out found) || found.Value is not double lon || !lon.IsBetween(-180, 180)) { return GeoJsonParseResult.InvalidLongitude; } - geoJSON = new Point(new Position(lat.AsNumber, lon.AsNumber)); + geoJSON = new Point(new Position(lat, lon)); return GeoJsonParseResult.Success; } @@ -52,11 +50,11 @@ namespace Squidex.Domain.Apps.Core.Contents return GeoJsonParseResult.InvalidValue; } - private static bool TryParseGeoJson(ListDictionary obj, IJsonSerializer serializer, out GeoJSONObject? geoJSON) + private static bool TryParseGeoJson(JsonObject obj, IJsonSerializer serializer, out GeoJSONObject? geoJSON) { geoJSON = null; - if (!obj.TryGetValue("type", out var type) || type.Type != JsonValueType.String) + if (!obj.TryGetValue("type", out var type) || type.Value is not string) { return false; } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/ContentFieldDataConverter.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/ContentFieldDataConverter.cs index 8ea045638..dd259d6df 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/ContentFieldDataConverter.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/ContentFieldDataConverter.cs @@ -46,9 +46,13 @@ namespace Squidex.Domain.Apps.Core.Contents.Json var value = serializer.Deserialize(reader)!; - if (Language.IsDefault(propertyName) || propertyName == InvariantPartitioning.Key) + if (propertyName == InvariantPartitioning.Key) { - propertyName = string.Intern(propertyName); + propertyName = InvariantPartitioning.Key; + } + else if (Language.TryGetLanguage(propertyName, out var language)) + { + propertyName = language.Iso2Code; } result[propertyName] = value; diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/FieldConverters.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/FieldConverters.cs index 5e46d199f..ece3b30bb 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/FieldConverters.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/FieldConverters.cs @@ -34,7 +34,7 @@ namespace Squidex.Domain.Apps.Core.ConvertContent { foreach (var (_, value) in data) { - if (value.Type == JsonValueType.Null) + if (value.Value == default) { continue; } @@ -252,27 +252,25 @@ namespace Squidex.Domain.Apps.Core.ConvertContent private static JsonValue? ConvertArray(IArrayField field, JsonValue value, ValueConverter[] converters, ResolvedComponents components) { - if (value.Type == JsonValueType.Array) + if (value.Value is JsonArray a) { - var array = value.AsArray; - JsonArray? result = null; - for (int i = 0, j = 0; i < array.Count; i++, j++) + for (int i = 0, j = 0; i < a.Count; i++, j++) { - var oldValue = array[i]; + var oldValue = a[i]; var newValue = ConvertArrayItem(field, oldValue, converters, components); if (newValue == null) { - result ??= new JsonArray(array); + result ??= new JsonArray(a); result.RemoveAt(j); j--; } else if (!ReferenceEquals(newValue.Value.Value, oldValue.Value)) { - result ??= new JsonArray(array); + result ??= new JsonArray(a); result[j] = newValue.Value; } } @@ -286,27 +284,25 @@ namespace Squidex.Domain.Apps.Core.ConvertContent private static JsonValue? ConvertComponents(JsonValue? value, ValueConverter[] converters, ResolvedComponents components) { - if (value?.Type == JsonValueType.Array) + if (value?.Value is JsonArray a) { - var array = value.Value.AsArray; - JsonArray? result = null; - for (int i = 0, j = 0; i < array.Count; i++, j++) + for (int i = 0, j = 0; i < a.Count; i++, j++) { - var oldValue = array[i]; + var oldValue = a[i]; var newValue = ConvertComponent(oldValue, converters, components); if (newValue == null) { - result ??= new JsonArray(array); + result ??= new JsonArray(a); result.RemoveAt(j); j--; } - else if (!ReferenceEquals(newValue.Value.Value, array[i].Value)) + else if (!ReferenceEquals(newValue.Value.Value, a[i].Value)) { - result ??= new JsonArray(array); + result ??= new JsonArray(a); result[j] = newValue.Value; } } @@ -320,9 +316,9 @@ namespace Squidex.Domain.Apps.Core.ConvertContent private static JsonValue? ConvertComponent(JsonValue? value, ValueConverter[] converters, ResolvedComponents components) { - if (value.HasValue && value.Value.Type == JsonValueType.Object && value.Value.AsObject.TryGetValue(Component.Discriminator, out var type) && type.Type == JsonValueType.String) + if (value?.Value is JsonObject o && o.TryGetValue(Component.Discriminator, out var found) && found.Value is string s) { - var id = DomainId.Create(type.AsString); + var id = DomainId.Create(s); if (components.TryGetValue(id, out var schema)) { @@ -340,7 +336,7 @@ namespace Squidex.Domain.Apps.Core.ConvertContent private static JsonValue? ConvertArrayItem(IArrayField field, JsonValue value, ValueConverter[] converters, ResolvedComponents components) { - if (value.Type == JsonValueType.Object) + if (value.Value is JsonObject) { return ConvertNested(field.FieldCollection, value, field, converters, components); } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/StringFormatter.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/StringFormatter.cs index 12a51c5a5..4bcea3c5a 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/StringFormatter.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/StringFormatter.cs @@ -27,7 +27,7 @@ namespace Squidex.Domain.Apps.Core.ConvertContent { Guard.NotNull(field); - if (value.Type == JsonValueType.Null) + if (value == default) { return string.Empty; } @@ -49,7 +49,7 @@ namespace Squidex.Domain.Apps.Core.ConvertContent public string Visit(BooleanFieldProperties properties, Args args) { - if (args.Value.Type == JsonValueType.Boolean && args.Value.AsBoolean) + if (Equals(args.Value.Value, true)) { return "Yes"; } @@ -76,11 +76,11 @@ namespace Squidex.Domain.Apps.Core.ConvertContent public string Visit(GeolocationFieldProperties properties, Args args) { - if (args.Value.Type == JsonValueType.Object && - args.Value.TryGetValue(JsonValueType.Number, "latitude", out var lat) && - args.Value.TryGetValue(JsonValueType.Number, "longitude", out var lon)) + if (args.Value.Value is JsonObject o && + o.TryGetValue("latitude", out var found) && found.Value is double lat && + o.TryGetValue("longitude", out found) && found.Value is double lon) { - return $"{lat.AsNumber}, {lon.AsNumber}"; + return $"{lat}, {lon}"; } else { @@ -117,9 +117,9 @@ namespace Squidex.Domain.Apps.Core.ConvertContent public string Visit(TagsFieldProperties properties, Args args) { - if (args.Value.Type == JsonValueType.Array) + if (args.Value.Value is JsonArray a) { - return string.Join(", ", args.Value.AsArray); + return string.Join(", ", a); } else { @@ -134,15 +134,13 @@ namespace Squidex.Domain.Apps.Core.ConvertContent private static string FormatArray(JsonValue value, string singularName, string pluralName) { - if (value.Type == JsonValueType.Array) + if (value.Value is JsonArray a) { - var array = value.AsArray; - - if (array.Count > 1) + if (a.Count > 1) { - return $"{array.Count} {pluralName}"; + return $"{a.Count} {pluralName}"; } - else if (array.Count == 1) + else if (a.Count == 1) { return $"1 {singularName}"; } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ValueConverters.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ValueConverters.cs index 325919666..888290e13 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ValueConverters.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ValueConverters.cs @@ -30,7 +30,7 @@ namespace Squidex.Domain.Apps.Core.ConvertContent { return (value, field, parent) => { - if (value.Type == JsonValueType.Null) + if (value == default) { return value; } @@ -96,15 +96,11 @@ namespace Squidex.Domain.Apps.Core.ConvertContent return (value, field, parent) => { - if (field is IField && value.Type == JsonValueType.Array && shouldHandle(field, parent)) + if (field is IField && value.Value is JsonArray a && shouldHandle(field, parent)) { - var array = value.AsArray; - - for (var i = 0; i < array.Count; i++) + for (var i = 0; i < a.Count; i++) { - var id = array[i].ToString(); - - array[i] = urlGenerator.AssetContent(appId, id); + a[i] = urlGenerator.AssetContent(appId, a[i].ToString()); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/DefaultValues/DefaultValueExtensions.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/DefaultValues/DefaultValueExtensions.cs index 0a6729987..4bba3dad5 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/DefaultValues/DefaultValueExtensions.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/DefaultValues/DefaultValueExtensions.cs @@ -45,7 +45,7 @@ namespace Squidex.Domain.Apps.Core.DefaultValues { var defaultValue = DefaultValueFactory.CreateDefaultValue(field, SystemClock.Instance.GetCurrentInstant(), partitionKey); - if (field.RawProperties.IsRequired || defaultValue.Type == JsonValueType.Null) + if (field.RawProperties.IsRequired || defaultValue == default) { return; } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ContentReferencesExtensions.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ContentReferencesExtensions.cs index 2da1a8a1a..d91bdab68 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ContentReferencesExtensions.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ContentReferencesExtensions.cs @@ -24,14 +24,14 @@ namespace Squidex.Domain.Apps.Core.ExtractReferenceIds static bool CanHaveReference(JsonValue value) { - if (value.Type == JsonValueType.Array) + if (value.Value is JsonArray) { return true; } - if (value.Type == JsonValueType.Object) + if (value.Value is JsonObject o) { - foreach (var (_, nested) in value.AsObject) + foreach (var (_, nested) in o) { if (CanHaveReference(nested)) { @@ -60,61 +60,42 @@ namespace Squidex.Domain.Apps.Core.ExtractReferenceIds return false; } - public static HashSet GetReferencedIds(this ContentData source, Schema schema, - ResolvedComponents components, int referencesPerField = int.MaxValue) - { - Guard.NotNull(schema); - - var ids = new HashSet(); - - AddReferencedIds(source, schema, ids, components, referencesPerField); - - return ids; - } - public static void AddReferencedIds(this ContentData source, Schema schema, HashSet result, - ResolvedComponents components, int referencesPerField = int.MaxValue) + ResolvedComponents components, int take = int.MaxValue) { Guard.NotNull(schema); + Guard.NotNull(result); + Guard.NotNull(components); - AddReferencedIds(source, schema.Fields, result, components, referencesPerField); + ReferencesExtractor.Extract(schema.Fields, source, result, take, components); } public static void AddReferencedIds(this ContentData source, IEnumerable fields, HashSet result, - ResolvedComponents components, int referencesPerField = int.MaxValue) + ResolvedComponents components, int take = int.MaxValue) { Guard.NotNull(fields); Guard.NotNull(result); Guard.NotNull(components); - foreach (var field in fields) - { - AddReferencedIds(field, source, result, components, referencesPerField); - } + ReferencesExtractor.Extract(fields, source, result, take, components); } - private static void AddReferencedIds(IField field, ContentData source, HashSet result, - ResolvedComponents components, int referencesPerField = int.MaxValue) + public static void AddReferencedIds(this JsonValue value, IField field, HashSet result, + ResolvedComponents components, int take = int.MaxValue) { + Guard.NotNull(field); + Guard.NotNull(result); Guard.NotNull(components); - if (source.TryGetValue(field.Name, out var fieldData) && fieldData != null) - { - foreach (var partitionValue in fieldData) - { - ReferencesExtractor.Extract(field, partitionValue.Value, result, referencesPerField, components); - } - } + ReferencesExtractor.Extract(field, value, result, take, components); } public static HashSet GetReferencedIds(this IField field, JsonValue value, - ResolvedComponents components, int referencesPerField = int.MaxValue) + ResolvedComponents components, int take = int.MaxValue) { - Guard.NotNull(components); - var result = new HashSet(); - ReferencesExtractor.Extract(field, value, result, referencesPerField, components); + AddReferencedIds(value, field, result, components, take); return result; } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesCleaner.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesCleaner.cs index c9793e2d1..5042e73a4 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesCleaner.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesCleaner.cs @@ -97,10 +97,8 @@ namespace Squidex.Domain.Apps.Core.ExtractReferenceIds private static JsonValue CleanIds(Args args) { - if (args.Value.Type == JsonValueType.Array) + if (args.Value.Value is JsonArray array) { - var array = args.Value.AsArray; - var result = args.Value.AsArray; for (var i = 0; i < result.Count; i++) @@ -125,7 +123,7 @@ namespace Squidex.Domain.Apps.Core.ExtractReferenceIds private static bool IsValidReference(JsonValue item, Args args) { - return item.Type == JsonValueType.String && args.ValidIds.Contains(DomainId.Create(item.AsString)); + return item.Value is string s && args.ValidIds.Contains(DomainId.Create(s)); } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtractor.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtractor.cs index 7183fdcc6..64963d4df 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtractor.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtractor.cs @@ -14,143 +14,109 @@ using Squidex.Infrastructure.Json.Objects; namespace Squidex.Domain.Apps.Core.ExtractReferenceIds { - internal sealed class ReferencesExtractor : IFieldVisitor + internal static class ReferencesExtractor { - private static readonly ReferencesExtractor Instance = new ReferencesExtractor(); - public record struct Args(JsonValue Value, ISet Result, int Take, ResolvedComponents Components); - private ReferencesExtractor() - { - } - - public static None Extract(IField field, JsonValue value, HashSet result, int take, ResolvedComponents components) + public static void Extract(IField field, JsonValue value, HashSet result, int take, ResolvedComponents components) { var args = new Args(value, result, take, components); - return field.Accept(Instance, args); + ExtractCore(field, args); } - public None Visit(IArrayField field, Args args) + public static void Extract(IEnumerable schema, ContentData data, HashSet result, int take, ResolvedComponents components) { - if (args.Value.Type == JsonValueType.Array) + foreach (var field in schema) { - foreach (var value in args.Value.AsArray) - { - ExtractFromArrayItem(field, value, args); - } + Extract(field, data, result, take, components); } - - return None.Value; } - public None Visit(IField field, Args args) + public static void Extract(IField field, ContentData data, HashSet result, int take, ResolvedComponents components) { - AddIds(ref args); - - return None.Value; - } - - public None Visit(IField field, Args args) - { - AddIds(ref args); - - return None.Value; + if (CanHaveReferences(field.RawProperties) && data.TryGetValue(field.Name, out var fieldData) && fieldData != null) + { + foreach (var (_, value) in fieldData) + { + Extract(field, value, result, take, components); + } + } } - public None Visit(IField field, Args args) + private static void ExtractCore(IField field, Args args) { - return None.Value; + switch (field) + { + case IField: + AddIds(ref args); + break; + case IField: + AddIds(ref args); + break; + case IField: + ExtractFromComponent(args.Value, args); + break; + case IField: + ExtractFromComponents(args); + break; + case IArrayField arrayField: + ExtractFromArray(arrayField, args); + break; + } } - public None Visit(IField field, Args args) + private static void ExtractFromArray(IArrayField field, Args args) { - ExtractFromComponent(args.Value, args); - - return None.Value; + if (args.Value.Value is JsonArray a) + { + foreach (var value in a) + { + ExtractFromItem(field, value, args); + } + } } - public None Visit(IField field, Args args) + private static void ExtractFromComponents(Args args) { - if (args.Value.Type == JsonValueType.Array) + if (args.Value.Value is JsonArray a) { - foreach (var value in args.Value.AsArray) + foreach (var value in a) { ExtractFromComponent(value, args); } } - - return None.Value; - } - - public None Visit(IField field, Args args) - { - return None.Value; } - public None Visit(IField field, Args args) + private static void ExtractFromItem(IArrayField field, JsonValue value, Args args) { - return None.Value; - } - - public None Visit(IField field, Args args) - { - return None.Value; - } - - public None Visit(IField field, Args args) - { - return None.Value; - } - - public None Visit(IField field, Args args) - { - return None.Value; - } - - public None Visit(IField field, Args args) - { - return None.Value; - } - - public None Visit(IField field, Args args) - { - return None.Value; - } - - private void ExtractFromArrayItem(IArrayField field, JsonValue value, Args args) - { - if (value.Type == JsonValueType.Object) + if (value.Value is JsonObject o) { - var obj = value.AsObject; - foreach (var nestedField in field.Fields) { - if (obj.TryGetValue(nestedField.Name, out var nestedValue)) + if (CanHaveReferences(nestedField.RawProperties) && o.TryGetValue(nestedField.Name, out var nested)) { - nestedField.Accept(this, args with { Value = nestedValue }); + ExtractCore(nestedField, args with { Value = nested }); } } } } - private void ExtractFromComponent(JsonValue value, Args args) + private static void ExtractFromComponent(JsonValue value, Args args) { - if (value.Type == JsonValueType.Object) + if (value.Value is JsonObject o) { - var obj = value.AsObject; - - if (obj.TryGetValue(Component.Discriminator, out var type) && type.Type == JsonValueType.String) + if (o.TryGetValue(Component.Discriminator, out var found) && found.Value is string s) { - var id = DomainId.Create(type.AsString); + var id = DomainId.Create(s); if (args.Components.TryGetValue(id, out var schema)) { foreach (var componentField in schema.Fields) { - if (obj.TryGetValue(componentField.Name, out var componentValue)) + if (CanHaveReferences(componentField.RawProperties) && o.TryGetValue(componentField.Name, out var nested)) { - componentField.Accept(this, args with { Value = componentValue }); + ExtractCore(componentField, args with { Value = nested }); } } } @@ -158,17 +124,27 @@ namespace Squidex.Domain.Apps.Core.ExtractReferenceIds } } + private static bool CanHaveReferences(FieldProperties properties) + { + return + properties is ArrayFieldProperties || + properties is ReferencesFieldProperties || + properties is AssetsFieldProperties || + properties is ComponentFieldProperties || + properties is ComponentsFieldProperties; + } + private static void AddIds(ref Args args) { var added = 0; - if (args.Value.Type == JsonValueType.Array) + if (args.Value.Value is JsonArray a) { - foreach (var id in args.Value.AsArray) + foreach (var id in a) { - if (id.Type == JsonValueType.String) + if (id.Value is string s) { - args.Result.Add(DomainId.Create(id.AsString)); + args.Result.Add(DomainId.Create(s)); added++; diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ValueReferencesConverter.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ValueReferencesConverter.cs index aebd210db..b4d2e29b4 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ValueReferencesConverter.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ValueReferencesConverter.cs @@ -22,7 +22,7 @@ namespace Squidex.Domain.Apps.Core.ExtractReferenceIds return (value, field, parent) => { - if (value.Type == JsonValueType.Null) + if (value == default) { return value; } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTypeProvider.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTypeProvider.cs index a648bd0f7..b3339b39d 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTypeProvider.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTypeProvider.cs @@ -213,11 +213,14 @@ namespace Squidex.Domain.Apps.Core.HandleRules typeNameRegistry.Map(actionType.Type, actionType.Type.Name); } - var eventTypes = typeof(EnrichedEvent).Assembly.GetTypes().Where(x => typeof(EnrichedEvent).IsAssignableFrom(x) && !x.IsAbstract); - var addedTypes = new HashSet(); - foreach (var type in eventTypes) + static IEnumerable FindTypes(Type baseType) + { + return baseType.Assembly.GetTypes().Where(x => baseType.IsAssignableFrom(x) && !x.IsAbstract); + } + + foreach (var type in FindTypes(typeof(EnrichedEvent))) { if (addedTypes.Add(type)) { @@ -225,9 +228,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules } } - var triggerTypes = typeof(RuleTrigger).Assembly.GetTypes().Where(x => typeof(RuleTrigger).IsAssignableFrom(x) && !x.IsAbstract); - - foreach (var type in triggerTypes) + foreach (var type in FindTypes(typeof(RuleTrigger))) { if (addedTypes.Add(type)) { diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/JsonMapper.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/JsonMapper.cs index 7b5ab5b8d..446ba6d9a 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/JsonMapper.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/JsonMapper.cs @@ -19,27 +19,27 @@ namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper { public static JsValue Map(JsonValue value, Engine engine) { - switch (value.Type) + switch (value.Value) { - case JsonValueType.Null: + case null: return JsValue.Null; - case JsonValueType.String: - return new JsString(value.AsString); - case JsonValueType.Boolean: - return new JsBoolean(value.AsBoolean); - case JsonValueType.Number: - return new JsNumber(value.AsNumber); - case JsonValueType.Object: - return FromObject(value.AsObject, engine); - case JsonValueType.Array: - return FromArray(value.AsArray, engine); + case bool b: + return new JsBoolean(b); + case double n: + return new JsNumber(n); + case string s: + return new JsString(s); + case JsonObject o: + return FromObject(o, engine); + case JsonArray a: + return FromArray(a, engine); } ThrowInvalidType(nameof(value)); return JsValue.Null; } - private static JsValue FromArray(List arr, Engine engine) + private static JsValue FromArray(JsonArray arr, Engine engine) { var target = new JsValue[arr.Count]; @@ -51,7 +51,7 @@ namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper return engine.Realm.Intrinsics.Array.Construct(target); } - private static JsValue FromObject(ListDictionary obj, Engine engine) + private static JsValue FromObject(JsonObject obj, Engine engine) { var target = new ObjectInstance(engine); diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Tags/TagNormalizer.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Tags/TagNormalizer.cs index fd98e5258..fb6bf7dc3 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Tags/TagNormalizer.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Tags/TagNormalizer.cs @@ -110,13 +110,13 @@ namespace Squidex.Domain.Apps.Core.Tags { foreach (var partition in fieldData) { - if (partition.Value.Type == JsonValueType.Array) + if (partition.Value.Value is JsonArray a) { - foreach (var value in partition.Value.AsArray) + foreach (var value in a) { - if (value.Type == JsonValueType.Object) + if (value.Value is JsonObject o) { - if (value.AsObject.TryGetValue(nestedField.Name, out var nestedValue)) + if (o.TryGetValue(nestedField.Name, out var nestedValue)) { ExtractTags(nestedValue, values, arrays); } @@ -134,13 +134,13 @@ namespace Squidex.Domain.Apps.Core.Tags private static void ExtractTags(JsonValue value, ISet values, ICollection arrays) { - if (value.Type == JsonValueType.Array) + if (value.Value is JsonArray a) { - foreach (var item in value.AsArray) + foreach (var item in a) { - if (item.Type == JsonValueType.String) + if (item.Value is string s) { - values.Add(item.ToString()); + values.Add(s); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Templates/Extensions/ContentFluidExtension.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Templates/Extensions/ContentFluidExtension.cs index 4fb4df268..4e1ab3284 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Templates/Extensions/ContentFluidExtension.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Templates/Extensions/ContentFluidExtension.cs @@ -24,20 +24,20 @@ namespace Squidex.Domain.Apps.Core.Templates.Extensions FluidValue.SetTypeMapping(source => { - switch (source.Type) + switch (source.Value) { - case JsonValueType.Null: + case null: return FluidValue.Create(null); - case JsonValueType.Boolean: - return FluidValue.Create(source.AsBoolean); - case JsonValueType.Number: - return FluidValue.Create(source.AsNumber); - case JsonValueType.String: - return FluidValue.Create(source.AsString); - case JsonValueType.Array: - return new JsonArrayFluidValue(source.AsArray); - case JsonValueType.Object: - return new ObjectValue(source.AsObject); + case bool b: + return FluidValue.Create(b); + case double n: + return FluidValue.Create(n); + case string s: + return FluidValue.Create(s); + case JsonObject o: + return new ObjectValue(o); + case JsonArray a: + return new JsonArrayFluidValue(a); } ThrowHelper.InvalidOperationException(); @@ -46,9 +46,9 @@ namespace Squidex.Domain.Apps.Core.Templates.Extensions memberAccessStrategy.Register((value, name) => { - if (value.Type == JsonValueType.Object) + if (value.Value is JsonObject o) { - return value.AsObject.GetOrDefault(name); + return o.GetOrDefault(name); } return null; diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs index 3d0331753..1fc087241 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs @@ -5,8 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System.Collections.Concurrent; -using Microsoft.Extensions.Logging; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.ValidateContent.Validators; @@ -21,68 +19,81 @@ namespace Squidex.Domain.Apps.Core.ValidateContent private readonly PartitionResolver partitionResolver; private readonly ValidationContext context; private readonly IEnumerable factories; - private readonly ILogger log; - private readonly ConcurrentBag errors = new ConcurrentBag(); - public IReadOnlyCollection Errors + public IEnumerable Errors { - get => errors; + get => context.Root.Errors; } - public ContentValidator(PartitionResolver partitionResolver, ValidationContext context, IEnumerable factories, - ILogger log) + public ContentValidator(PartitionResolver partitionResolver, ValidationContext context, + IEnumerable factories) { Guard.NotNull(context); Guard.NotNull(factories); Guard.NotNull(partitionResolver); - Guard.NotNull(log); this.context = context; this.factories = factories; this.partitionResolver = partitionResolver; - - this.log = log; } - private void AddError(IEnumerable path, string message) + public ValueTask ValidateInputPartialAsync(ContentData data) { - var pathString = path.ToPathString(); + Guard.NotNull(data); + + ValidateInputCore(data, true); - errors.Add(new ValidationError(message, pathString)); + return context.Root.CompleteAsync(); } - public ValueTask ValidateInputPartialAsync(ContentData data) + public ValueTask ValidateInputAsync(ContentData data) { Guard.NotNull(data); - var validator = CreateSchemaValidator(true); + ValidateInputCore(data, false); - return validator.ValidateAsync(data, context, AddError); + return context.Root.CompleteAsync(); } - public ValueTask ValidateInputAsync(ContentData data) + public ValueTask ValidateInputAndContentAsync(ContentData data) { Guard.NotNull(data); - var validator = CreateSchemaValidator(false); + ValidateInputCore(data, false); + ValidateContentCore(data); - return validator.ValidateAsync(data, context, AddError); + return context.Root.CompleteAsync(); } public ValueTask ValidateContentAsync(ContentData data) { Guard.NotNull(data); - var validator = new AggregateValidator(CreateContentValidators(), log); + ValidateContentCore(data); + + return context.Root.CompleteAsync(); + } + + private void ValidateInputCore(ContentData data, bool partial) + { + CreateSchemaValidator(partial).Validate(data, context); + } - return validator.ValidateAsync(data, context, AddError); + private void ValidateContentCore(ContentData data) + { + CreatecSchemaValidator().Validate(data, context); + } + + private IValidator CreatecSchemaValidator() + { + return new AggregateValidator(CreateContentValidators()); } private IValidator CreateSchemaValidator(bool isPartial) { - var fieldValidators = new Dictionary(context.Schema.Fields.Count); + var fieldValidators = new Dictionary(context.Root.Schema.Fields.Count); - foreach (var field in context.Schema.Fields) + foreach (var field in context.Root.Schema.Fields) { fieldValidators[field.Name] = (!field.RawProperties.IsRequired, CreateFieldValidator(field, isPartial)); } @@ -109,12 +120,12 @@ namespace Squidex.Domain.Apps.Core.ValidateContent return new AggregateValidator( CreateFieldValidators(field) .Union(Enumerable.Repeat( - new ObjectValidator(partitioningValidators, isPartial, typeName), 1)), log); + new ObjectValidator(partitioningValidators, isPartial, typeName), 1))); } private IValidator CreateValueValidator(IField field) { - return new FieldValidator(new AggregateValidator(CreateValueValidators(field), log), field); + return new FieldValidator(new AggregateValidator(CreateValueValidators(field)), field); } private IEnumerable CreateContentValidators() diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/DefaultFieldValueValidatorsFactory.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/DefaultFieldValueValidatorsFactory.cs index 51ccda914..a01a1a1de 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/DefaultFieldValueValidatorsFactory.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/DefaultFieldValueValidatorsFactory.cs @@ -19,13 +19,13 @@ namespace Squidex.Domain.Apps.Core.ValidateContent { private static readonly DefaultFieldValueValidatorsFactory Instance = new DefaultFieldValueValidatorsFactory(); - public record struct Args(ValidatorContext Context, ValidatorFactory Factory); + public record struct Args(ValidationContext Context, ValidatorFactory Factory); private DefaultFieldValueValidatorsFactory() { } - public static IEnumerable CreateValidators(ValidatorContext context, IField field, ValidatorFactory factory) + public static IEnumerable CreateValidators(ValidationContext context, IField field, ValidatorFactory factory) { var args = new Args(context, factory); @@ -254,7 +254,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent } } - private static bool IsRequired(FieldProperties properties, ValidatorContext context) + private static bool IsRequired(FieldProperties properties, ValidationContext context) { var isRequired = properties.IsRequired; diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/DefaultValidatorsFactory.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/DefaultValidatorsFactory.cs index 8f5b55f50..c00e03e04 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/DefaultValidatorsFactory.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/DefaultValidatorsFactory.cs @@ -12,7 +12,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent { public sealed class DefaultValidatorsFactory : IValidatorsFactory { - public IEnumerable CreateFieldValidators(ValidatorContext context, IField field, ValidatorFactory factory) + public IEnumerable CreateFieldValidators(ValidationContext context, IField field, ValidatorFactory factory) { if (field is IField) { @@ -20,7 +20,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent } } - public IEnumerable CreateValueValidators(ValidatorContext context, IField field, ValidatorFactory factory) + public IEnumerable CreateValueValidators(ValidationContext context, IField field, ValidatorFactory factory) { return DefaultFieldValueValidatorsFactory.CreateValidators(context, field, factory); } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/IValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/IValidator.cs index c13481bce..dd84aefda 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/IValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/IValidator.cs @@ -5,14 +5,10 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -#pragma warning disable MA0048 // File name must match type name - namespace Squidex.Domain.Apps.Core.ValidateContent { - public delegate void AddError(IEnumerable path, string message); - public interface IValidator { - ValueTask ValidateAsync(object? value, ValidationContext context, AddError addError); + void Validate(object? value, ValidationContext context); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/IValidatorsFactory.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/IValidatorsFactory.cs index e96f1f797..9a2c9747b 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/IValidatorsFactory.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/IValidatorsFactory.cs @@ -15,17 +15,17 @@ namespace Squidex.Domain.Apps.Core.ValidateContent public interface IValidatorsFactory { - IEnumerable CreateFieldValidators(ValidatorContext context, IField field, ValidatorFactory factory) + IEnumerable CreateFieldValidators(ValidationContext context, IField field, ValidatorFactory factory) { yield break; } - IEnumerable CreateValueValidators(ValidatorContext context, IField field, ValidatorFactory factory) + IEnumerable CreateValueValidators(ValidationContext context, IField field, ValidatorFactory factory) { yield break; } - IEnumerable CreateContentValidators(ValidatorContext context, ValidatorFactory factory) + IEnumerable CreateContentValidators(ValidationContext context, ValidatorFactory factory) { yield break; } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs index 8d842016f..bc02a6cb0 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs @@ -76,9 +76,9 @@ namespace Squidex.Domain.Apps.Core.ValidateContent public (object? Result, JsonError? Error) Visit(IField field, Args args) { - if (args.Value.Type == JsonValueType.Boolean) + if (args.Value.Value is bool b) { - return (args.Value.AsBoolean, null); + return (b, null); } return (null, new JsonError(T.Get("contents.invalidBoolean"))); @@ -86,9 +86,9 @@ namespace Squidex.Domain.Apps.Core.ValidateContent public (object? Result, JsonError? Error) Visit(IField field, Args args) { - if (args.Value.Type == JsonValueType.Number) + if (args.Value.Value is double d) { - return (args.Value.AsNumber, null); + return (d, null); } return (null, new JsonError(T.Get("contents.invalidNumber"))); @@ -96,9 +96,9 @@ namespace Squidex.Domain.Apps.Core.ValidateContent public (object? Result, JsonError? Error) Visit(IField field, Args args) { - if (args.Value.Type == JsonValueType.String) + if (args.Value.Value is string s) { - return (args.Value.AsString, null); + return (s, null); } return (null, new JsonError(T.Get("contents.invalidString"))); @@ -111,9 +111,9 @@ namespace Squidex.Domain.Apps.Core.ValidateContent public (object? Result, JsonError? Error) Visit(IField field, Args args) { - if (args.Value.Type == JsonValueType.String) + if (args.Value.Value is string s) { - var parseResult = InstantPattern.ExtendedIso.Parse(args.Value.ToString()); + var parseResult = InstantPattern.ExtendedIso.Parse(s); if (!parseResult.Success) { @@ -145,21 +145,17 @@ namespace Squidex.Domain.Apps.Core.ValidateContent private static (object? Result, JsonError? Error) ConvertToIdList(JsonValue value) { - if (value.Type == JsonValueType.Array) + if (value.Value is JsonArray a) { - var array = value.AsArray; + var result = new List(a.Count); - var result = new List(array.Count); - - foreach (var item in array) + foreach (var item in a) { - if (item.Type == JsonValueType.String) + if (item.Value is string s) { - var typed = item.AsString; - - if (!string.IsNullOrWhiteSpace(item.AsString)) + if (!string.IsNullOrWhiteSpace(s)) { - result.Add(DomainId.Create(typed)); + result.Add(DomainId.Create(s)); continue; } } @@ -175,21 +171,17 @@ namespace Squidex.Domain.Apps.Core.ValidateContent private static (object? Result, JsonError? Error) ConvertToStringList(JsonValue value) { - if (value.Type == JsonValueType.Array) + if (value.Value is JsonArray a) { - var array = value.AsArray; + var result = new List(a.Count); - var result = new List(array.Count); - - foreach (var item in array) + foreach (var item in a) { - if (item.Type == JsonValueType.String) + if (item.Value is string s) { - var typed = item.AsString; - - if (!string.IsNullOrWhiteSpace(item.AsString)) + if (!string.IsNullOrWhiteSpace(s)) { - result.Add(typed); + result.Add(s); continue; } } @@ -205,17 +197,15 @@ namespace Squidex.Domain.Apps.Core.ValidateContent private static (object? Result, JsonError? Error) ConvertToObjectList(JsonValue value) { - if (value.Type == JsonValueType.Array) + if (value.Value is JsonArray a) { - var array = value.AsArray; + var result = new List(a.Count); - var result = new List(array.Count); - - foreach (var item in array) + foreach (var item in a) { - if (item.Type == JsonValueType.Object) + if (item.Value is JsonObject o) { - result.Add(item.AsObject); + result.Add(o); continue; } @@ -231,13 +221,11 @@ namespace Squidex.Domain.Apps.Core.ValidateContent private static (object? Result, JsonError? Error) ConvertToComponentList(JsonValue value, ResolvedComponents components, ReadonlyList? allowedIds) { - if (value.Type == JsonValueType.Array) + if (value.Value is JsonArray a) { - var array = value.AsArray; - - var result = new List(array.Count); + var result = new List(a.Count); - foreach (var item in array) + foreach (var item in a) { var (component, error) = ConvertToComponent(item, components, allowedIds); @@ -261,31 +249,29 @@ namespace Squidex.Domain.Apps.Core.ValidateContent private static (Component? Result, JsonError? Error) ConvertToComponent(JsonValue value, ResolvedComponents components, ReadonlyList? allowedIds) { - if (value.Type != JsonValueType.Object) + if (value.Value is not JsonObject o) { return (null, new JsonError(T.Get("contents.invalidComponentNoObject"))); } var id = DomainId.Empty; - var obj = value.AsObject; - - if (obj.TryGetValue("schemaName", out var schemaName) && schemaName.Type == JsonValueType.String) + if (o.TryGetValue("schemaName", out var found) && found.Value is string schemaName) { - id = components.FirstOrDefault(x => x.Value.Name == schemaName.AsString).Key; + id = components.FirstOrDefault(x => x.Value.Name == schemaName).Key; - obj.Remove("schemaName"); - obj[Component.Discriminator] = id; + o.Remove("schemaName"); + o[Component.Discriminator] = id; } - else if (obj.TryGetValue(Component.Discriminator, out var discriminator) && discriminator.Type == JsonValueType.String) + else if (o.TryGetValue(Component.Discriminator, out found) && found.Value is string discriminator) { - id = DomainId.Create(discriminator.AsString); + id = DomainId.Create(discriminator); } else if (allowedIds?.Count == 1) { id = allowedIds[0]; - obj[Component.Discriminator] = id; + o[Component.Discriminator] = id; } if (id == default) @@ -298,7 +284,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent return (null, new JsonError(T.Get("contents.invalidComponentUnknownSchema"))); } - var data = new JsonObject(obj); + var data = new JsonObject(o); data.Remove(Component.Discriminator); diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueValidator.cs index 7d9373fea..e24c71b20 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueValidator.cs @@ -48,7 +48,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent public bool Visit(IField field, Args args) { - return args.Value.Type == JsonValueType.Boolean; + return args.Value.Value is bool; } public bool Visit(IField field, Args args) @@ -63,9 +63,9 @@ namespace Squidex.Domain.Apps.Core.ValidateContent public bool Visit(IField field, Args args) { - if (args.Value.Type == JsonValueType.String) + if (args.Value.Value is string s) { - var parseResult = InstantPattern.ExtendedIso.Parse(args.Value.ToString()); + var parseResult = InstantPattern.ExtendedIso.Parse(s); return parseResult.Success; } @@ -87,7 +87,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent public bool Visit(IField field, Args args) { - return args.Value.Type == JsonValueType.Number; + return args.Value.Value is double; } public bool Visit(IField field, Args args) @@ -97,7 +97,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent public bool Visit(IField field, Args args) { - return args.Value.Type == JsonValueType.String; + return args.Value.Value is string; } public bool Visit(IField field, Args args) @@ -112,14 +112,14 @@ namespace Squidex.Domain.Apps.Core.ValidateContent private static bool IsValidStringList(JsonValue value) { - if (value.Type != JsonValueType.Array) + if (value.Value is not JsonArray a) { return false; } - foreach (var item in value.AsArray) + foreach (var item in a) { - if (item.Type != JsonValueType.String) + if (item.Value is not string) { return false; } @@ -130,14 +130,14 @@ namespace Squidex.Domain.Apps.Core.ValidateContent private static bool IsValidObjectList(JsonValue value) { - if (value.Type != JsonValueType.Array) + if (value.Value is not JsonArray a) { return false; } - foreach (var item in value.AsArray) + foreach (var item in a) { - if (item.Type != JsonValueType.Object) + if (item.Value is not JsonObject) { return false; } @@ -148,12 +148,12 @@ namespace Squidex.Domain.Apps.Core.ValidateContent private static bool IsValidComponentList(JsonValue value) { - if (value.Type != JsonValueType.Array) + if (value.Value is not JsonArray a) { return false; } - foreach (var item in value.AsArray) + foreach (var item in a) { if (!IsValidComponent(item)) { diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/RootContext.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/RootContext.cs new file mode 100644 index 000000000..37f42f480 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/RootContext.cs @@ -0,0 +1,79 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Concurrent; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json; +using Squidex.Infrastructure.Tasks; +using Squidex.Infrastructure.Validation; + +namespace Squidex.Domain.Apps.Core.ValidateContent +{ + public sealed class RootContext + { + private readonly ConcurrentBag errors = new ConcurrentBag(); + private readonly Scheduler scheduler = new Scheduler(); + + public IJsonSerializer JsonSerializer { get; } + + public DomainId ContentId { get; } + + public NamedId AppId { get; } + + public NamedId SchemaId { get; } + + public Schema Schema { get; } + + public ResolvedComponents Components { get; } + + public IEnumerable Errors + { + get => errors; + } + + public RootContext( + IJsonSerializer jsonSerializer, + NamedId appId, + NamedId schemaId, + Schema schema, + DomainId contentId, + ResolvedComponents components) + { + AppId = appId; + Components = components; + ContentId = contentId; + JsonSerializer = jsonSerializer; + Schema = schema; + SchemaId = schemaId; + } + + public void AddError(IEnumerable path, string message) + { + errors.Add(new ValidationError(message, path.ToPathString())); + } + + public void AddTask(SchedulerTask task) + { + scheduler.Schedule(task); + } + + public void ThrowOnErrors() + { + if (!errors.IsEmpty) + { + throw new ValidationException(errors.ToList()); + } + } + + public ValueTask CompleteAsync( + CancellationToken ct = default) + { + return scheduler.CompleteAsync(ct); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationContext.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationContext.cs index 26bde2390..6c8aeefba 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationContext.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationContext.cs @@ -6,37 +6,29 @@ // ========================================================================== using System.Collections.Immutable; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Json; namespace Squidex.Domain.Apps.Core.ValidateContent { - public sealed class ValidationContext : ValidatorContext + public sealed record ValidationContext { public ImmutableQueue Path { get; private set; } = ImmutableQueue.Empty; - public IJsonSerializer JsonSerializer { get; } + public bool IsOptional { get; init; } - public ResolvedComponents Components { get; } + public RootContext Root { get; } - public DomainId ContentId { get; } + public ValidationMode Mode { get; init; } - public bool IsOptional { get; private set; } + public ValidationAction Action { get; init; } - public ValidationContext( - IJsonSerializer jsonSerializer, - NamedId appId, - NamedId schemaId, - Schema schema, - ResolvedComponents components, - DomainId contentId) - : base(appId, schemaId, schema) + public ValidationContext(RootContext rootContext) { - JsonSerializer = jsonSerializer; + Root = rootContext; + } - Components = components; - ContentId = contentId; + public void AddError(IEnumerable path, string message) + { + Root.AddError(path, message); } public ValidationContext Optimized(bool optimized = true) @@ -56,7 +48,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent return this; } - return Clone(clone => clone.IsOptional = isOptional); + return this with { IsOptional = isOptional }; } public ValidationContext WithAction(ValidationAction action) @@ -66,7 +58,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent return this; } - return Clone(clone => clone.Action = action); + return this with { Action = action }; } public ValidationContext WithMode(ValidationMode mode) @@ -76,21 +68,17 @@ namespace Squidex.Domain.Apps.Core.ValidateContent return this; } - return Clone(clone => clone.Mode = mode); + return this with { Mode = mode }; } public ValidationContext Nested(string property) { - return Clone(clone => clone.Path = clone.Path.Enqueue(property)); + return this with { Path = Path.Enqueue(property) }; } - private ValidationContext Clone(Action updater) + public ValidationContext Nested(string property, bool isOptional) { - var clone = (ValidationContext)MemberwiseClone(); - - updater(clone); - - return clone; + return this with { Path = Path.Enqueue(property), IsOptional = isOptional }; } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidatorContext.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidatorContext.cs deleted file mode 100644 index 4bac3539e..000000000 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidatorContext.cs +++ /dev/null @@ -1,36 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Core.ValidateContent -{ - public abstract class ValidatorContext - { - public NamedId AppId { get; } - - public NamedId SchemaId { get; } - - public Schema Schema { get; } - - public ValidationMode Mode { get; protected set; } - - public ValidationAction Action { get; protected set; } - - protected ValidatorContext( - NamedId appId, - NamedId schemaId, - Schema schema) - { - AppId = appId; - - Schema = schema; - SchemaId = schemaId; - } - } -} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AggregateValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AggregateValidator.cs index f69ae4edd..d6e51a6b3 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AggregateValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AggregateValidator.cs @@ -5,8 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using Microsoft.Extensions.Logging; -using Squidex.Infrastructure.Tasks; using Squidex.Infrastructure.Translations; namespace Squidex.Domain.Apps.Core.ValidateContent.Validators @@ -14,30 +12,29 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators public sealed class AggregateValidator : IValidator { private readonly IValidator[]? validators; - private readonly ILogger log; - public AggregateValidator(IEnumerable? validators, - ILogger log) + public AggregateValidator(IEnumerable? validators) { this.validators = validators?.ToArray(); - - this.log = log; } - public async ValueTask ValidateAsync(object? value, ValidationContext context, AddError addError) + public void Validate(object? value, ValidationContext context) { + if (validators == null || validators.Length == 0) + { + return; + } + try { - if (validators?.Length > 0) + foreach (var validator in validators) { - await AsyncHelper.WhenAllThrottledAsync(validators, (x, _) => x.ValidateAsync(value, context, addError)); + validator.Validate(value, context); } } - catch (Exception ex) + catch { - log.LogError(ex, "Failed to validate fields."); - - addError(context.Path, T.Get("contents.validation.error")); + context.AddError(context.Path, T.Get("contents.validation.error")); } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AllowedValuesValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AllowedValuesValidator.cs index 70ad35f65..f92565b9c 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AllowedValuesValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AllowedValuesValidator.cs @@ -26,14 +26,12 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators this.allowedValues = allowedValues; } - public ValueTask ValidateAsync(object? value, ValidationContext context, AddError addError) + public void Validate(object? value, ValidationContext context) { if (value is TValue typedValue && !allowedValues.Contains(typedValue)) { - addError(context.Path, T.Get("contents.validation.notAllowed")); + context.AddError(context.Path, T.Get("contents.validation.notAllowed")); } - - return default; } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AssetsValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AssetsValidator.cs index da49495ca..0f2fd2c38 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AssetsValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AssetsValidator.cs @@ -44,128 +44,132 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators this.checkAssets = checkAssets; } - public async ValueTask ValidateAsync(object? value, ValidationContext context, AddError addError) + public void Validate(object? value, ValidationContext context) + { + context.Root.AddTask(ct => ValidateCoreAsync(value, context)); + } + + private async Task ValidateCoreAsync(object? value, ValidationContext context) { var foundIds = new List(); if (value is ICollection { Count: > 0 } assetIds) { var assets = await checkAssets(assetIds); - var index = 0; + var index = 1; foreach (var assetId in assetIds) { - index++; + var assetPath = context.Path.Enqueue($"[{index}]"); + var assetItem = assets.FirstOrDefault(x => x.AssetId == assetId); - var path = context.Path.Enqueue($"[{index}]"); - - var asset = assets.FirstOrDefault(x => x.AssetId == assetId); - - if (asset == null) + if (assetItem == null) { if (context.Action == ValidationAction.Upsert) { - addError(path, T.Get("contents.validation.assetNotFound", new { id = assetId })); + context.AddError(assetPath, T.Get("contents.validation.assetNotFound", new { id = assetId })); } continue; } - foundIds.Add(asset.AssetId); + foundIds.Add(assetItem.AssetId); - ValidateCommon(asset, path, addError); - ValidateType(asset, path, addError); + ValidateCommon(assetItem, assetPath, context); + ValidateType(assetItem, assetPath, context); - if (asset.Type == AssetType.Image) + if (assetItem.Type == AssetType.Image) { - var w = asset.Metadata.GetPixelWidth(); - var h = asset.Metadata.GetPixelHeight(); + var w = assetItem.Metadata.GetPixelWidth(); + var h = assetItem.Metadata.GetPixelHeight(); if (w != null && h != null) { - ValidateDimensions(w.Value, h.Value, path, addError); + ValidateDimensions(w.Value, h.Value, assetPath, context); } } - else if (asset.Type == AssetType.Video) + else if (assetItem.Type == AssetType.Video) { - var w = asset.Metadata.GetVideoWidth(); - var h = asset.Metadata.GetVideoHeight(); + var w = assetItem.Metadata.GetVideoWidth(); + var h = assetItem.Metadata.GetVideoHeight(); if (w != null && h != null) { - ValidateDimensions(w.Value, h.Value, path, addError); + ValidateDimensions(w.Value, h.Value, assetPath, context); } } + + index++; } } if (collectionValidator != null) { - await collectionValidator.ValidateAsync(foundIds, context, addError); + collectionValidator.Validate(foundIds, context); } if (uniqueValidator != null) { - await uniqueValidator.ValidateAsync(foundIds, context, addError); + uniqueValidator.Validate(foundIds, context); } } - private void ValidateCommon(IAssetInfo asset, ImmutableQueue path, AddError addError) + private void ValidateCommon(IAssetInfo asset, ImmutableQueue path, ValidationContext context) { if (properties.MinSize != null && asset.FileSize < properties.MinSize) { var min = properties.MinSize.Value.ToReadableSize(); - addError(path, T.Get("contents.validation.minimumSize", new { size = asset.FileSize.ToReadableSize(), min })); + context.AddError(path, T.Get("contents.validation.minimumSize", new { size = asset.FileSize.ToReadableSize(), min })); } if (properties.MaxSize != null && asset.FileSize > properties.MaxSize) { var max = properties.MaxSize.Value.ToReadableSize(); - addError(path, T.Get("contents.validation.maximumSize", new { size = asset.FileSize.ToReadableSize(), max })); + context.AddError(path, T.Get("contents.validation.maximumSize", new { size = asset.FileSize.ToReadableSize(), max })); } if (properties.AllowedExtensions != null && properties.AllowedExtensions.Count > 0 && !properties.AllowedExtensions.Any(x => asset.FileName.EndsWith("." + x, StringComparison.OrdinalIgnoreCase))) { - addError(path, T.Get("contents.validation.extension")); + context.AddError(path, T.Get("contents.validation.extension")); } } - private void ValidateType(IAssetInfo asset, ImmutableQueue path, AddError addError) + private void ValidateType(IAssetInfo asset, ImmutableQueue path, ValidationContext context) { var type = asset.MimeType == "image/svg+xml" ? AssetType.Image : asset.Type; if (properties.ExpectedType != null && properties.ExpectedType != type) { - addError(path, T.Get("contents.validation.assetType", new { type = properties.ExpectedType })); + context.AddError(path, T.Get("contents.validation.assetType", new { type = properties.ExpectedType })); } } - private void ValidateDimensions(int w, int h, ImmutableQueue path, AddError addError) + private void ValidateDimensions(int w, int h, ImmutableQueue path, ValidationContext context) { var actualRatio = (double)w / h; if (properties.MinWidth != null && w < properties.MinWidth) { - addError(path, T.Get("contents.validation.minimumWidth", new { width = w, min = properties.MinWidth })); + context.AddError(path, T.Get("contents.validation.minimumWidth", new { width = w, min = properties.MinWidth })); } if (properties.MaxWidth != null && w > properties.MaxWidth) { - addError(path, T.Get("contents.validation.maximumWidth", new { width = w, max = properties.MaxWidth })); + context.AddError(path, T.Get("contents.validation.maximumWidth", new { width = w, max = properties.MaxWidth })); } if (properties.MinHeight != null && h < properties.MinHeight) { - addError(path, T.Get("contents.validation.minimumHeight", new { height = h, min = properties.MinHeight })); + context.AddError(path, T.Get("contents.validation.minimumHeight", new { height = h, min = properties.MinHeight })); } if (properties.MaxHeight != null && h > properties.MaxHeight) { - addError(path, T.Get("contents.validation.maximumHeight", new { height = h, max = properties.MaxHeight })); + context.AddError(path, T.Get("contents.validation.maximumHeight", new { height = h, max = properties.MaxHeight })); } if (properties.AspectHeight != null && properties.AspectWidth != null) @@ -174,7 +178,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators if (Math.Abs(expectedRatio - actualRatio) > double.Epsilon) { - addError(path, T.Get("contents.validation.aspectRatio", new { width = properties.AspectWidth, height = properties.AspectHeight })); + context.AddError(path, T.Get("contents.validation.aspectRatio", new { width = properties.AspectWidth, height = properties.AspectHeight })); } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionItemValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionItemValidator.cs index 7d01ab6d8..3c32fc89a 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionItemValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionItemValidator.cs @@ -7,7 +7,6 @@ using System.Collections; using Squidex.Infrastructure; -using Squidex.Infrastructure.Tasks; namespace Squidex.Domain.Apps.Core.ValidateContent.Validators { @@ -22,21 +21,19 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators this.itemValidator = itemValidator; } - public async ValueTask ValidateAsync(object? value, ValidationContext context, AddError addError) + public void Validate(object? value, ValidationContext context) { if (value is ICollection { Count: > 0 } items) { - var targets = items.OfType().Select((item, index) => - { - var innerContext = context.Nested($"[{index + 1}]"); - - return (item, innerContext); - }); + var itemIndex = 1; - await AsyncHelper.WhenAllThrottledAsync(targets, async (x, _) => + foreach (var item in items) { - await itemValidator.ValidateAsync(x.item, x.innerContext, addError); - }); + var itemContext = context.Nested($"[{itemIndex}]"); + + itemValidator.Validate(item, itemContext); + itemIndex++; + } } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionValidator.cs index 4c13b52ac..ce8cf50f1 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionValidator.cs @@ -29,43 +29,41 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators this.maxItems = maxItems; } - public ValueTask ValidateAsync(object? value, ValidationContext context, AddError addError) + public void Validate(object? value, ValidationContext context) { if (value is not ICollection items || items.Count == 0) { if (isRequired && !context.IsOptional) { - addError(context.Path, T.Get("contents.validation.required")); + context.AddError(context.Path, T.Get("contents.validation.required")); } - return default; + return; } if (minItems != null && maxItems != null) { if (minItems == maxItems && minItems != items.Count) { - addError(context.Path, T.Get("contents.validation.itemCount", new { count = minItems })); + context.AddError(context.Path, T.Get("contents.validation.itemCount", new { count = minItems })); } else if (items.Count < minItems || items.Count > maxItems) { - addError(context.Path, T.Get("contents.validation.itemCountBetween", new { min = minItems, max = maxItems })); + context.AddError(context.Path, T.Get("contents.validation.itemCountBetween", new { min = minItems, max = maxItems })); } } else { if (minItems != null && items.Count < minItems) { - addError(context.Path, T.Get("contents.validation.minItems", new { min = minItems })); + context.AddError(context.Path, T.Get("contents.validation.minItems", new { min = minItems })); } if (maxItems != null && items.Count > maxItems) { - addError(context.Path, T.Get("contents.validation.maxItems", new { max = maxItems })); + context.AddError(context.Path, T.Get("contents.validation.maxItems", new { max = maxItems })); } } - - return default; } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ComponentValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ComponentValidator.cs index 74f833ea0..5a564dee7 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ComponentValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ComponentValidator.cs @@ -19,7 +19,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators this.validatorFactory = validatorFactory; } - public async ValueTask ValidateAsync(object? value, ValidationContext context, AddError addError) + public void Validate(object? value, ValidationContext context) { if (value is Component component) { @@ -27,7 +27,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators if (validator != null) { - await validator.ValidateAsync(component.Data, context, addError); + validator.Validate(component.Data, context); } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/FieldValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/FieldValidator.cs index 5647aa973..e1c37616c 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/FieldValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/FieldValidator.cs @@ -26,7 +26,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators this.validator = validator; } - public async ValueTask ValidateAsync(object? value, ValidationContext context, AddError addError) + public void Validate(object? value, ValidationContext context) { var typedValue = value; @@ -34,7 +34,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators { if (value is JsonValue jsonValue) { - if (jsonValue.Type == JsonValueType.Null) + if (jsonValue == default) { typedValue = null; } @@ -42,11 +42,13 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators { typedValue = jsonValue.Value; - var (json, error) = JsonValueConverter.ConvertValue(field, jsonValue, context.JsonSerializer, context.Components); + var (json, error) = JsonValueConverter.ConvertValue(field, jsonValue, + context.Root.JsonSerializer, + context.Root.Components); if (error != null) { - addError(context.Path, error.Error); + context.AddError(context.Path, error.Error); } else { @@ -57,11 +59,11 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators } catch { - addError(context.Path, T.Get("contents.validation.invalid")); + context.AddError(context.Path, T.Get("contents.validation.invalid")); return; } - await validator.ValidateAsync(typedValue, context, addError); + validator.Validate(typedValue, context); } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/NoValueValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/NoValueValidator.cs index c5a460ee3..a20f4cd31 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/NoValueValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/NoValueValidator.cs @@ -17,14 +17,12 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators { } - public ValueTask ValidateAsync(object? value, ValidationContext context, AddError addError) + public void Validate(object? value, ValidationContext context) { if (!value.IsUndefined()) { - addError(context.Path, T.Get("contents.validation.mustBeEmpty")); + context.AddError(context.Path, T.Get("contents.validation.mustBeEmpty")); } - - return default; } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ObjectValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ObjectValidator.cs index 9751fdc4c..a96f576e6 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ObjectValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ObjectValidator.cs @@ -5,7 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using Squidex.Infrastructure.Tasks; using Squidex.Infrastructure.Translations; namespace Squidex.Domain.Apps.Core.ValidateContent.Validators @@ -21,11 +20,10 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators { this.fields = fields; this.fieldType = fieldType; - this.isPartial = isPartial; } - public async ValueTask ValidateAsync(object? value, ValidationContext context, AddError addError) + public void Validate(object? value, ValidationContext context) { if (value.IsNullOrUndefined()) { @@ -40,17 +38,15 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators if (!fields.ContainsKey(name)) { - addError(context.Path.Enqueue(name), T.Get("contents.validation.unknownField", new { fieldType })); + context.AddError(context.Path.Enqueue(name), T.Get("contents.validation.unknownField", new { fieldType })); } } - await AsyncHelper.WhenAllThrottledAsync(fields, async (kvp, _) => + foreach (var (name, field) in fields) { - var (isOptional, validator) = kvp.Value; - var fieldValue = Undefined.Value; - if (!values.TryGetValue(kvp.Key, out var nestedValue)) + if (!values.TryGetValue(name, out var nestedValue)) { if (isPartial) { @@ -62,10 +58,10 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators fieldValue = nestedValue!; } - var fieldContext = context.Nested(kvp.Key).Optional(isOptional); + var fieldContext = context.Nested(name, field.IsOptional); - await validator.ValidateAsync(fieldValue, fieldContext, addError); - }); + field.Validator.Validate(fieldValue, fieldContext); + } } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/PatternValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/PatternValidator.cs index 301a8931f..9bfb1e9b6 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/PatternValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/PatternValidator.cs @@ -33,7 +33,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators regex = new Regex($"^{pattern}$", options, Timeout); } - public ValueTask ValidateAsync(object? value, ValidationContext context, AddError addError) + public void Validate(object? value, ValidationContext context) { if (value is string stringValue) { @@ -45,22 +45,20 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators { if (string.IsNullOrWhiteSpace(errorMessage)) { - addError(context.Path, T.Get("contents.validation.pattern")); + context.AddError(context.Path, T.Get("contents.validation.pattern")); } else { - addError(context.Path, errorMessage); + context.AddError(context.Path, errorMessage); } } } catch { - addError(context.Path, T.Get("contents.validation.regexTooSlow")); + context.AddError(context.Path, T.Get("contents.validation.regexTooSlow")); } } } - - return default; } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RangeValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RangeValidator.cs index c73013713..4f16f9d24 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RangeValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RangeValidator.cs @@ -26,7 +26,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators this.max = max; } - public ValueTask ValidateAsync(object? value, ValidationContext context, AddError addError) + public void Validate(object? value, ValidationContext context) { if (value is TValue typedValue) { @@ -34,28 +34,26 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators { if (Equals(min, max) && Equals(min.Value, max.Value)) { - addError(context.Path, T.Get("contents.validation.exactValue", new { value = max.Value })); + context.AddError(context.Path, T.Get("contents.validation.exactValue", new { value = max.Value })); } else if (typedValue.CompareTo(min.Value) < 0 || typedValue.CompareTo(max.Value) > 0) { - addError(context.Path, T.Get("contents.validation.between", new { min, max })); + context.AddError(context.Path, T.Get("contents.validation.between", new { min, max })); } } else { if (min != null && typedValue.CompareTo(min.Value) < 0) { - addError(context.Path, T.Get("contents.validation.min", new { min })); + context.AddError(context.Path, T.Get("contents.validation.min", new { min })); } if (max != null && typedValue.CompareTo(max.Value) > 0) { - addError(context.Path, T.Get("contents.validation.max", new { max })); + context.AddError(context.Path, T.Get("contents.validation.max", new { max })); } } } - - return default; } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ReferencesValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ReferencesValidator.cs index db7378e2d..d36db17a4 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ReferencesValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ReferencesValidator.cs @@ -43,20 +43,23 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators this.checkReferences = checkReferences; } - public async ValueTask ValidateAsync(object? value, ValidationContext context, AddError addError) + public void Validate(object? value, ValidationContext context) + { + context.Root.AddTask(ct => ValidateCoreAsync(value, context)); + } + + private async Task ValidateCoreAsync(object? value, ValidationContext context) { var foundIds = new List(); if (value is ICollection { Count: > 0 } contentIds) { var references = await checkReferences(contentIds.ToHashSet()); - var index = 0; + var referenceIndex = 1; foreach (var id in contentIds) { - index++; - - var path = context.Path.Enqueue($"[{index}]"); + var path = context.Path.Enqueue($"[{referenceIndex}]"); var (schemaId, _, status) = references.FirstOrDefault(x => x.Id == id); @@ -64,7 +67,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators { if (context.Action == ValidationAction.Upsert) { - addError(path, T.Get("contents.validation.referenceNotFound", new { id })); + context.AddError(path, T.Get("contents.validation.referenceNotFound", new { id })); } continue; @@ -76,7 +79,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators { if (context.Action == ValidationAction.Upsert) { - addError(path, T.Get("contents.validation.referenceToInvalidSchema", new { id })); + context.AddError(path, T.Get("contents.validation.referenceToInvalidSchema", new { id })); } isValid = false; @@ -88,17 +91,19 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators { foundIds.Add(id); } + + referenceIndex++; } } if (collectionValidator != null) { - await collectionValidator.ValidateAsync(foundIds, context, addError); + collectionValidator.Validate(foundIds, context); } if (uniqueValidator != null) { - await uniqueValidator.ValidateAsync(foundIds, context, addError); + uniqueValidator.Validate(foundIds, context); } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredStringValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredStringValidator.cs index d73d53e55..39c827550 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredStringValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredStringValidator.cs @@ -18,19 +18,17 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators this.validateEmptyStrings = validateEmptyStrings; } - public ValueTask ValidateAsync(object? value, ValidationContext context, AddError addError) + public void Validate(object? value, ValidationContext context) { if (context.IsOptional) { - return default; + return; } if (value.IsNullOrUndefined() || IsEmptyString(value)) { - addError(context.Path, T.Get("contents.validation.required")); + context.AddError(context.Path, T.Get("contents.validation.required")); } - - return default; } private bool IsEmptyString(object? value) diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredValidator.cs index b459860b8..2f7a2dc4a 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredValidator.cs @@ -11,14 +11,12 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators { public class RequiredValidator : IValidator { - public ValueTask ValidateAsync(object? value, ValidationContext context, AddError addError) + public void Validate(object? value, ValidationContext context) { if (value.IsNullOrUndefined() && !context.IsOptional) { - addError(context.Path, T.Get("contents.validation.required")); + context.AddError(context.Path, T.Get("contents.validation.required")); } - - return default; } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringLengthValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringLengthValidator.cs index 1a068cd57..36e94a0ce 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringLengthValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringLengthValidator.cs @@ -26,7 +26,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators this.maxLength = maxLength; } - public ValueTask ValidateAsync(object? value, ValidationContext context, AddError addError) + public void Validate(object? value, ValidationContext context) { if (value is string stringValue && !string.IsNullOrEmpty(stringValue)) { @@ -34,28 +34,26 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators { if (minLength == maxLength && minLength != stringValue.Length) { - addError(context.Path, T.Get("contents.validation.characterCount", new { count = minLength })); + context.AddError(context.Path, T.Get("contents.validation.characterCount", new { count = minLength })); } else if (stringValue.Length < minLength || stringValue.Length > maxLength) { - addError(context.Path, T.Get("contents.validation.charactersBetween", new { min = minLength, max = maxLength })); + context.AddError(context.Path, T.Get("contents.validation.charactersBetween", new { min = minLength, max = maxLength })); } } else { if (minLength != null && stringValue.Length < minLength) { - addError(context.Path, T.Get("contents.validation.minLength", new { min = minLength })); + context.AddError(context.Path, T.Get("contents.validation.minLength", new { min = minLength })); } if (maxLength != null && stringValue.Length > maxLength) { - addError(context.Path, T.Get("contents.validation.maxLength", new { max = maxLength })); + context.AddError(context.Path, T.Get("contents.validation.maxLength", new { max = maxLength })); } } } - - return default; } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringTextValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringTextValidator.cs index fa9d95d4d..9bc90e2eb 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringTextValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringTextValidator.cs @@ -42,7 +42,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators this.maxWords = maxWords; } - public ValueTask ValidateAsync(object? value, ValidationContext context, AddError addError) + public void Validate(object? value, ValidationContext context) { if (value is string stringValue && !string.IsNullOrEmpty(stringValue)) { @@ -59,23 +59,23 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators { if (minWords == maxWords && minWords != words) { - addError(context.Path, T.Get("contents.validation.wordCount", new { count = minWords })); + context.AddError(context.Path, T.Get("contents.validation.wordCount", new { count = minWords })); } else if (words < minWords || words > maxWords) { - addError(context.Path, T.Get("contents.validation.wordsBetween", new { min = minWords, max = maxWords })); + context.AddError(context.Path, T.Get("contents.validation.wordsBetween", new { min = minWords, max = maxWords })); } } else { if (words < minWords) { - addError(context.Path, T.Get("contents.validation.minWords", new { min = minWords })); + context.AddError(context.Path, T.Get("contents.validation.minWords", new { min = minWords })); } if (words > maxWords) { - addError(context.Path, T.Get("contents.validation.maxWords", new { max = maxWords })); + context.AddError(context.Path, T.Get("contents.validation.maxWords", new { max = maxWords })); } } } @@ -88,29 +88,27 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators { if (minCharacters == maxCharacters && minCharacters != characters) { - addError(context.Path, T.Get("contents.validation.normalCharacterCount", new { count = minCharacters })); + context.AddError(context.Path, T.Get("contents.validation.normalCharacterCount", new { count = minCharacters })); } else if (characters < minCharacters || characters > maxCharacters) { - addError(context.Path, T.Get("contents.validation.normalCharactersBetween", new { min = minCharacters, max = maxCharacters })); + context.AddError(context.Path, T.Get("contents.validation.normalCharactersBetween", new { min = minCharacters, max = maxCharacters })); } } else { if (characters < minCharacters) { - addError(context.Path, T.Get("contents.validation.minNormalCharacters", new { min = minCharacters })); + context.AddError(context.Path, T.Get("contents.validation.minNormalCharacters", new { min = minCharacters })); } if (characters > maxCharacters) { - addError(context.Path, T.Get("contents.validation.maxCharacters", new { max = maxCharacters })); + context.AddError(context.Path, T.Get("contents.validation.maxCharacters", new { max = maxCharacters })); } } } } - - return default; } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueObjectValuesValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueObjectValuesValidator.cs index dddfcc3d1..9f3b258c9 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueObjectValuesValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueObjectValuesValidator.cs @@ -20,21 +20,19 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators this.fields = fields; } - public ValueTask ValidateAsync(object? value, ValidationContext context, AddError addError) + public void Validate(object? value, ValidationContext context) { if (value is IEnumerable objects && objects.Count() > 1) { - Validate(context, addError, objects); + Validate(objects, context); } else if (value is IEnumerable components && components.Count() > 1) { - Validate(context, addError, components.Select(x => x.Data)); + Validate(components.Select(x => x.Data), context); } - - return default; } - private void Validate(ValidationContext context, AddError addError, IEnumerable items) + private void Validate(IEnumerable items, ValidationContext context) { var duplicates = new HashSet(10); @@ -46,7 +44,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators { if (item.TryGetValue(field, out var fieldValue) && !duplicates.Add(fieldValue)) { - addError(context.Path, T.Get("contents.validation.uniqueObjectValues", new { field })); + context.AddError(context.Path, T.Get("contents.validation.uniqueObjectValues", new { field })); break; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValidator.cs index 86d9bf9d6..2b4058644 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValidator.cs @@ -25,7 +25,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators this.checkUniqueness = checkUniqueness; } - public async ValueTask ValidateAsync(object? value, ValidationContext context, AddError addError) + public void Validate(object? value, ValidationContext context) { var count = context.Path.Count(); @@ -44,16 +44,21 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators if (filter != null) { - var found = await checkUniqueness(filter); - - if (found.Any(x => x.Id != context.ContentId)) - { - addError(context.Path, T.Get("contents.validation.unique")); - } + context.Root.AddTask(ct => ValidateCoreAsync(context, filter)); } } } + private async Task ValidateCoreAsync(ValidationContext context, FilterNode filter) + { + var found = await checkUniqueness(filter); + + if (found.Any(x => x.Id != context.Root.ContentId)) + { + context.AddError(context.Path, T.Get("contents.validation.unique")); + } + } + private static List Path(ValidationContext context) { return Enumerable.Repeat("Data", 1).Union(context.Path).ToList(); diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValuesValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValuesValidator.cs index 6a97b3291..a8d498083 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValuesValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValuesValidator.cs @@ -11,7 +11,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators { public sealed class UniqueValuesValidator : IValidator { - public ValueTask ValidateAsync(object? value, ValidationContext context, AddError addError) + public void Validate(object? value, ValidationContext context) { if (value is IEnumerable items && items.Any()) { @@ -19,11 +19,9 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators if (itemsArray.Length != itemsArray.Distinct().Count()) { - addError(context.Path, T.Get("contents.validation.duplicates")); + context.AddError(context.Path, T.Get("contents.validation.duplicates")); } } - - return default; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs index c91f0c77a..9f73e65d8 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs @@ -86,7 +86,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets public async Task> QueryAsync(DomainId appId, DomainId? parentId, Q q, CancellationToken ct = default) { - using (Telemetry.Activities.StartActivity("ContentQueryService/QueryAsync")) + using (Telemetry.Activities.StartActivity("MongoAssetRepository/QueryAsync")) { try { @@ -148,7 +148,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets public async Task> QueryIdsAsync(DomainId appId, HashSet ids, CancellationToken ct = default) { - using (Telemetry.Activities.StartActivity("ContentQueryService/QueryIdsAsync")) + using (Telemetry.Activities.StartActivity("MongoAssetRepository/QueryIdsAsync")) { var assetEntities = await Collection.Find(BuildFilter(appId, ids)).Only(x => x.Id) @@ -163,7 +163,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets public async Task> QueryChildIdsAsync(DomainId appId, DomainId parentId, CancellationToken ct = default) { - using (Telemetry.Activities.StartActivity("ContentQueryService/QueryChildIdsAsync")) + using (Telemetry.Activities.StartActivity("MongoAssetRepository/QueryChildIdsAsync")) { var assetEntities = await Collection.Find(x => x.IndexedAppId == appId && !x.IsDeleted && x.ParentId == parentId).Only(x => x.Id) @@ -178,7 +178,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets public async Task FindAssetByHashAsync(DomainId appId, string hash, string fileName, long fileSize, CancellationToken ct = default) { - using (Telemetry.Activities.StartActivity("ContentQueryService/FindAssetByHashAsync")) + using (Telemetry.Activities.StartActivity("MongoAssetRepository/FindAssetByHashAsync")) { var assetEntity = await Collection.Find(x => x.IndexedAppId == appId && !x.IsDeleted && x.FileHash == hash && x.FileName == fileName && x.FileSize == fileSize) @@ -191,7 +191,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets public async Task FindAssetBySlugAsync(DomainId appId, string slug, CancellationToken ct = default) { - using (Telemetry.Activities.StartActivity("ContentQueryService/FindAssetBySlugAsync")) + using (Telemetry.Activities.StartActivity("MongoAssetRepository/FindAssetBySlugAsync")) { var assetEntity = await Collection.Find(x => x.IndexedAppId == appId && !x.IsDeleted && x.Slug == slug) @@ -204,7 +204,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets public async Task FindAssetAsync(DomainId appId, DomainId id, CancellationToken ct = default) { - using (Telemetry.Activities.StartActivity("ContentQueryService/FindAssetAsync")) + using (Telemetry.Activities.StartActivity("MongoAssetRepository/FindAssetAsync")) { var documentId = DomainId.Combine(appId, id); @@ -219,7 +219,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets public async Task FindAssetAsync(DomainId id, CancellationToken ct = default) { - using (Telemetry.Activities.StartActivity("ContentQueryService/FindAssetAsync")) + using (Telemetry.Activities.StartActivity("MongoAssetRepository/FindAssetAsync")) { var assetEntity = await Collection.Find(x => x.Id == id && !x.IsDeleted) diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs index a706db0d5..3de39a9a0 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs @@ -179,6 +179,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents entity.DocumentId = value.UniqueId; entity.IndexedAppId = value.AppId.Id; entity.IndexedSchemaId = value.SchemaId.Id; + entity.ReferencedIds ??= new HashSet(); entity.Version = newVersion; if (data.CanHaveReference()) @@ -189,12 +190,10 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents { var components = await appProvider.GetComponentsAsync(schema); - entity.ReferencedIds = entity.Data.GetReferencedIds(schema.SchemaDef, components); + entity.Data.AddReferencedIds(schema.SchemaDef, entity.ReferencedIds, components); } } - entity.ReferencedIds ??= new HashSet(); - return entity; } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs index 2da66b875..2bfb20b8d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs @@ -43,7 +43,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Rules public async Task> QueryIdsAsync(DomainId appId, CancellationToken ct = default) { - using (Telemetry.Activities.StartActivity("MongoSchemaRepository/QueryIdsAsync")) + using (Telemetry.Activities.StartActivity("MongoRuleRepository/QueryIdsAsync")) { var entities = await Collection.Find(x => x.IndexedAppId == appId && !x.IndexedDeleted).Only(x => x.IndexedId).ToListAsync(ct); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/AppUISettingsGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppUISettingsGrain.cs index d7fb21f63..dd44fe254 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/AppUISettingsGrain.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppUISettingsGrain.cs @@ -87,13 +87,13 @@ namespace Squidex.Domain.Apps.Entities.Apps { foreach (var segment in segments.Take(segments.Length - 1)) { - if (!current.TryGetValue(segment, out var temp)) + if (!current.TryGetValue(segment, out var found)) { if (add) { - temp = new JsonObject(); + found = new JsonObject(); - current[segment] = temp; + current[segment] = found; } else { @@ -101,9 +101,9 @@ namespace Squidex.Domain.Apps.Entities.Apps } } - if (temp.Type == JsonValueType.Object) + if (found.Value is JsonObject o) { - current = temp.AsObject; + current = o; } else { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsIndex.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsIndex.cs index 2f7546bd3..d25509e29 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsIndex.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsIndex.cs @@ -55,7 +55,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes public async Task> GetAppsForUserAsync(string userId, PermissionSet permissions, CancellationToken ct = default) { - using (Telemetry.Activities.StartActivity("AppProvider/GetAppsForUserAsync")) + using (Telemetry.Activities.StartActivity("AppsIndex/GetAppsForUserAsync")) { var ids = await Task.WhenAll( @@ -74,7 +74,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes public async Task GetAppAsync(string name, bool canCache = false, CancellationToken ct = default) { - using (Telemetry.Activities.StartActivity("AppProvider/GetAppByNameAsync")) + using (Telemetry.Activities.StartActivity("AppsIndex/GetAppByNameAsync")) { if (canCache) { @@ -98,7 +98,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes public async Task GetAppAsync(DomainId appId, bool canCache = false, CancellationToken ct = default) { - using (Telemetry.Activities.StartActivity("AppProvider/GetAppAsync")) + using (Telemetry.Activities.StartActivity("AppsIndex/GetAppAsync")) { if (canCache) { @@ -121,7 +121,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes private async Task> GetAppIdsByUserAsync(string userId) { - using (Telemetry.Activities.StartActivity("AppProvider/GetAppIdsByUserAsync")) + using (Telemetry.Activities.StartActivity("AppsIndex/GetAppIdsByUserAsync")) { var result = await appRepository.QueryIdsAsync(userId); @@ -131,7 +131,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes private async Task> GetAppIdsAsync(string[] names) { - using (Telemetry.Activities.StartActivity("AppProvider/GetAppIdsAsync")) + using (Telemetry.Activities.StartActivity("AppsIndex/GetAppIdsAsync")) { var result = await Cache().GetAppIdsAsync(names); @@ -141,7 +141,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes private async Task GetAppIdAsync(string name) { - using (Telemetry.Activities.StartActivity("AppProvider/GetAppIdAsync")) + using (Telemetry.Activities.StartActivity("AppsIndex/GetAppIdAsync")) { var result = await Cache().GetAppIdsAsync(new[] { name }); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetEnricher.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetEnricher.cs index 4e3b11119..ceae89429 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetEnricher.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetEnricher.cs @@ -50,7 +50,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries Guard.NotNull(assets); Guard.NotNull(context); - using (Telemetry.Activities.StartActivity("ContentQueryService/EnrichAsync")) + using (Telemetry.Activities.StartActivity("AssetEnricher/EnrichAsync")) { var results = assets.Select(x => SimpleMapper.Map(x, new AssetEntity())).ToList(); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/BackupContents.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/BackupContents.cs index 6ec9b6ddb..611052958 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/BackupContents.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/BackupContents.cs @@ -107,15 +107,13 @@ namespace Squidex.Domain.Apps.Entities.Contents foreach (var (key, value) in source) { - switch (value.Type) + switch (value.Value) { - case JsonValueType.String: + case string s: { - var oldValue = value.AsString; + var newValue = s.Replace(assetsUrlOld!.AssetsApp, assetsUrlNew!.AssetsApp, StringComparison.Ordinal); - var newValue = oldValue.Replace(assetsUrlOld!.AssetsApp, assetsUrlNew!.AssetsApp, StringComparison.Ordinal); - - if (!ReferenceEquals(newValue, oldValue)) + if (!ReferenceEquals(newValue, s)) { replacements ??= new List<(string, string)>(); replacements.Add((key, newValue)); @@ -124,7 +122,7 @@ namespace Squidex.Domain.Apps.Entities.Contents newValue = newValue.Replace(assetsUrlOld!.Assets, assetsUrlNew!.Assets, StringComparison.Ordinal); - if (!ReferenceEquals(newValue, oldValue)) + if (!ReferenceEquals(newValue, s)) { replacements ??= new List<(string, string)>(); replacements.Add((key, newValue)); @@ -134,12 +132,12 @@ namespace Squidex.Domain.Apps.Entities.Contents break; - case JsonValueType.Array: - ReplaceAssetUrl(value.AsArray); + case JsonArray a: + ReplaceAssetUrl(a); break; - case JsonValueType.Object: - ReplaceAssetUrl(value.AsObject); + case JsonObject o: + ReplaceAssetUrl(o); break; } } @@ -159,15 +157,13 @@ namespace Squidex.Domain.Apps.Entities.Contents { var value = source[i]; - switch (value.Type) + switch (value.Value) { - case JsonValueType.String: + case string s: { - var oldValue = value.AsString; - - var newValue = oldValue.Replace(assetsUrlOld!.AssetsApp, assetsUrlNew!.AssetsApp, StringComparison.Ordinal); + var newValue = s.Replace(assetsUrlOld!.AssetsApp, assetsUrlNew!.AssetsApp, StringComparison.Ordinal); - if (!ReferenceEquals(newValue, oldValue)) + if (!ReferenceEquals(newValue, s)) { source[i] = newValue; break; @@ -175,7 +171,7 @@ namespace Squidex.Domain.Apps.Entities.Contents newValue = newValue.Replace(assetsUrlOld!.Assets, assetsUrlNew!.Assets, StringComparison.Ordinal); - if (!ReferenceEquals(newValue, oldValue)) + if (!ReferenceEquals(newValue, s)) { source[i] = newValue; break; @@ -184,11 +180,8 @@ namespace Squidex.Domain.Apps.Entities.Contents break; - case JsonValueType.Array: - break; - - case JsonValueType.Object: - ReplaceAssetUrl(value.AsObject); + case JsonObject o: + ReplaceAssetUrl(o); break; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/ValidationExtensions.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/ValidationExtensions.cs index 0cb6ebd3c..10f793f02 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/ValidationExtensions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/ValidationExtensions.cs @@ -5,7 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using Microsoft.Extensions.Logging; using Squidex.Domain.Apps.Core; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.DefaultValues; @@ -79,8 +78,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards { var validator = GetValidator(operation, optimize, published); - await validator.ValidateInputAsync(data); - await validator.ValidateContentAsync(data); + await validator.ValidateInputAndContentAsync(data); operation.AddErrors(validator.Errors).ThrowOnErrors(); } @@ -104,20 +102,20 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards private static ContentValidator GetValidator(this ContentOperation operation, bool optimize, bool published) { - var validationContext = - new ValidationContext(operation.Resolve(), + var rootContext = + new RootContext(operation.Resolve(), operation.App.NamedId(), operation.Schema.NamedId(), operation.SchemaDef, - operation.Components, - operation.CommandId) - .Optimized(optimize).AsPublishing(published); + operation.CommandId, + operation.Components); + + var validationContext = new ValidationContext(rootContext).Optimized(optimize).AsPublishing(published); var validator = new ContentValidator(operation.Partition(), validationContext, - operation.Resolve>(), - operation.Resolve>()); + operation.Resolve>()); return validator; } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs index 3887739e6..cbe874344 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs @@ -188,14 +188,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL { List? result = null; - if (value.Type == JsonValueType.Array) + if (value.Value is JsonArray a) { - foreach (var item in value.AsArray) + foreach (var item in a) { - if (item.Type == JsonValueType.String) + if (item.Value is string id) { result ??= new List(); - result.Add(DomainId.Create(item.AsString)); + result.Add(DomainId.Create(id)); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/DataInputGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/DataInputGraphType.cs index ee1ef9182..70c3c1bdc 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/DataInputGraphType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/DataInputGraphType.cs @@ -78,7 +78,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents foreach (var item in list) { - if (item is JsonValue nested && nested.Type == JsonValueType.Object) + if (item is JsonValue nested && nested.Value is JsonObject) { array.Add(nested); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/FieldVisitor.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/FieldVisitor.cs index 7bc500a96..d8e6e2eb5 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/FieldVisitor.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/FieldVisitor.cs @@ -28,10 +28,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents private static readonly IFieldResolver JsonBoolean = CreateValueResolver((value, fieldContext, contex) => { - switch (value.Type) + switch (value.Value) { - case JsonValueType.Boolean: - return value.AsBoolean; + case bool b: + return b; default: ThrowHelper.NotSupportedException(); return default!; @@ -40,10 +40,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents private static readonly IFieldResolver JsonComponents = CreateValueResolver((value, fieldContext, contex) => { - switch (value.Type) + switch (value.Value) { - case JsonValueType.Array: - return value.AsArray.Select(x => x.AsObject).ToList(); + case JsonArray a: + return a.Select(x => x.AsObject).ToList(); default: ThrowHelper.NotSupportedException(); return default!; @@ -52,10 +52,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents private static readonly IFieldResolver JsonDateTime = CreateValueResolver((value, fieldContext, contex) => { - switch (value.Type) + switch (value.Value) { - case JsonValueType.String: - return value.AsString; + case string s: + return s; default: ThrowHelper.NotSupportedException(); return default!; @@ -64,10 +64,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents private static readonly IFieldResolver JsonNumber = CreateValueResolver((value, fieldContext, contex) => { - switch (value.Type) + switch (value.Value) { - case JsonValueType.Number: - return value.AsNumber; + case double n: + return n; default: ThrowHelper.NotSupportedException(); return default!; @@ -76,10 +76,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents private static readonly IFieldResolver JsonString = CreateValueResolver((value, fieldContext, contex) => { - switch (value.Type) + switch (value.Value) { - case JsonValueType.String: - return value.AsString; + case string s: + return s; default: ThrowHelper.NotSupportedException(); return default!; @@ -88,10 +88,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents private static readonly IFieldResolver JsonStrings = CreateValueResolver((value, fieldContext, contex) => { - switch (value.Type) + switch (value.Value) { - case JsonValueType.Array: - return value.AsArray.Select(x => x.ToString()).ToList(); + case JsonArray a: + return a.Select(x => x.ToString()).ToList(); default: ThrowHelper.NotSupportedException(); return default!; diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs index 73592ca3c..972e85a72 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs @@ -66,7 +66,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries if (cloneData) { - result.Data = result.Data.Clone(); + using (Telemetry.Activities.StartActivity("ContentEnricher/CloneData")) + { + result.Data = result.Data.Clone(); + } } results.Add(result); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ConvertData.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ConvertData.cs index 5fa5bfc89..9e5147893 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ConvertData.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ConvertData.cs @@ -62,7 +62,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps private async Task CleanReferencesAsync(Context context, IEnumerable contents, ProvideSchema schemas, CancellationToken ct) { - if (!context.ShouldSkipCleanup()) + if (context.ShouldSkipCleanup()) + { + return null; + } + + using (Telemetry.Activities.StartActivity("ConvertData/CleanReferencesAsync")) { var ids = new HashSet(); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ResolveAssets.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ResolveAssets.cs index d0569960a..7c8ed846b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ResolveAssets.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ResolveAssets.cs @@ -61,6 +61,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps private void ResolveAssetsUrls(ISchemaEntity schema, ResolvedComponents components, IGrouping contents, ILookup assets) { + HashSet? fieldIds = null; + foreach (var field in schema.SchemaDef.ResolvingAssets()) { foreach (var content in contents) @@ -73,8 +75,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps { foreach (var (partitionKey, partitionValue) in fieldData) { + fieldIds ??= new HashSet(); + fieldIds.Clear(); + + partitionValue.AddReferencedIds(field, fieldIds, components); + var referencedAsset = - field.GetReferencedIds(partitionValue, components) + fieldIds .Select(x => assets[x]) .SelectMany(x => x) .FirstOrDefault(); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ResolveReferences.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ResolveReferences.cs index 54827075d..264c8522e 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ResolveReferences.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ResolveReferences.cs @@ -64,6 +64,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps private async Task ResolveReferencesAsync(Context context, ISchemaEntity schema, ResolvedComponents components, IEnumerable contents, ILookup references, ProvideSchema schemas) { + HashSet? fieldIds = null; + var formatted = new Dictionary(); foreach (var field in schema.SchemaDef.ResolvingReferences()) @@ -80,8 +82,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps { foreach (var (partition, partitionValue) in fieldData) { + fieldIds ??= new HashSet(); + fieldIds.Clear(); + + partitionValue.AddReferencedIds(field, fieldIds, components); + var referencedContents = - field.GetReferencedIds(partitionValue, components) + fieldIds .Select(x => references[x]) .SelectMany(x => x) .ToList(); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ScriptContent.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ScriptContent.cs index 736bed454..c12bb3bc1 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ScriptContent.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ScriptContent.cs @@ -6,7 +6,6 @@ // ========================================================================== using Squidex.Domain.Apps.Core.Scripting; -using Squidex.Infrastructure.Tasks; namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps { @@ -59,10 +58,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps await scriptEngine.ExecuteAsync(vars, preScript, options, ct); } - await AsyncHelper.WhenAllThrottledAsync(group, async (content, _) => + foreach (var content in group) { await TransformAsync(vars, script, content, ct); - }, ct: ct); + } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/Extensions.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/Extensions.cs index b864f38b0..21c8783d2 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/Extensions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/Extensions.cs @@ -83,20 +83,20 @@ namespace Squidex.Domain.Apps.Entities.Contents.Text private static void AppendJsonText(Dictionary languages, string language, JsonValue value) { - switch (value.Type) + switch (value.Value) { - case JsonValueType.String: - AppendText(languages, language, value.AsString); + case string s: + AppendText(languages, language, s); break; - case JsonValueType.Array: - foreach (var item in value.AsArray) + case JsonArray a: + foreach (var item in a) { AppendJsonText(languages, language, item); } break; - case JsonValueType.Object: - foreach (var (_, item) in value.AsObject) + case JsonObject o: + foreach (var (_, item) in o) { AppendJsonText(languages, language, item); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Validation/DependencyValidatorsFactory.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Validation/DependencyValidatorsFactory.cs index e4b05e2c8..ccc5dde9f 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Validation/DependencyValidatorsFactory.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Validation/DependencyValidatorsFactory.cs @@ -24,7 +24,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Validation this.contentRepository = contentRepository; } - public IEnumerable CreateValueValidators(ValidatorContext context, IField field, ValidatorFactory createFieldValidator) + public IEnumerable CreateValueValidators(ValidationContext context, IField field, ValidatorFactory createFieldValidator) { if (context.Mode == ValidationMode.Optimized) { @@ -37,7 +37,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Validation { var checkAssets = new CheckAssets(async ids => { - return await assetRepository.QueryAsync(context.AppId.Id, null, Q.Empty.WithIds(ids), default); + return await assetRepository.QueryAsync(context.Root.AppId.Id, null, Q.Empty.WithIds(ids), default); }); yield return new AssetsValidator(isRequired, assetsField.Properties, checkAssets); @@ -47,7 +47,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Validation { var checkReferences = new CheckContentsByIds(async ids => { - return await contentRepository.QueryIdsAsync(context.AppId.Id, ids, SearchScope.All, default); + return await contentRepository.QueryIdsAsync(context.Root.AppId.Id, ids, SearchScope.All, default); }); yield return new ReferencesValidator(isRequired, referencesField.Properties, checkReferences); @@ -57,7 +57,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Validation { var checkUniqueness = new CheckUniqueness(async filter => { - return await contentRepository.QueryIdsAsync(context.AppId.Id, context.SchemaId.Id, filter, default); + return await contentRepository.QueryIdsAsync(context.Root.AppId.Id, context.Root.SchemaId.Id, filter, default); }); yield return new UniqueValidator(checkUniqueness); @@ -67,14 +67,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.Validation { var checkUniqueness = new CheckUniqueness(async filter => { - return await contentRepository.QueryIdsAsync(context.AppId.Id, context.SchemaId.Id, filter, default); + return await contentRepository.QueryIdsAsync(context.Root.AppId.Id, context.Root.SchemaId.Id, filter, default); }); yield return new UniqueValidator(checkUniqueness); } } - private static bool IsRequired(ValidatorContext context, FieldProperties properties) + private static bool IsRequired(ValidationContext context, FieldProperties properties) { var isRequired = properties.IsRequired; diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleDequeuerGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleDequeuerGrain.cs index b62d0921b..6ce9e1bec 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleDequeuerGrain.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleDequeuerGrain.cs @@ -114,10 +114,9 @@ namespace Squidex.Domain.Apps.Entities.Rules if (response.Status == RuleResult.Failed) { - log.LogWarning(response.Exception, "Failed to execute rule event with rule id {ruleId}/{description}: {dump}", + log.LogWarning(response.Exception, "Failed to execute rule event with rule id {ruleId}/{description}.", @event.Job.RuleId, - @event.Job.Description, - response.Dump); + @event.Job.Description); } } catch (Exception ex) diff --git a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Writer.cs b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Writer.cs index 5a44dc482..666436048 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Writer.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Writer.cs @@ -48,7 +48,7 @@ namespace Squidex.Infrastructure.EventSourcing Guard.NotNull(events); Guard.GreaterEquals(expectedVersion, EtagVersion.Any); - using (Telemetry.Activities.StartActivity("ContentQueryService/AppendAsync")) + using (Telemetry.Activities.StartActivity("MongoEventStore/AppendAsync")) { if (events.Count == 0) { @@ -111,7 +111,7 @@ namespace Squidex.Infrastructure.EventSourcing { Guard.NotNull(commits); - using (Telemetry.Activities.StartActivity("ContentQueryService/AppendUnsafeAsync")) + using (Telemetry.Activities.StartActivity("MongoEventStore/AppendUnsafeAsync")) { var writes = new List>(); diff --git a/backend/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStoreBase.cs b/backend/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStoreBase.cs index f5bd670f8..2a214a0aa 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStoreBase.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStoreBase.cs @@ -41,7 +41,7 @@ namespace Squidex.Infrastructure.States public async Task<(T Value, bool Valid, long Version)> ReadAsync(DomainId key, CancellationToken ct = default) { - using (Telemetry.Activities.StartActivity("ContentQueryService/ReadAsync")) + using (Telemetry.Activities.StartActivity("MongoSnapshotStoreBase/ReadAsync")) { var existing = await Collection.Find(x => x.DocumentId.Equals(key)) @@ -59,7 +59,7 @@ namespace Squidex.Infrastructure.States public async Task WriteAsync(DomainId key, T value, long oldVersion, long newVersion, CancellationToken ct = default) { - using (Telemetry.Activities.StartActivity("ContentQueryService/WriteAsync")) + using (Telemetry.Activities.StartActivity("MongoSnapshotStoreBase/WriteAsync")) { var document = CreateDocument(key, value, newVersion); @@ -70,7 +70,7 @@ namespace Squidex.Infrastructure.States public async Task WriteManyAsync(IEnumerable<(DomainId Key, T Value, long Version)> snapshots, CancellationToken ct = default) { - using (Telemetry.Activities.StartActivity("ContentQueryService/WriteManyAsync")) + using (Telemetry.Activities.StartActivity("MongoSnapshotStoreBase/WriteManyAsync")) { var writes = snapshots.Select(x => new ReplaceOneModel(Filter.Eq(y => y.DocumentId, x.Key), CreateDocument(x.Key, x.Value, x.Version)) @@ -90,7 +90,7 @@ namespace Squidex.Infrastructure.States public async Task RemoveAsync(DomainId key, CancellationToken ct = default) { - using (Telemetry.Activities.StartActivity("ContentQueryService/RemoveAsync")) + using (Telemetry.Activities.StartActivity("MongoSnapshotStoreBase/RemoveAsync")) { await Collection.DeleteOneAsync(x => x.DocumentId.Equals(key), ct); } @@ -99,7 +99,7 @@ namespace Squidex.Infrastructure.States public async IAsyncEnumerable<(T State, long Version)> ReadAllAsync( [EnumeratorCancellation] CancellationToken ct = default) { - using (Telemetry.Activities.StartActivity("ContentQueryService/ReadAllAsync")) + using (Telemetry.Activities.StartActivity("MongoSnapshotStoreBase/ReadAllAsync")) { var find = Collection.Find(new BsonDocument(), Batching.Options); diff --git a/backend/src/Squidex.Infrastructure/CollectionExtensions.cs b/backend/src/Squidex.Infrastructure/CollectionExtensions.cs index 79d704c2a..2305f4288 100644 --- a/backend/src/Squidex.Infrastructure/CollectionExtensions.cs +++ b/backend/src/Squidex.Infrastructure/CollectionExtensions.cs @@ -248,10 +248,10 @@ namespace Squidex.Infrastructure public static bool EqualsDictionary(this IReadOnlyDictionary dictionary, IReadOnlyDictionary? other) where TKey : notnull { - return EqualsDictionary(dictionary, other, EqualityComparer.Default, EqualityComparer.Default); + return EqualsDictionary(dictionary, other, EqualityComparer.Default); } - public static bool EqualsDictionary(this IReadOnlyDictionary dictionary, IReadOnlyDictionary? other, IEqualityComparer keyComparer, IEqualityComparer valueComparer) where TKey : notnull + public static bool EqualsDictionary(this IReadOnlyDictionary dictionary, IReadOnlyDictionary? other, IEqualityComparer valueComparer) where TKey : notnull { if (other == null) { @@ -268,9 +268,15 @@ namespace Squidex.Infrastructure return false; } - var comparer = new KeyValuePairComparer(keyComparer, valueComparer); + foreach (var (key, value) in dictionary) + { + if (!other.TryGetValue(key, out var otherValue) || !valueComparer.Equals(value, otherValue)) + { + return false; + } + } - return !dictionary.Except(other, comparer).Any(); + return true; } public static bool EqualsList(this IReadOnlyList list, IReadOnlyList? other) @@ -393,37 +399,5 @@ namespace Squidex.Infrastructure index++; } } - - public sealed class KeyValuePairComparer : IEqualityComparer> where TKey : notnull - { - private readonly IEqualityComparer keyComparer; - private readonly IEqualityComparer valueComparer; - - public KeyValuePairComparer(IEqualityComparer keyComparer, IEqualityComparer valueComparer) - { - this.keyComparer = keyComparer; - this.valueComparer = valueComparer; - } - - public bool Equals(KeyValuePair x, KeyValuePair y) - { - return keyComparer.Equals(x.Key, y.Key) && valueComparer.Equals(x.Value, y.Value); - } - - public int GetHashCode(KeyValuePair obj) - { - return keyComparer.GetHashCode(obj.Key) ^ ValueHashCode(obj); - } - - private int ValueHashCode(KeyValuePair obj) - { - if (Equals(obj.Value, null)) - { - return 0; - } - - return valueComparer.GetHashCode(obj.Value); - } - } } } diff --git a/backend/src/Squidex.Infrastructure/Collections/ListDictionary.cs b/backend/src/Squidex.Infrastructure/Collections/ListDictionary.cs index de75e1a7a..bbffab456 100644 --- a/backend/src/Squidex.Infrastructure/Collections/ListDictionary.cs +++ b/backend/src/Squidex.Infrastructure/Collections/ListDictionary.cs @@ -160,7 +160,7 @@ namespace Squidex.Infrastructure.Collections ThrowHelper.ArgumentException("Key already exists.", nameof(key)); } - entries.Add(new KeyValuePair(key, value)); + AddUnsafe(key, value); } public void Add(KeyValuePair item) @@ -168,6 +168,11 @@ namespace Squidex.Infrastructure.Collections Add(item.Key, item.Value); } + public void AddUnsafe(TKey key, TValue value) + { + entries.Add(new KeyValuePair(key, value)); + } + public void Clear() { entries.Clear(); diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/DefaultEventDataFormatter.cs b/backend/src/Squidex.Infrastructure/EventSourcing/DefaultEventDataFormatter.cs index 1ba5cc0b5..d2665001a 100644 --- a/backend/src/Squidex.Infrastructure/EventSourcing/DefaultEventDataFormatter.cs +++ b/backend/src/Squidex.Infrastructure/EventSourcing/DefaultEventDataFormatter.cs @@ -5,7 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System.Diagnostics; using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Migrations; using Squidex.Infrastructure.Reflection; @@ -26,36 +25,40 @@ namespace Squidex.Infrastructure.EventSourcing public Envelope? ParseIfKnown(StoredEvent storedEvent) { - try - { - return Parse(storedEvent); - } - catch (TypeNameNotFoundException) + return ParseCore(storedEvent); + } + + public Envelope Parse(StoredEvent storedEvent) + { + var envelope = ParseCore(storedEvent); + + if (envelope == null) { - return null; + throw new TypeNameNotFoundException($"Cannot find event with type name '{storedEvent.Data.Type}'."); } + + return envelope; } - public Envelope Parse(StoredEvent storedEvent) + private Envelope? ParseCore(StoredEvent storedEvent) { Guard.NotNull(storedEvent); - var eventData = storedEvent.Data; + var payloadType = typeNameRegistry.GetTypeOrNull(storedEvent.Data.Type); - var payloadType = typeNameRegistry.GetType(eventData.Type); - var payloadValue = serializer.Deserialize(eventData.Payload, payloadType); + if (payloadType == null) + { + return null; + } + + var payloadValue = serializer.Deserialize(storedEvent.Data.Payload, payloadType); if (payloadValue is IMigrated migratedEvent) { payloadValue = migratedEvent.Migrate(); - - if (ReferenceEquals(migratedEvent, payloadValue)) - { - Debug.WriteLine("Migration should return new event."); - } } - var envelope = new Envelope(payloadValue, eventData.Headers); + var envelope = new Envelope(payloadValue, storedEvent.Data.Headers); envelope.SetEventPosition(storedEvent.EventPosition); envelope.SetEventStreamNumber(storedEvent.EventStreamNumber); @@ -65,19 +68,14 @@ namespace Squidex.Infrastructure.EventSourcing public EventData ToEventData(Envelope envelope, Guid commitId, bool migrate = true) { - var eventPayload = envelope.Payload; + var payloadValue = envelope.Payload; - if (migrate && eventPayload is IMigrated migratedEvent) + if (migrate && payloadValue is IMigrated migratedEvent) { - eventPayload = migratedEvent.Migrate(); - - if (ReferenceEquals(migratedEvent, eventPayload)) - { - Debug.WriteLine("Migration should return new event."); - } + payloadValue = migratedEvent.Migrate(); } - var payloadType = typeNameRegistry.GetName(eventPayload.GetType()); + var payloadType = typeNameRegistry.GetName(payloadValue.GetType()); var payloadJson = serializer.Serialize(envelope.Payload); envelope.SetCommitId(commitId); diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/EnvelopeExtensions.cs b/backend/src/Squidex.Infrastructure/EventSourcing/EnvelopeExtensions.cs index 9c024539e..ed44c7715 100644 --- a/backend/src/Squidex.Infrastructure/EventSourcing/EnvelopeExtensions.cs +++ b/backend/src/Squidex.Infrastructure/EventSourcing/EnvelopeExtensions.cs @@ -100,13 +100,13 @@ namespace Squidex.Infrastructure.EventSourcing public static long GetLong(this EnvelopeHeaders obj, string key) { - if (obj.TryGetValue(key, out var v)) + if (obj.TryGetValue(key, out var found)) { - if (v.Type == JsonValueType.Number) + if (found.Value is double d) { - return (long)v.AsNumber; + return (long)d; } - else if (v.Type == JsonValueType.String && double.TryParse(v.AsString, NumberStyles.Any, CultureInfo.InvariantCulture, out var result)) + else if (found.Value is string s && double.TryParse(s, NumberStyles.Any, CultureInfo.InvariantCulture, out var result)) { return (long)result; } @@ -117,7 +117,7 @@ namespace Squidex.Infrastructure.EventSourcing public static Guid GetGuid(this EnvelopeHeaders obj, string key) { - if (obj.TryGetValue(key, out var v) && v.Type == JsonValueType.String && Guid.TryParse(v.AsString, out var guid)) + if (obj.TryGetValue(key, out var found) && found.Value is string s && Guid.TryParse(s, out var guid)) { return guid; } @@ -127,7 +127,7 @@ namespace Squidex.Infrastructure.EventSourcing public static Instant GetInstant(this EnvelopeHeaders obj, string key) { - if (obj.TryGetValue(key, out var v) && v.Type == JsonValueType.String && InstantPattern.ExtendedIso.Parse(v.AsString).TryGetValue(default, out var instant)) + if (obj.TryGetValue(key, out var found) && found.Value is string s && InstantPattern.ExtendedIso.Parse(s).TryGetValue(default, out var instant)) { return instant; } @@ -137,9 +137,9 @@ namespace Squidex.Infrastructure.EventSourcing public static string GetString(this EnvelopeHeaders obj, string key) { - if (obj.TryGetValue(key, out var v)) + if (obj.TryGetValue(key, out var found)) { - return v.ToString(); + return found.ToString(); } return string.Empty; @@ -147,9 +147,9 @@ namespace Squidex.Infrastructure.EventSourcing public static bool GetBoolean(this EnvelopeHeaders obj, string key) { - if (obj.TryGetValue(key, out var v) && v.Type == JsonValueType.Boolean) + if (obj.TryGetValue(key, out var found) && found.Value is bool b) { - return v.AsBoolean; + return b; } return false; diff --git a/backend/src/Squidex.Infrastructure/Json/Newtonsoft/JsonValueConverter.cs b/backend/src/Squidex.Infrastructure/Json/Newtonsoft/JsonValueConverter.cs index bf1a075f4..26f7ea2d6 100644 --- a/backend/src/Squidex.Infrastructure/Json/Newtonsoft/JsonValueConverter.cs +++ b/backend/src/Squidex.Infrastructure/Json/Newtonsoft/JsonValueConverter.cs @@ -6,6 +6,7 @@ // ========================================================================== using Newtonsoft.Json; +using Squidex.Infrastructure.Collections; using Squidex.Infrastructure.Json.Objects; #pragma warning disable RECS0018 // Comparison of floating point numbers with equality operator @@ -26,19 +27,40 @@ namespace Squidex.Infrastructure.Json.Newtonsoft public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) { - return ReadJson(reader); + var previousDateParseHandling = reader.DateParseHandling; + + reader.DateParseHandling = DateParseHandling.None; + try + { + return ReadJsonCore(reader); + } + finally + { + reader.DateParseHandling = previousDateParseHandling; + } } - private static JsonValue ReadJson(JsonReader reader) + private static JsonValue ReadJsonCore(JsonReader reader) { switch (reader.TokenType) { - case JsonToken.Comment: - reader.Read(); - break; + 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(1); + var result = new JsonObject(4); + + Dictionary dictionary = result; while (reader.Read()) { @@ -49,26 +71,27 @@ namespace Squidex.Infrastructure.Json.Newtonsoft if (!reader.Read()) { - throw new JsonSerializationException("Unexpected end when reading Object."); + ThrowInvalidObjectException(); } - var value = ReadJson(reader); + var value = ReadJsonCore(reader); - result[propertyName] = value; + dictionary.Add(propertyName, value); break; case JsonToken.EndObject: result.TrimExcess(); - return result; + return new JsonValue(result); } } - throw new JsonSerializationException("Unexpected end when reading Object."); + ThrowInvalidObjectException(); + return default!; } case JsonToken.StartArray: { - var result = new JsonArray(1); + var result = new JsonArray(4); while (reader.Read()) { @@ -79,42 +102,43 @@ namespace Squidex.Infrastructure.Json.Newtonsoft case JsonToken.EndArray: result.TrimExcess(); - return result; + return new JsonValue(result); default: - var value = ReadJson(reader); + var value = ReadJsonCore(reader); result.Add(value); break; } } - throw new JsonSerializationException("Unexpected end when reading Object."); + ThrowInvalidArrayException(); + return default!; } - case JsonToken.Integer when reader.Value is int i: - return i; - case JsonToken.Integer when reader.Value is long l: - return l; - case JsonToken.Float when reader.Value is float f: - return f; - case JsonToken.Float when reader.Value is double d: - return d; - case JsonToken.Boolean when reader.Value is bool b: - return b; - case JsonToken.Date when reader.Value is DateTime d: - return d.ToIso8601(); - case JsonToken.String when reader.Value is string s: - return s; - case JsonToken.Null: - return default; - case JsonToken.Undefined: - return default; + case JsonToken.Comment: + reader.Read(); + break; } - ThrowHelper.NotSupportedException(); + 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) @@ -128,34 +152,32 @@ namespace Squidex.Infrastructure.Json.Newtonsoft private static void WriteJson(JsonWriter writer, JsonValue value) { - switch (value.Type) + switch (value.Value) { - case JsonValueType.Null: + case null: writer.WriteNull(); break; - case JsonValueType.Boolean: - writer.WriteValue(value.AsBoolean); + case bool b: + writer.WriteValue(b); break; - case JsonValueType.String: - writer.WriteValue(value.AsString); + case string s: + writer.WriteValue(s); break; - case JsonValueType.Number: - var number = value.AsNumber; - - if (number % 1 == 0) + case double n: + if (n % 1 == 0) { - writer.WriteValue((long)number); + writer.WriteValue((long)n); } else { - writer.WriteValue(number); + writer.WriteValue(n); } break; - case JsonValueType.Array: + case JsonArray a: writer.WriteStartArray(); - foreach (var item in value.AsArray) + foreach (var item in a) { WriteJson(writer, item); } @@ -163,10 +185,10 @@ namespace Squidex.Infrastructure.Json.Newtonsoft writer.WriteEndArray(); break; - case JsonValueType.Object: + case JsonObject o: writer.WriteStartObject(); - foreach (var (key, jsonValue) in value.AsObject) + foreach (var (key, jsonValue) in o) { writer.WritePropertyName(key); diff --git a/backend/src/Squidex.Infrastructure/Json/Objects/JsonObject.cs b/backend/src/Squidex.Infrastructure/Json/Objects/JsonObject.cs index 6442f694d..be9b5305a 100644 --- a/backend/src/Squidex.Infrastructure/Json/Objects/JsonObject.cs +++ b/backend/src/Squidex.Infrastructure/Json/Objects/JsonObject.cs @@ -9,7 +9,7 @@ using Squidex.Infrastructure.Collections; namespace Squidex.Infrastructure.Json.Objects { - public class JsonObject : ListDictionary, IEquatable + public class JsonObject : Dictionary, IEquatable { public JsonObject() { diff --git a/backend/src/Squidex.Infrastructure/Json/Objects/JsonValue.cs b/backend/src/Squidex.Infrastructure/Json/Objects/JsonValue.cs index 0b27a6acd..7b7e7a0ad 100644 --- a/backend/src/Squidex.Infrastructure/Json/Objects/JsonValue.cs +++ b/backend/src/Squidex.Infrastructure/Json/Objects/JsonValue.cs @@ -9,8 +9,6 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; using NodaTime; -#pragma warning disable RECS0018 // Comparison of floating point numbers with equality operator - namespace Squidex.Infrastructure.Json.Objects { public readonly struct JsonValue : IEquatable @@ -330,8 +328,8 @@ namespace Squidex.Infrastructure.Json.Objects return "null"; case bool b: return b ? "true" : "false"; - case double d: - return d.ToString(CultureInfo.InvariantCulture); + case double n: + return n.ToString(CultureInfo.InvariantCulture); case string s: return s; case JsonArray a: @@ -352,8 +350,8 @@ namespace Squidex.Infrastructure.Json.Objects return "null"; case bool b: return b ? "true" : "false"; - case double d: - return d.ToString(CultureInfo.InvariantCulture); + case double n: + return n.ToString(CultureInfo.InvariantCulture); case string s: return $"\"{s}\""; case JsonArray a: @@ -370,14 +368,6 @@ namespace Squidex.Infrastructure.Json.Objects { switch (Value) { - case null: - return this; - case bool: - return this; - case double: - return this; - case string: - return this; case JsonArray a: { var result = new JsonArray(a.Count); @@ -403,8 +393,7 @@ namespace Squidex.Infrastructure.Json.Objects } default: - ThrowInvalidType(); - return default!; + return this; } } @@ -442,19 +431,6 @@ namespace Squidex.Infrastructure.Json.Objects return hasSegment; } - public bool TryGetValue(JsonValueType type, string pathSegment, out JsonValue result) - { - result = default!; - - if (TryGetValue(pathSegment, out var temp) && temp.Type == type) - { - result = temp; - return true; - } - - return false; - } - public bool TryGetValue(string pathSegment, out JsonValue result) { result = default!; diff --git a/backend/src/Squidex.Infrastructure/Orleans/ActivityPropagationGrainCallFilter.cs b/backend/src/Squidex.Infrastructure/Orleans/ActivityPropagationGrainCallFilter.cs new file mode 100644 index 000000000..1bad32e6c --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Orleans/ActivityPropagationGrainCallFilter.cs @@ -0,0 +1,118 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Diagnostics; +using Orleans; +using Orleans.Runtime; + +#pragma warning disable MA0048 // File name must match type name + +namespace Squidex.Infrastructure.Orleans +{ + public abstract class ActivityPropagationGrainCallFilter + { + public const string ActivityNameIn = "Orleans.Runtime.GrainCall.In"; + public const string ActivityNameOut = "Orleans.Runtime.GrainCall.Out"; + protected const string TraceParentHeaderName = "traceparent"; + protected const string TraceStateHeaderName = "tracestate"; + + protected static async Task ProcessNewActivity(IGrainCallContext context, string activityName, ActivityKind activityKind, ActivityContext activityContext) + { + ActivityTagsCollection? tags = null; + + if (Telemetry.Activities.HasListeners()) + { + tags = new ActivityTagsCollection + { + { "net.peer.name", context.Grain?.ToString() }, + { "rpc.method", context.InterfaceMethod?.Name }, + { "rpc.service", context.InterfaceMethod?.DeclaringType?.FullName }, + { "rpc.system", "orleans" } + }; + } + + using (var activity = Telemetry.Activities.StartActivity(activityName, activityKind, activityContext, tags)) + { + if (activity is not null) + { + RequestContext.Set(TraceParentHeaderName, activity.Id); + } + + try + { + await context.Invoke(); + + if (activity is not null && activity.IsAllDataRequested) + { + activity.SetTag("status", "Ok"); + } + } + catch (Exception e) + { + if (activity is not null && activity.IsAllDataRequested) + { + // Exception attributes from https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/exceptions.md + activity.SetTag("exception.type", e.GetType().FullName); + activity.SetTag("exception.message", e.Message); + activity.SetTag("exception.stacktrace", e.StackTrace); + activity.SetTag("exception.escaped", true); + activity.SetTag("status", "Error"); + } + + throw; + } + } + } + } + + public sealed class ActivityPropagationOutgoingGrainCallFilter : ActivityPropagationGrainCallFilter, IOutgoingGrainCallFilter + { + public Task Invoke(IOutgoingGrainCallContext context) + { + if (Activity.Current != null) + { + return ProcessCurrentActivity(context); + } + + return ProcessNewActivity(context, ActivityNameOut, ActivityKind.Client, default); + } + + private static Task ProcessCurrentActivity(IOutgoingGrainCallContext context) + { + var currentActivity = Activity.Current; + + if (currentActivity is not null && currentActivity.IdFormat == ActivityIdFormat.W3C) + { + RequestContext.Set(TraceParentHeaderName, currentActivity.Id); + + if (currentActivity.TraceStateString is not null) + { + RequestContext.Set(TraceStateHeaderName, currentActivity.TraceStateString); + } + } + + return context.Invoke(); + } + } + + public sealed class ActivityPropagationIncomingGrainCallFilter : ActivityPropagationGrainCallFilter, IIncomingGrainCallFilter + { + public Task Invoke(IIncomingGrainCallContext context) + { + var traceParent = RequestContext.Get(TraceParentHeaderName) as string; + var traceState = RequestContext.Get(TraceStateHeaderName) as string; + var parentContext = default(ActivityContext); + + if (traceParent is not null) + { + parentContext = ActivityContext.Parse(traceParent, traceState); + } + + return ProcessNewActivity(context, ActivityNameIn, ActivityKind.Server, parentContext); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Orleans/LoggingFilter.cs b/backend/src/Squidex.Infrastructure/Orleans/LoggingFilter.cs index 0f071cc71..a36d25361 100644 --- a/backend/src/Squidex.Infrastructure/Orleans/LoggingFilter.cs +++ b/backend/src/Squidex.Infrastructure/Orleans/LoggingFilter.cs @@ -21,7 +21,7 @@ namespace Squidex.Infrastructure.Orleans public async Task Invoke(IIncomingGrainCallContext context) { - var name = $"Grain/{context.Grain?.GetType().Name}/{context.ImplementationMethod?.Name}"; + var name = $"Grain/{context.ImplementationMethod?.DeclaringType.FullName}/{context.ImplementationMethod?.Name}"; using (Telemetry.Activities.StartActivity(name)) { diff --git a/backend/src/Squidex.Infrastructure/Queries/Json/ValueConverter.cs b/backend/src/Squidex.Infrastructure/Queries/Json/ValueConverter.cs index c68549ed0..39f463ab5 100644 --- a/backend/src/Squidex.Infrastructure/Queries/Json/ValueConverter.cs +++ b/backend/src/Squidex.Infrastructure/Queries/Json/ValueConverter.cs @@ -28,7 +28,7 @@ namespace Squidex.Infrastructure.Queries.Json var type = field.Schema.Type; - if (value.Type == JsonValueType.Null && type != FilterSchemaType.GeoObject && field.IsNullable) + if (value == default && type != FilterSchemaType.GeoObject && field.IsNullable) { return ClrValue.Null; } @@ -47,9 +47,9 @@ namespace Squidex.Infrastructure.Queries.Json case FilterSchemaType.Any: { - if (value.Type == JsonValueType.Array) + if (value.Value is JsonArray a) { - var array = ParseArray(errors, path, value.AsArray, TryParseDynamic); + var array = ParseArray(errors, path, a, TryParseDynamic); result = array.Select(x => x?.Value).ToList(); } @@ -63,9 +63,9 @@ namespace Squidex.Infrastructure.Queries.Json case FilterSchemaType.Boolean: { - if (value.Type == JsonValueType.Array) + if (value.Value is JsonArray a) { - result = ParseArray(errors, path, value.AsArray, TryParseBoolean); + result = ParseArray(errors, path, a, TryParseBoolean); } else if (TryParseBoolean(errors, path, value, out var temp)) { @@ -77,9 +77,9 @@ namespace Squidex.Infrastructure.Queries.Json case FilterSchemaType.Number: { - if (value.Type == JsonValueType.Array) + if (value.Value is JsonArray a) { - result = ParseArray(errors, path, value.AsArray, TryParseNumber); + result = ParseArray(errors, path, a, TryParseNumber); } else if (TryParseNumber(errors, path, value, out var temp)) { @@ -91,9 +91,9 @@ namespace Squidex.Infrastructure.Queries.Json case FilterSchemaType.Guid: { - if (value.Type == JsonValueType.Array) + if (value.Value is JsonArray a) { - result = ParseArray(errors, path, value.AsArray, TryParseGuid); + result = ParseArray(errors, path, a, TryParseGuid); } else if (TryParseGuid(errors, path, value, out var temp)) { @@ -105,9 +105,9 @@ namespace Squidex.Infrastructure.Queries.Json case FilterSchemaType.DateTime: { - if (value.Type == JsonValueType.Array) + if (value.Value is JsonArray a) { - result = ParseArray(errors, path, value.AsArray, TryParseDateTime); + result = ParseArray(errors, path, a, TryParseDateTime); } else if (TryParseDateTime(errors, path, value, out var temp)) { @@ -120,9 +120,9 @@ namespace Squidex.Infrastructure.Queries.Json case FilterSchemaType.StringArray: case FilterSchemaType.String: { - if (value.Type == JsonValueType.Array) + if (value.Value is JsonArray a) { - result = ParseArray(errors, path, value.AsArray, TryParseString!); + result = ParseArray(errors, path, a, TryParseString!); } else if (TryParseString(errors, path, value, out var temp)) { @@ -163,12 +163,12 @@ namespace Squidex.Infrastructure.Queries.Json result = default!; - if (value.Type == JsonValueType.Object && - value.TryGetValue("latitude", out var lat) && lat.Type == JsonValueType.Number && - value.TryGetValue("longitude", out var lon) && lon.Type == JsonValueType.Number && - value.TryGetValue("distance", out var distance) && distance.Type == JsonValueType.Number) + if (value.Value is JsonObject o && + o.TryGetValue("latitude", out var found) && found.Value is double lat && + o.TryGetValue("longitude", out found) && found.Value is double lon && + o.TryGetValue("distance", out found) && found.Value is double distance) { - result = new FilterSphere(lon.AsNumber, lat.AsNumber, distance.AsNumber); + result = new FilterSphere(lon, lat, distance); return true; } @@ -184,9 +184,9 @@ namespace Squidex.Infrastructure.Queries.Json result = default; - if (value.Type == JsonValueType.Boolean) + if (value.Value is bool b) { - result = value.AsBoolean; + result = b; return true; } @@ -202,9 +202,9 @@ namespace Squidex.Infrastructure.Queries.Json result = default; - if (value.Type == JsonValueType.Number) + if (value.Value is double n) { - result = value.AsNumber; + result = n; return true; } @@ -220,9 +220,9 @@ namespace Squidex.Infrastructure.Queries.Json result = default; - if (value.Type == JsonValueType.String) + if (value.Value is string s) { - result = value.AsString; + result = s; return true; } @@ -238,9 +238,9 @@ namespace Squidex.Infrastructure.Queries.Json result = default; - if (value.Type == JsonValueType.String) + if (value.Value is string s) { - if (Guid.TryParse(value.AsString, out result)) + if (Guid.TryParse(s, out result)) { return true; } @@ -261,13 +261,11 @@ namespace Squidex.Infrastructure.Queries.Json result = default; - if (value.Type == JsonValueType.String) + if (value.Value is string s) { - var typed = value.AsString; - foreach (var pattern in InstantPatterns) { - var parsed = pattern.Parse(typed); + var parsed = pattern.Parse(s); if (parsed.Success) { @@ -291,21 +289,19 @@ namespace Squidex.Infrastructure.Queries.Json { result = null; - switch (value.Type) + switch (value.Value) { - case JsonValueType.Null: + case null: return true; - case JsonValueType.Number: - result = value.AsNumber; + case bool b: + result = b; return true; - case JsonValueType.Boolean: - result = value.AsBoolean; + case double n: + result = n; return true; - case JsonValueType.String: + case string s: { - var typed = value.AsString; - - if (Guid.TryParse(typed, out var guid)) + if (Guid.TryParse(s, out var guid)) { result = guid; @@ -314,7 +310,7 @@ namespace Squidex.Infrastructure.Queries.Json foreach (var pattern in InstantPatterns) { - var parsed = pattern.Parse(typed); + var parsed = pattern.Parse(s); if (parsed.Success) { @@ -324,7 +320,7 @@ namespace Squidex.Infrastructure.Queries.Json } } - result = typed; + result = s; return true; } diff --git a/backend/src/Squidex.Infrastructure/Reflection/TypeNameRegistry.cs b/backend/src/Squidex.Infrastructure/Reflection/TypeNameRegistry.cs index 045e7dfb5..8e274c211 100644 --- a/backend/src/Squidex.Infrastructure/Reflection/TypeNameRegistry.cs +++ b/backend/src/Squidex.Infrastructure/Reflection/TypeNameRegistry.cs @@ -127,7 +127,7 @@ namespace Squidex.Infrastructure.Reflection return result; } - public Type GetTypeOrNull(string name) + public Type? GetTypeOrNull(string name) { var result = typesByName.GetOrDefault(name); diff --git a/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj b/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj index 4c1e69297..fa3d03e75 100644 --- a/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj +++ b/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj @@ -62,7 +62,4 @@ Resources.Designer.cs - - - diff --git a/backend/src/Squidex.Infrastructure/Tasks/Scheduler.cs b/backend/src/Squidex.Infrastructure/Tasks/Scheduler.cs new file mode 100644 index 000000000..0dbbdc1df --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Tasks/Scheduler.cs @@ -0,0 +1,123 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Infrastructure.Tasks +{ +#pragma warning disable MA0048 // File name must match type name + public delegate Task SchedulerTask(CancellationToken ct); +#pragma warning restore MA0048 // File name must match type name + + public sealed class Scheduler + { + private readonly TaskCompletionSource tcs = new TaskCompletionSource(); + private readonly SemaphoreSlim semaphore; + private List? tasks; + private int pendingTasks; + + public Scheduler(int maxDegreeOfParallelism = 0) + { + if (maxDegreeOfParallelism <= 0) + { + maxDegreeOfParallelism = Environment.ProcessorCount * 2; + } + + semaphore = new SemaphoreSlim(maxDegreeOfParallelism); + } + + public void Schedule(SchedulerTask task) + { + if (pendingTasks < 0) + { + // Already completed. + return; + } + + if (pendingTasks >= 1) + { + // If we already in a tasks we just queue it with the semaphore. + ScheduleTask(task, default).Forget(); + return; + } + + tasks ??= new List(1); + tasks.Add(task); + } + + public async ValueTask CompleteAsync( + CancellationToken ct = default) + { + if (tasks == null || tasks.Count == 0) + { + return; + } + + // Use the value to indicate that the task have been started. + pendingTasks = 1; + try + { + RunTasks(ct).AsTask().Forget(); + + await tcs.Task; + } + finally + { + pendingTasks = -1; + } + } + + private async ValueTask RunTasks( + CancellationToken ct) + { + // If nothing needs to be done, we can just stop here. + if (tasks == null || tasks.Count == 0) + { + tcs.TrySetResult(true); + return; + } + + // Quick check to avoid the allocation of the list. + if (tasks.Count == 1) + { + await ScheduleTask(tasks[0], ct); + return; + } + + var runningTasks = new List(); + + foreach (var validationTask in tasks) + { + runningTasks.Add(ScheduleTask(validationTask, ct)); + } + + await Task.WhenAll(runningTasks); + } + + private async Task ScheduleTask(SchedulerTask task, + CancellationToken ct) + { + try + { + // Use the interlock to reduce degree of parallelization. + Interlocked.Increment(ref pendingTasks); + + await semaphore.WaitAsync(ct); + await task(ct); + } + catch + { + return; + } + finally + { + if (Interlocked.Decrement(ref pendingTasks) <= 1) + { + tcs.TrySetResult(true); + } + } + } + } +} diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/BulkUpdateContentsJobDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/BulkUpdateContentsJobDto.cs index 991a3e9b2..f328c0f81 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/BulkUpdateContentsJobDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/BulkUpdateContentsJobDto.cs @@ -18,7 +18,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models /// /// An optional query to identify the content to update. /// - public JsonQueryDto? Query { get; set; } + public QueryJsonDto? Query { get; set; } /// /// An optional id of the content to update. diff --git a/backend/src/Squidex/Areas/Api/Controllers/JsonQueryDto.cs b/backend/src/Squidex/Areas/Api/Controllers/QueryJsonDto.cs similarity index 90% rename from backend/src/Squidex/Areas/Api/Controllers/JsonQueryDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/QueryJsonDto.cs index 098f655fa..4e8bead88 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/JsonQueryDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/QueryJsonDto.cs @@ -12,7 +12,7 @@ using Squidex.Infrastructure.Queries; namespace Squidex.Areas.Api.Controllers { [JsonSchemaFlatten] - public sealed class JsonQueryDto : Query + public sealed class QueryJsonDto : Query { } } diff --git a/backend/src/Squidex/Config/Domain/AssetServices.cs b/backend/src/Squidex/Config/Domain/AssetServices.cs index a5c144374..48feaff64 100644 --- a/backend/src/Squidex/Config/Domain/AssetServices.cs +++ b/backend/src/Squidex/Config/Domain/AssetServices.cs @@ -155,7 +155,7 @@ namespace Squidex.Config.Domain services.AddSingletonAs(c => { - var mongoClient = Singletons.GetOrAdd(mongoConfiguration, s => new MongoClient(s)); + var mongoClient = StoreServices.GetMongoClient(mongoConfiguration); var mongoDatabase = mongoClient.GetDatabase(mongoDatabaseName); var gridFsbucket = new GridFSBucket(mongoDatabase, new GridFSBucketOptions diff --git a/backend/src/Squidex/Config/Domain/EventSourcingServices.cs b/backend/src/Squidex/Config/Domain/EventSourcingServices.cs index 8efaba2d1..fceed69e3 100644 --- a/backend/src/Squidex/Config/Domain/EventSourcingServices.cs +++ b/backend/src/Squidex/Config/Domain/EventSourcingServices.cs @@ -29,7 +29,7 @@ namespace Squidex.Config.Domain services.AddSingletonAs(c => { - var mongoClient = Singletons.GetOrAdd(mongoConfiguration, s => new MongoClient(s)); + var mongoClient = StoreServices.GetMongoClient(mongoConfiguration); var mongoDatabase = mongoClient.GetDatabase(mongoDatabaseName); return new MongoEventStore(mongoDatabase, c.GetRequiredService()); diff --git a/backend/src/Squidex/Config/Domain/StoreServices.cs b/backend/src/Squidex/Config/Domain/StoreServices.cs index aae4df809..484578ad0 100644 --- a/backend/src/Squidex/Config/Domain/StoreServices.cs +++ b/backend/src/Squidex/Config/Domain/StoreServices.cs @@ -8,6 +8,7 @@ using Microsoft.AspNetCore.Identity; using Migrations.Migrations.MongoDb; using MongoDB.Driver; +using MongoDB.Driver.Core.Extensions.DiagnosticSources; using Newtonsoft.Json; using Squidex.Assets; using Squidex.Domain.Apps.Entities; @@ -61,7 +62,7 @@ namespace Squidex.Config.Domain services.AddSingleton(typeof(ISnapshotStore<>), typeof(MongoSnapshotStore<>)); - services.AddSingletonAs(c => GetClient(mongoConfiguration)) + services.AddSingletonAs(c => GetMongoClient(mongoConfiguration)) .As(); services.AddSingletonAs(c => GetDatabase(c, mongoDatabaseName)) @@ -189,9 +190,19 @@ namespace Squidex.Config.Domain }); } - private static IMongoClient GetClient(string configuration) + public static IMongoClient GetMongoClient(string configuration) { - return Singletons.GetOrAdd(configuration, s => new MongoClient(s)); + return Singletons.GetOrAdd(configuration, connectionString => + { + var clientSettings = MongoClientSettings.FromConnectionString(connectionString); + + clientSettings.ClusterConfigurator = builder => + { + builder.Subscribe(new DiagnosticsActivityEventSubscriber()); + }; + + return new MongoClient(clientSettings); + }); } private static IMongoDatabase GetDatabase(IServiceProvider serviceProvider, string name) diff --git a/backend/src/Squidex/Config/Domain/TelemetryServices.cs b/backend/src/Squidex/Config/Domain/TelemetryServices.cs index bd5b86505..a0af5b05c 100644 --- a/backend/src/Squidex/Config/Domain/TelemetryServices.cs +++ b/backend/src/Squidex/Config/Domain/TelemetryServices.cs @@ -33,6 +33,7 @@ namespace Squidex.Config.Domain builder.AddAspNetCoreInstrumentation(); builder.AddHttpClientInstrumentation(); + builder.AddMongoDBInstrumentation(); foreach (var configurator in serviceProvider.GetRequiredService>()) { diff --git a/backend/src/Squidex/Config/Orleans/OrleansServices.cs b/backend/src/Squidex/Config/Orleans/OrleansServices.cs index 6ada3e54b..f2e4e294f 100644 --- a/backend/src/Squidex/Config/Orleans/OrleansServices.cs +++ b/backend/src/Squidex/Config/Orleans/OrleansServices.cs @@ -75,10 +75,12 @@ namespace Squidex.Config.Orleans options.HostSelf = false; }); - builder.AddIncomingGrainCallFilter(); + builder.AddOutgoingGrainCallFilter(); builder.AddIncomingGrainCallFilter(); + builder.AddIncomingGrainCallFilter(); builder.AddIncomingGrainCallFilter(); builder.AddIncomingGrainCallFilter(); + builder.AddIncomingGrainCallFilter(); builder.AddIncomingGrainCallFilter(); var (siloPort, gatewayPort) = GetPorts(config); diff --git a/backend/src/Squidex/Squidex.csproj b/backend/src/Squidex/Squidex.csproj index 29111af81..d598f3088 100644 --- a/backend/src/Squidex/Squidex.csproj +++ b/backend/src/Squidex/Squidex.csproj @@ -41,6 +41,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + @@ -50,6 +51,7 @@ + @@ -59,6 +61,7 @@ + diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs index 59d0442e3..842cda30e 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs @@ -42,7 +42,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules { public (bool Match, ValueTask) Format(EnrichedEvent @event, object value, string[] path) { - if (path[0] == "data" && value is JsonValue jsonValue && jsonValue.Type == JsonValueType.Array) + if (path[0] == "data" && value is JsonValue jsonValue && jsonValue.Value is JsonArray) { return (true, GetValueAsync()); } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs index 6faacf21d..6ed86065c 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs @@ -25,7 +25,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent private sealed class CustomFactory : IValidatorsFactory { - public IEnumerable CreateValueValidators(ValidatorContext context, IField field, ValidatorFactory createFieldValidator) + public IEnumerable CreateValueValidators(ValidationContext context, IField field, ValidatorFactory createFieldValidator) { if (field is IField assets) { diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs index f07503845..66214b04d 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs @@ -30,7 +30,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { var validator = A.Fake(); - A.CallTo(() => validator.ValidateAsync(A._, A._, A._)) + A.CallTo(() => validator.Validate(A._, A._)) .Throws(new InvalidOperationException()); var validatorFactory = A.Fake(); @@ -61,7 +61,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { var validator = A.Fake(); - A.CallTo(() => validator.ValidateAsync(A._, A._, A._)) + A.CallTo(() => validator.Validate(A._, A._)) .Throws(new InvalidOperationException()); var validatorFactory = A.Fake(); diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ReferencesFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ReferencesFieldTests.cs index e5f305522..e4bd0c182 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ReferencesFieldTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ReferencesFieldTests.cs @@ -34,7 +34,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent this.schemaId = schemaId; } - public IEnumerable CreateValueValidators(ValidatorContext context, IField field, ValidatorFactory createFieldValidator) + public IEnumerable CreateValueValidators(ValidationContext context, IField field, ValidatorFactory createFieldValidator) { if (field is IField references) { diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ValidationTestExtensions.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ValidationTestExtensions.cs index 9306bd53d..3d2a04b3d 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ValidationTestExtensions.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ValidationTestExtensions.cs @@ -5,8 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using FakeItEasy; -using Microsoft.Extensions.Logging; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.TestHelpers; @@ -26,7 +24,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent private static readonly NamedId AppId = NamedId.Of(DomainId.NewGuid(), "my-app"); private static readonly NamedId SchemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); - public static ValueTask ValidateAsync(this IValidator validator, object? value, IList errors, + public static async ValueTask ValidateAsync(this IValidator validator, object? value, IList errors, Schema? schema = null, ValidationMode mode = ValidationMode.Default, ValidationUpdater? updater = null, @@ -36,10 +34,14 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { var context = CreateContext(schema, mode, updater, action, components, contentId); - return validator.ValidateAsync(value, context, CreateFormatter(errors)); + validator.Validate(value, context); + + await context.Root.CompleteAsync(); + + AddErrors(context.Root, errors); } - public static ValueTask ValidateAsync(this IField field, object? value, IList errors, + public static async ValueTask ValidateAsync(this IField field, object? value, IList errors, Schema? schema = null, ValidationMode mode = ValidationMode.Default, ValidationUpdater? updater = null, @@ -50,9 +52,12 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { var context = CreateContext(schema, mode, updater, action, components, contentId); - var validator = new ValidatorBuilder(factory, context).ValueValidator(field); + new ValidatorBuilder(factory, context).ValueValidator(field) + .Validate(value, context); - return validator.ValidateAsync(value, context, CreateFormatter(errors)); + await context.Root.CompleteAsync(); + + AddErrors(context.Root, errors); } public static async Task ValidatePartialAsync(this ContentData data, PartitionResolver partitionResolver, IList errors, @@ -66,14 +71,10 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { var context = CreateContext(schema, mode, updater, action, components, contentId); - var validator = new ValidatorBuilder(factory, context).ContentValidator(partitionResolver); - - await validator.ValidateInputPartialAsync(data); + await new ValidatorBuilder(factory, context).ContentValidator(partitionResolver) + .ValidateInputPartialAsync(data); - foreach (var error in validator.Errors) - { - errors.Add(error); - } + errors.AddRange(context.Root.Errors); } public static async Task ValidateAsync(this ContentData data, PartitionResolver partitionResolver, IList errors, @@ -87,48 +88,49 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { var context = CreateContext(schema, mode, updater, action, components, contentId); - var validator = new ValidatorBuilder(factory, context).ContentValidator(partitionResolver); + await new ValidatorBuilder(factory, context).ContentValidator(partitionResolver) + .ValidateInputAsync(data); - await validator.ValidateInputAsync(data); - - foreach (var error in validator.Errors) - { - errors.Add(error); - } + errors.AddRange(context.Root.Errors); } - public static AddError CreateFormatter(IList errors) + private static void AddErrors(RootContext context, IList errors) { - return (field, message) => + foreach (var error in context.Errors) { - if (field == null || !field.Any()) + var propertyName = error.PropertyNames?.FirstOrDefault(); + + if (string.IsNullOrWhiteSpace(propertyName)) { - errors.Add(message); + errors.Add(error.Message); } else { - errors.Add($"{field.ToPathString()}: {message}"); + errors.Add($"{propertyName}: {error.Message}"); } - }; + } } - public static ValidationContext CreateContext( - Schema? schema = null, - ValidationMode mode = ValidationMode.Default, - ValidationUpdater? updater = null, + private static ValidationContext CreateContext( + Schema? schema, + ValidationMode mode, + ValidationUpdater? updater, ValidationAction action = ValidationAction.Upsert, ResolvedComponents? components = null, DomainId? contentId = null) { - var context = new ValidationContext( + var rootContext = new RootContext( TestUtils.DefaultSerializer, AppId, SchemaId, schema ?? new Schema(SchemaId.Name), - components ?? ResolvedComponents.Empty, - contentId ?? DomainId.NewGuid()); + contentId ?? DomainId.NewGuid(), + components ?? ResolvedComponents.Empty); - context = context.WithMode(mode).WithAction(action); + var context = + new ValidationContext(rootContext) + .WithMode(mode) + .WithAction(action); if (updater != null) { @@ -152,16 +154,12 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent public ContentValidator ContentValidator(PartitionResolver partitionResolver) { - var log = A.Fake>(); - - return new ContentValidator(partitionResolver, validationContext, CreateFactories(), log); + return new ContentValidator(partitionResolver, validationContext, CreateFactories()); } private IValidator CreateValueValidator(IField field) { - var log = A.Fake>(); - - return new FieldValidator(new AggregateValidator(CreateValueValidators(field), log), field); + return new FieldValidator(new AggregateValidator(CreateValueValidators(field)), field); } public IValidator ValueValidator(IField field) diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/ComponentValidatorTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/ComponentValidatorTests.cs index 0faa9ff95..cb0e589ac 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/ComponentValidatorTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/ComponentValidatorTests.cs @@ -40,7 +40,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators Assert.True(isFactoryCalled); - A.CallTo(() => validator.ValidateAsync(componentData, A._, A._)) + A.CallTo(() => validator.Validate(componentData, A._)) .MustHaveHappened(); } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/UniqueValidatorTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/UniqueValidatorTests.cs index 950584284..8af103677 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/UniqueValidatorTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/UniqueValidatorTests.cs @@ -28,7 +28,6 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators await sut.ValidateAsync(12.5, errors, updater: c => c.Nested("property").Nested("de")); Assert.Empty(errors); - Assert.Empty(filter); } @@ -45,11 +44,11 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators [Fact] public async Task Should_not_add_error_if_same_content_with_string_value_found() { - var ctx = ValidationTestExtensions.CreateContext().Nested("property").Nested("iv"); + var contentId = DomainId.NewGuid(); - var sut = new UniqueValidator(FoundDuplicates(ctx.ContentId)); + var sut = new UniqueValidator(FoundDuplicates(contentId)); - await sut.ValidateAsync("hi", ctx, ValidationTestExtensions.CreateFormatter(errors)); + await sut.ValidateAsync("hello", errors, contentId: contentId, updater: ctx => ctx.Nested("property").Nested("iv")); Assert.Empty(errors); } @@ -57,11 +56,11 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators [Fact] public async Task Should_not_add_error_if_same_content_with_double_value_found() { - var ctx = ValidationTestExtensions.CreateContext().Nested("property").Nested("iv"); + var contentId = DomainId.NewGuid(); - var sut = new UniqueValidator(FoundDuplicates(ctx.ContentId)); + var sut = new UniqueValidator(FoundDuplicates(contentId)); - await sut.ValidateAsync(12.5, ctx, ValidationTestExtensions.CreateFormatter(errors)); + await sut.ValidateAsync(12.5, errors, contentId: contentId, updater: ctx => ctx.Nested("property").Nested("iv")); Assert.Empty(errors); } @@ -73,12 +72,12 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators var sut = new UniqueValidator(FoundDuplicates(DomainId.NewGuid(), f => filter = f)); - await sut.ValidateAsync("hi", errors, updater: c => c.Nested("property").Nested("iv")); + await sut.ValidateAsync("hello", errors, updater: c => c.Nested("property").Nested("iv")); errors.Should().BeEquivalentTo( new[] { "property.iv: Another content with the same value exists." }); - Assert.Equal("Data.property.iv == 'hi'", filter); + Assert.Equal("Data.property.iv == 'hello'", filter); } [Fact] diff --git a/backend/tests/Squidex.Infrastructure.Tests/Collections/ListDictionaryTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Collections/ListDictionaryTests.cs index 11de228b6..7a1e0aa0e 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Collections/ListDictionaryTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Collections/ListDictionaryTests.cs @@ -10,6 +10,7 @@ using Xunit; #pragma warning disable xUnit2017 // Do not use Contains() to check if a value exists in a collection #pragma warning disable IDE0028 // Simplify collection initialization +#pragma warning disable CA1841 // Prefer Dictionary.Contains methods namespace Squidex.Infrastructure.Collections { @@ -67,6 +68,17 @@ namespace Squidex.Infrastructure.Collections Assert.Equal(10, sut[1]); } + [Fact] + public void Should_add_item_unsafe() + { + var sut = new ListDictionary(); + + sut.AddUnsafe(1, 10); + + Assert.Single(sut); + Assert.Equal(10, sut[1]); + } + [Fact] public void Should_add_item_as_pair() { diff --git a/backend/tests/Squidex.Infrastructure.Tests/Tasks/SchedulerTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Tasks/SchedulerTests.cs new file mode 100644 index 000000000..799012b45 --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/Tasks/SchedulerTests.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace Squidex.Infrastructure.Tasks +{ + public class SchedulerTests + { + private readonly ConcurrentBag results = new ConcurrentBag(); + private readonly Scheduler sut = new Scheduler(); + + [Fact] + public async Task Should_schedule_single_task() + { + Schedule(1); + + await sut.CompleteAsync(); + + Assert.Equal(new[] { 1 }, results.ToArray()); + } + + [Fact] + public async Task Should_schedule_multiple_tasks() + { + Schedule(1); + Schedule(2); + + await sut.CompleteAsync(); + + Assert.Equal(new[] { 1, 2 }, results.OrderBy(x => x).ToArray()); + } + + [Fact] + public async Task Should_schedule_nested_tasks() + { + sut.Schedule(async _ => + { + await Task.Delay(1); + + results.Add(1); + + sut.Schedule(async _ => + { + await Task.Delay(1); + + results.Add(2); + + Schedule(3); + }); + }); + + await sut.CompleteAsync(); + + Assert.Equal(new[] { 1, 2, 3 }, results.OrderBy(x => x).ToArray()); + } + + [Fact] + public async Task Should_ignore_schedule_after_completion() + { + Schedule(1); + + await sut.CompleteAsync(); + + Schedule(3); + + await Task.Delay(50); + + Assert.Equal(new[] { 1 }, results.OrderBy(x => x).ToArray()); + } + + private void Schedule(int value) + { + sut.Schedule(async _ => + { + await Task.Delay(1); + + results.Add(value); + }); + } + } +} diff --git a/frontend/src/app/features/content/shared/forms/field-editor.component.html b/frontend/src/app/features/content/shared/forms/field-editor.component.html index b96f56025..a2d51ef46 100644 --- a/frontend/src/app/features/content/shared/forms/field-editor.component.html +++ b/frontend/src/app/features/content/shared/forms/field-editor.component.html @@ -1,11 +1,11 @@
- -
diff --git a/frontend/src/app/features/rules/shared/actions/generic-action.component.html b/frontend/src/app/features/rules/shared/actions/generic-action.component.html index b01df742c..b1da472ba 100644 --- a/frontend/src/app/features/rules/shared/actions/generic-action.component.html +++ b/frontend/src/app/features/rules/shared/actions/generic-action.component.html @@ -40,7 +40,7 @@
- {{ 'i18n:rules.advancedFormattingHint' | sqxTranslate }}: {{ 'i18n:common.documentation' | sqxTranslate }} + {{ 'i18n:rules.advancedFormattingHint' | sqxTranslate }}: {{ 'i18n:common.documentation' | sqxTranslate }}