Browse Source

Feature/performance round (#888)

* Json improvements.

* Performance improvements.

* Fixes json handling performance.

* Telemetry improvements.

* Add missing files.
pull/892/head
Sebastian Stehle 4 years ago
committed by GitHub
parent
commit
a805a26da6
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 46
      backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaProducer.cs
  2. 43
      backend/extensions/Squidex.Extensions/Validation/CompositeUniqueValidator.cs
  3. 4
      backend/extensions/Squidex.Extensions/Validation/CompositeUniqueValidatorFactory.cs
  4. 18
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/RolesSurrogate.cs
  5. 16
      backend/src/Squidex.Domain.Apps.Core.Model/Assets/AssetMetadata.cs
  6. 10
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/Component.cs
  7. 5
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/ContentFieldData.cs
  8. 16
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/GeoJsonValue.cs
  9. 8
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/ContentFieldDataConverter.cs
  10. 34
      backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/FieldConverters.cs
  11. 26
      backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/StringFormatter.cs
  12. 12
      backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ValueConverters.cs
  13. 2
      backend/src/Squidex.Domain.Apps.Core.Operations/DefaultValues/DefaultValueExtensions.cs
  14. 51
      backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ContentReferencesExtensions.cs
  15. 6
      backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesCleaner.cs
  16. 148
      backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtractor.cs
  17. 2
      backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ValueReferencesConverter.cs
  18. 13
      backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTypeProvider.cs
  19. 28
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/JsonMapper.cs
  20. 16
      backend/src/Squidex.Domain.Apps.Core.Operations/Tags/TagNormalizer.cs
  21. 28
      backend/src/Squidex.Domain.Apps.Core.Operations/Templates/Extensions/ContentFluidExtension.cs
  22. 63
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs
  23. 6
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/DefaultFieldValueValidatorsFactory.cs
  24. 4
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/DefaultValidatorsFactory.cs
  25. 6
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/IValidator.cs
  26. 6
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/IValidatorsFactory.cs
  27. 88
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs
  28. 26
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueValidator.cs
  29. 79
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/RootContext.cs
  30. 46
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationContext.cs
  31. 36
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidatorContext.cs
  32. 25
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AggregateValidator.cs
  33. 6
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AllowedValuesValidator.cs
  34. 72
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AssetsValidator.cs
  35. 19
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionItemValidator.cs
  36. 16
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionValidator.cs
  37. 4
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ComponentValidator.cs
  38. 14
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/FieldValidator.cs
  39. 6
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/NoValueValidator.cs
  40. 18
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ObjectValidator.cs
  41. 10
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/PatternValidator.cs
  42. 12
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RangeValidator.cs
  43. 23
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ReferencesValidator.cs
  44. 8
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredStringValidator.cs
  45. 6
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredValidator.cs
  46. 12
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringLengthValidator.cs
  47. 20
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringTextValidator.cs
  48. 12
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueObjectValuesValidator.cs
  49. 15
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValidator.cs
  50. 6
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValuesValidator.cs
  51. 14
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs
  52. 5
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs
  53. 2
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs
  54. 10
      backend/src/Squidex.Domain.Apps.Entities/Apps/AppUISettingsGrain.cs
  55. 12
      backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsIndex.cs
  56. 2
      backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetEnricher.cs
  57. 39
      backend/src/Squidex.Domain.Apps.Entities/Contents/BackupContents.cs
  58. 18
      backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/ValidationExtensions.cs
  59. 8
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs
  60. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/DataInputGraphType.cs
  61. 36
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/FieldVisitor.cs
  62. 3
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs
  63. 7
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ConvertData.cs
  64. 9
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ResolveAssets.cs
  65. 9
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ResolveReferences.cs
  66. 5
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ScriptContent.cs
  67. 14
      backend/src/Squidex.Domain.Apps.Entities/Contents/Text/Extensions.cs
  68. 12
      backend/src/Squidex.Domain.Apps.Entities/Contents/Validation/DependencyValidatorsFactory.cs
  69. 5
      backend/src/Squidex.Domain.Apps.Entities/Rules/RuleDequeuerGrain.cs
  70. 4
      backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Writer.cs
  71. 10
      backend/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStoreBase.cs
  72. 46
      backend/src/Squidex.Infrastructure/CollectionExtensions.cs
  73. 7
      backend/src/Squidex.Infrastructure/Collections/ListDictionary.cs
  74. 48
      backend/src/Squidex.Infrastructure/EventSourcing/DefaultEventDataFormatter.cs
  75. 20
      backend/src/Squidex.Infrastructure/EventSourcing/EnvelopeExtensions.cs
  76. 122
      backend/src/Squidex.Infrastructure/Json/Newtonsoft/JsonValueConverter.cs
  77. 2
      backend/src/Squidex.Infrastructure/Json/Objects/JsonObject.cs
  78. 34
      backend/src/Squidex.Infrastructure/Json/Objects/JsonValue.cs
  79. 118
      backend/src/Squidex.Infrastructure/Orleans/ActivityPropagationGrainCallFilter.cs
  80. 2
      backend/src/Squidex.Infrastructure/Orleans/LoggingFilter.cs
  81. 80
      backend/src/Squidex.Infrastructure/Queries/Json/ValueConverter.cs
  82. 2
      backend/src/Squidex.Infrastructure/Reflection/TypeNameRegistry.cs
  83. 3
      backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj
  84. 123
      backend/src/Squidex.Infrastructure/Tasks/Scheduler.cs
  85. 2
      backend/src/Squidex/Areas/Api/Controllers/Contents/Models/BulkUpdateContentsJobDto.cs
  86. 2
      backend/src/Squidex/Areas/Api/Controllers/QueryJsonDto.cs
  87. 2
      backend/src/Squidex/Config/Domain/AssetServices.cs
  88. 2
      backend/src/Squidex/Config/Domain/EventSourcingServices.cs
  89. 17
      backend/src/Squidex/Config/Domain/StoreServices.cs
  90. 1
      backend/src/Squidex/Config/Domain/TelemetryServices.cs
  91. 4
      backend/src/Squidex/Config/Orleans/OrleansServices.cs
  92. 3
      backend/src/Squidex/Squidex.csproj
  93. 2
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs
  94. 2
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs
  95. 4
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs
  96. 2
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ReferencesFieldTests.cs
  97. 80
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ValidationTestExtensions.cs
  98. 2
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/ComponentValidatorTests.cs
  99. 17
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/UniqueValidatorTests.cs
  100. 12
      backend/tests/Squidex.Infrastructure.Tests/Collections/ListDictionaryTests.cs

46
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)
{
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):
switch (value.Value)
{
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<string, object>();
@ -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<object>();
@ -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));
}

43
backend/extensions/Squidex.Extensions/Validation/CompositeUniqueValidator.cs

@ -27,11 +27,17 @@ 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));
}
}
private async Task ValidateAsync(ContentData data, ValidationContext context)
{
var validateableFields = context.Root.Schema.Fields.Where(IsValidateableField);
var filters = new List<FilterNode<ClrValue>>();
@ -49,12 +55,11 @@ namespace Squidex.Extensions.Validation
{
var filter = ClrFilter.And(filters);
var found = await contentRepository.QueryIdsAsync(context.AppId.Id, context.SchemaId.Id, filter);
var found = await contentRepository.QueryIdsAsync(context.Root.AppId.Id, context.Root.SchemaId.Id, filter);
if (found.Any(x => x.Id != context.ContentId))
if (found.Any(x => x.Id != context.Root.ContentId))
{
addError(Enumerable.Empty<string>(), "A content with the same values already exist.");
}
context.AddError(Enumerable.Empty<string>(), "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;

4
backend/extensions/Squidex.Extensions/Validation/CompositeUniqueValidatorFactory.cs

@ -20,9 +20,9 @@ namespace Squidex.Extensions.Validation
this.contentRepository = contentRepository;
}
public IEnumerable<IValidator> CreateContentValidators(ValidatorContext context, ValidatorFactory createFieldValidator)
public IEnumerable<IValidator> 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);
}

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

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

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

5
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<string, JsonValue>, IEquatable<ContentFieldData>
public sealed class ContentFieldData : Dictionary<string, JsonValue>, IEquatable<ContentFieldData>
{
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;

16
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<string, JsonValue> 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;
}

8
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<JsonValue>(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;

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

26
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}";
}

12
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<AssetsFieldProperties> && value.Type == JsonValueType.Array && shouldHandle(field, parent))
if (field is IField<AssetsFieldProperties> && 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());
}
}

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

51
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<DomainId> GetReferencedIds(this ContentData source, Schema schema,
ResolvedComponents components, int referencesPerField = int.MaxValue)
{
Guard.NotNull(schema);
var ids = new HashSet<DomainId>();
AddReferencedIds(source, schema, ids, components, referencesPerField);
return ids;
}
public static void AddReferencedIds(this ContentData source, Schema schema, HashSet<DomainId> 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<IField> fields, HashSet<DomainId> 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<DomainId> result,
ResolvedComponents components, int referencesPerField = int.MaxValue)
public static void AddReferencedIds(this JsonValue value, IField field, HashSet<DomainId> 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<DomainId> 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<DomainId>();
ReferencesExtractor.Extract(field, value, result, referencesPerField, components);
AddReferencedIds(value, field, result, components, take);
return result;
}

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

148
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<None, ReferencesExtractor.Args>
internal static class ReferencesExtractor
{
private static readonly ReferencesExtractor Instance = new ReferencesExtractor();
public record struct Args(JsonValue Value, ISet<DomainId> Result, int Take, ResolvedComponents Components);
private ReferencesExtractor()
{
}
public static None Extract(IField field, JsonValue value, HashSet<DomainId> result, int take, ResolvedComponents components)
public static void Extract(IField field, JsonValue value, HashSet<DomainId> 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<IField> schema, ContentData data, HashSet<DomainId> 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<AssetsFieldProperties> field, Args args)
public static void Extract(IField field, ContentData data, HashSet<DomainId> result, int take, ResolvedComponents components)
{
AddIds(ref args);
return None.Value;
}
public None Visit(IField<ReferencesFieldProperties> field, Args args)
if (CanHaveReferences(field.RawProperties) && data.TryGetValue(field.Name, out var fieldData) && fieldData != null)
{
AddIds(ref args);
return None.Value;
}
public None Visit(IField<BooleanFieldProperties> field, Args args)
foreach (var (_, value) in fieldData)
{
return None.Value;
Extract(field, value, result, take, components);
}
}
}
public None Visit(IField<ComponentFieldProperties> field, Args args)
private static void ExtractCore(IField field, Args args)
{
switch (field)
{
case IField<AssetsFieldProperties>:
AddIds(ref args);
break;
case IField<ReferencesFieldProperties>:
AddIds(ref args);
break;
case IField<ComponentFieldProperties>:
ExtractFromComponent(args.Value, args);
return None.Value;
break;
case IField<ComponentsFieldProperties>:
ExtractFromComponents(args);
break;
case IArrayField arrayField:
ExtractFromArray(arrayField, args);
break;
}
}
public None Visit(IField<ComponentsFieldProperties> field, Args args)
private static void ExtractFromArray(IArrayField field, 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;
ExtractFromItem(field, value, args);
}
public None Visit(IField<DateTimeFieldProperties> field, Args args)
{
return None.Value;
}
public None Visit(IField<GeolocationFieldProperties> field, Args args)
{
return None.Value;
}
public None Visit(IField<JsonFieldProperties> field, Args args)
private static void ExtractFromComponents(Args args)
{
return None.Value;
}
public None Visit(IField<NumberFieldProperties> field, Args args)
if (args.Value.Value is JsonArray a)
{
return None.Value;
}
public None Visit(IField<StringFieldProperties> field, Args args)
foreach (var value in a)
{
return None.Value;
ExtractFromComponent(value, args);
}
public None Visit(IField<TagsFieldProperties> field, Args args)
{
return None.Value;
}
public None Visit(IField<UIFieldProperties> field, Args args)
{
return None.Value;
}
private void ExtractFromArrayItem(IArrayField field, JsonValue value, Args args)
private static void ExtractFromItem(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++;

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

13
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<Type>();
foreach (var type in eventTypes)
static IEnumerable<Type> 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))
{

28
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<JsonValue> 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<string, JsonValue> obj, Engine engine)
private static JsValue FromObject(JsonObject obj, Engine engine)
{
var target = new ObjectInstance(engine);

16
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<string> values, ICollection<JsonValue> 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);
}
}

28
backend/src/Squidex.Domain.Apps.Core.Operations/Templates/Extensions/ContentFluidExtension.cs

@ -24,20 +24,20 @@ namespace Squidex.Domain.Apps.Core.Templates.Extensions
FluidValue.SetTypeMapping<JsonValue>(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<JsonValue, object?>((value, name) =>
{
if (value.Type == JsonValueType.Object)
if (value.Value is JsonObject o)
{
return value.AsObject.GetOrDefault(name);
return o.GetOrDefault(name);
}
return null;

63
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<IValidatorsFactory> factories;
private readonly ILogger<ContentValidator> log;
private readonly ConcurrentBag<ValidationError> errors = new ConcurrentBag<ValidationError>();
public IReadOnlyCollection<ValidationError> Errors
public IEnumerable<ValidationError> Errors
{
get => errors;
get => context.Root.Errors;
}
public ContentValidator(PartitionResolver partitionResolver, ValidationContext context, IEnumerable<IValidatorsFactory> factories,
ILogger<ContentValidator> log)
public ContentValidator(PartitionResolver partitionResolver, ValidationContext context,
IEnumerable<IValidatorsFactory> 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<string> 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<string, (bool IsOptional, IValidator Validator)>(context.Schema.Fields.Count);
var fieldValidators = new Dictionary<string, (bool IsOptional, IValidator Validator)>(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<JsonValue>(partitioningValidators, isPartial, typeName), 1)), log);
new ObjectValidator<JsonValue>(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<IValidator> CreateContentValidators()

6
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<IValidator> CreateValidators(ValidatorContext context, IField field, ValidatorFactory factory)
public static IEnumerable<IValidator> 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;

4
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<IValidator> CreateFieldValidators(ValidatorContext context, IField field, ValidatorFactory factory)
public IEnumerable<IValidator> CreateFieldValidators(ValidationContext context, IField field, ValidatorFactory factory)
{
if (field is IField<UIFieldProperties>)
{
@ -20,7 +20,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
}
}
public IEnumerable<IValidator> CreateValueValidators(ValidatorContext context, IField field, ValidatorFactory factory)
public IEnumerable<IValidator> CreateValueValidators(ValidationContext context, IField field, ValidatorFactory factory)
{
return DefaultFieldValueValidatorsFactory.CreateValidators(context, field, factory);
}

6
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<string> path, string message);
public interface IValidator
{
ValueTask ValidateAsync(object? value, ValidationContext context, AddError addError);
void Validate(object? value, ValidationContext context);
}
}

6
backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/IValidatorsFactory.cs

@ -15,17 +15,17 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
public interface IValidatorsFactory
{
IEnumerable<IValidator> CreateFieldValidators(ValidatorContext context, IField field, ValidatorFactory factory)
IEnumerable<IValidator> CreateFieldValidators(ValidationContext context, IField field, ValidatorFactory factory)
{
yield break;
}
IEnumerable<IValidator> CreateValueValidators(ValidatorContext context, IField field, ValidatorFactory factory)
IEnumerable<IValidator> CreateValueValidators(ValidationContext context, IField field, ValidatorFactory factory)
{
yield break;
}
IEnumerable<IValidator> CreateContentValidators(ValidatorContext context, ValidatorFactory factory)
IEnumerable<IValidator> CreateContentValidators(ValidationContext context, ValidatorFactory factory)
{
yield break;
}

88
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<BooleanFieldProperties> 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<NumberFieldProperties> 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<StringFieldProperties> 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<DateTimeFieldProperties> 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<DomainId>(a.Count);
var result = new List<DomainId>(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<string?>(a.Count);
var result = new List<string?>(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<JsonObject>(a.Count);
var result = new List<JsonObject>(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<DomainId>? allowedIds)
{
if (value.Type == JsonValueType.Array)
if (value.Value is JsonArray a)
{
var array = value.AsArray;
var result = new List<Component>(array.Count);
var result = new List<Component>(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<DomainId>? 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);

26
backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueValidator.cs

@ -48,7 +48,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
public bool Visit(IField<BooleanFieldProperties> field, Args args)
{
return args.Value.Type == JsonValueType.Boolean;
return args.Value.Value is bool;
}
public bool Visit(IField<ComponentFieldProperties> field, Args args)
@ -63,9 +63,9 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
public bool Visit(IField<DateTimeFieldProperties> 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<NumberFieldProperties> field, Args args)
{
return args.Value.Type == JsonValueType.Number;
return args.Value.Value is double;
}
public bool Visit(IField<ReferencesFieldProperties> field, Args args)
@ -97,7 +97,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
public bool Visit(IField<StringFieldProperties> field, Args args)
{
return args.Value.Type == JsonValueType.String;
return args.Value.Value is string;
}
public bool Visit(IField<TagsFieldProperties> 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))
{

79
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<ValidationError> errors = new ConcurrentBag<ValidationError>();
private readonly Scheduler scheduler = new Scheduler();
public IJsonSerializer JsonSerializer { get; }
public DomainId ContentId { get; }
public NamedId<DomainId> AppId { get; }
public NamedId<DomainId> SchemaId { get; }
public Schema Schema { get; }
public ResolvedComponents Components { get; }
public IEnumerable<ValidationError> Errors
{
get => errors;
}
public RootContext(
IJsonSerializer jsonSerializer,
NamedId<DomainId> appId,
NamedId<DomainId> schemaId,
Schema schema,
DomainId contentId,
ResolvedComponents components)
{
AppId = appId;
Components = components;
ContentId = contentId;
JsonSerializer = jsonSerializer;
Schema = schema;
SchemaId = schemaId;
}
public void AddError(IEnumerable<string> 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);
}
}
}

46
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<string> Path { get; private set; } = ImmutableQueue<string>.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<DomainId> appId,
NamedId<DomainId> 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<string> 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<ValidationContext> 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 };
}
}
}

36
backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidatorContext.cs

@ -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<DomainId> AppId { get; }
public NamedId<DomainId> SchemaId { get; }
public Schema Schema { get; }
public ValidationMode Mode { get; protected set; }
public ValidationAction Action { get; protected set; }
protected ValidatorContext(
NamedId<DomainId> appId,
NamedId<DomainId> schemaId,
Schema schema)
{
AppId = appId;
Schema = schema;
SchemaId = schemaId;
}
}
}

25
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<ContentValidator> log;
public AggregateValidator(IEnumerable<IValidator>? validators,
ILogger<ContentValidator> log)
public AggregateValidator(IEnumerable<IValidator>? 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"));
}
}
}

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

72
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<DomainId>();
if (value is ICollection<DomainId> { 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<string> path, AddError addError)
private void ValidateCommon(IAssetInfo asset, ImmutableQueue<string> 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<string> path, AddError addError)
private void ValidateType(IAssetInfo asset, ImmutableQueue<string> 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<string> path, AddError addError)
private void ValidateDimensions(int w, int h, ImmutableQueue<string> 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 }));
}
}
}

19
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<object>().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++;
}
}
}
}

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

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

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

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

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

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

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

23
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<DomainId>();
if (value is ICollection<DomainId> { 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);
}
}
}

8
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)

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

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

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

12
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<JsonObject> objects && objects.Count() > 1)
{
Validate(context, addError, objects);
Validate(objects, context);
}
else if (value is IEnumerable<Component> 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<JsonObject> items)
private void Validate(IEnumerable<JsonObject> items, ValidationContext context)
{
var duplicates = new HashSet<JsonValue>(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;
}
}

15
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();
@ -43,14 +43,19 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
}
if (filter != null)
{
context.Root.AddTask(ct => ValidateCoreAsync(context, filter));
}
}
}
private async Task ValidateCoreAsync(ValidationContext context, FilterNode<ClrValue> filter)
{
var found = await checkUniqueness(filter);
if (found.Any(x => x.Id != context.ContentId))
if (found.Any(x => x.Id != context.Root.ContentId))
{
addError(context.Path, T.Get("contents.validation.unique"));
}
}
context.AddError(context.Path, T.Get("contents.validation.unique"));
}
}

6
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<TValue> : IValidator
{
public ValueTask ValidateAsync(object? value, ValidationContext context, AddError addError)
public void Validate(object? value, ValidationContext context)
{
if (value is IEnumerable<TValue> 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;
}
}
}

14
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs

@ -86,7 +86,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
public async Task<IResultList<IAssetEntity>> 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<IReadOnlyList<DomainId>> QueryIdsAsync(DomainId appId, HashSet<DomainId> 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<IReadOnlyList<DomainId>> 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<IAssetEntity?> 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<IAssetEntity?> 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<IAssetEntity?> 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<IAssetEntity?> 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)

5
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<DomainId>();
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<DomainId>();
return entity;
}

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

@ -43,7 +43,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Rules
public async Task<List<DomainId>> 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);

10
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
{

12
backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsIndex.cs

@ -55,7 +55,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes
public async Task<List<IAppEntity>> 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<IAppEntity?> 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<IAppEntity?> 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<IReadOnlyCollection<DomainId>> 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<IReadOnlyCollection<DomainId>> 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<DomainId> GetAppIdAsync(string name)
{
using (Telemetry.Activities.StartActivity("AppProvider/GetAppIdAsync"))
using (Telemetry.Activities.StartActivity("AppsIndex/GetAppIdAsync"))
{
var result = await Cache().GetAppIdsAsync(new[] { name });

2
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();

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

18
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<IJsonSerializer>(),
var rootContext =
new RootContext(operation.Resolve<IJsonSerializer>(),
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<IEnumerable<IValidatorsFactory>>(),
operation.Resolve<ILogger<ContentValidator>>());
operation.Resolve<IEnumerable<IValidatorsFactory>>());
return validator;
}

8
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs

@ -188,14 +188,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{
List<DomainId>? 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<DomainId>();
result.Add(DomainId.Create(item.AsString));
result.Add(DomainId.Create(id));
}
}
}

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

36
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!;

3
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs

@ -65,9 +65,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
var result = SimpleMapper.Map(content, new ContentEntity());
if (cloneData)
{
using (Telemetry.Activities.StartActivity("ContentEnricher/CloneData"))
{
result.Data = result.Data.Clone();
}
}
results.Add(result);
}

7
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<ValueConverter?> CleanReferencesAsync(Context context, IEnumerable<ContentEntity> contents, ProvideSchema schemas,
CancellationToken ct)
{
if (!context.ShouldSkipCleanup())
if (context.ShouldSkipCleanup())
{
return null;
}
using (Telemetry.Activities.StartActivity("ConvertData/CleanReferencesAsync"))
{
var ids = new HashSet<DomainId>();

9
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<DomainId, ContentEntity> contents, ILookup<DomainId, IEnrichedAssetEntity> assets)
{
HashSet<DomainId>? 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<DomainId>();
fieldIds.Clear();
partitionValue.AddReferencedIds(field, fieldIds, components);
var referencedAsset =
field.GetReferencedIds(partitionValue, components)
fieldIds
.Select(x => assets[x])
.SelectMany(x => x)
.FirstOrDefault();

9
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<ContentEntity> contents, ILookup<DomainId, IEnrichedContentEntity> references, ProvideSchema schemas)
{
HashSet<DomainId>? fieldIds = null;
var formatted = new Dictionary<IContentEntity, JsonObject>();
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<DomainId>();
fieldIds.Clear();
partitionValue.AddReferencedIds(field, fieldIds, components);
var referencedContents =
field.GetReferencedIds(partitionValue, components)
fieldIds
.Select(x => references[x])
.SelectMany(x => x)
.ToList();

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

14
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<string, StringBuilder> 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);
}

12
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<IValidator> CreateValueValidators(ValidatorContext context, IField field, ValidatorFactory createFieldValidator)
public IEnumerable<IValidator> 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;

5
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)

4
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<WriteModel<MongoEventCommit>>();

10
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<TState>(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);

46
backend/src/Squidex.Infrastructure/CollectionExtensions.cs

@ -248,10 +248,10 @@ namespace Squidex.Infrastructure
public static bool EqualsDictionary<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> dictionary, IReadOnlyDictionary<TKey, TValue>? other) where TKey : notnull
{
return EqualsDictionary(dictionary, other, EqualityComparer<TKey>.Default, EqualityComparer<TValue>.Default);
return EqualsDictionary(dictionary, other, EqualityComparer<TValue>.Default);
}
public static bool EqualsDictionary<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> dictionary, IReadOnlyDictionary<TKey, TValue>? other, IEqualityComparer<TKey> keyComparer, IEqualityComparer<TValue> valueComparer) where TKey : notnull
public static bool EqualsDictionary<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> dictionary, IReadOnlyDictionary<TKey, TValue>? other, IEqualityComparer<TValue> valueComparer) where TKey : notnull
{
if (other == null)
{
@ -268,9 +268,15 @@ namespace Squidex.Infrastructure
return false;
}
var comparer = new KeyValuePairComparer<TKey, TValue>(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<T>(this IReadOnlyList<T> list, IReadOnlyList<T>? other)
@ -393,37 +399,5 @@ namespace Squidex.Infrastructure
index++;
}
}
public sealed class KeyValuePairComparer<TKey, TValue> : IEqualityComparer<KeyValuePair<TKey, TValue>> where TKey : notnull
{
private readonly IEqualityComparer<TKey> keyComparer;
private readonly IEqualityComparer<TValue> valueComparer;
public KeyValuePairComparer(IEqualityComparer<TKey> keyComparer, IEqualityComparer<TValue> valueComparer)
{
this.keyComparer = keyComparer;
this.valueComparer = valueComparer;
}
public bool Equals(KeyValuePair<TKey, TValue> x, KeyValuePair<TKey, TValue> y)
{
return keyComparer.Equals(x.Key, y.Key) && valueComparer.Equals(x.Value, y.Value);
}
public int GetHashCode(KeyValuePair<TKey, TValue> obj)
{
return keyComparer.GetHashCode(obj.Key) ^ ValueHashCode(obj);
}
private int ValueHashCode(KeyValuePair<TKey, TValue> obj)
{
if (Equals(obj.Value, null))
{
return 0;
}
return valueComparer.GetHashCode(obj.Value);
}
}
}
}

7
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<TKey, TValue>(key, value));
AddUnsafe(key, value);
}
public void Add(KeyValuePair<TKey, TValue> 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<TKey, TValue>(key, value));
}
public void Clear()
{
entries.Clear();

48
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<IEvent>? ParseIfKnown(StoredEvent storedEvent)
{
try
{
return Parse(storedEvent);
return ParseCore(storedEvent);
}
catch (TypeNameNotFoundException)
public Envelope<IEvent> Parse(StoredEvent storedEvent)
{
return null;
var envelope = ParseCore(storedEvent);
if (envelope == null)
{
throw new TypeNameNotFoundException($"Cannot find event with type name '{storedEvent.Data.Type}'.");
}
return envelope;
}
public Envelope<IEvent> Parse(StoredEvent storedEvent)
private Envelope<IEvent>? ParseCore(StoredEvent storedEvent)
{
Guard.NotNull(storedEvent);
var eventData = storedEvent.Data;
var payloadType = typeNameRegistry.GetTypeOrNull(storedEvent.Data.Type);
if (payloadType == null)
{
return null;
}
var payloadType = typeNameRegistry.GetType(eventData.Type);
var payloadValue = serializer.Deserialize<IEvent>(eventData.Payload, payloadType);
var payloadValue = serializer.Deserialize<IEvent>(storedEvent.Data.Payload, payloadType);
if (payloadValue is IMigrated<IEvent> migratedEvent)
{
payloadValue = migratedEvent.Migrate();
if (ReferenceEquals(migratedEvent, payloadValue))
{
Debug.WriteLine("Migration should return new event.");
}
}
var envelope = new Envelope<IEvent>(payloadValue, eventData.Headers);
var envelope = new Envelope<IEvent>(payloadValue, storedEvent.Data.Headers);
envelope.SetEventPosition(storedEvent.EventPosition);
envelope.SetEventStreamNumber(storedEvent.EventStreamNumber);
@ -65,19 +68,14 @@ namespace Squidex.Infrastructure.EventSourcing
public EventData ToEventData(Envelope<IEvent> envelope, Guid commitId, bool migrate = true)
{
var eventPayload = envelope.Payload;
if (migrate && eventPayload is IMigrated<IEvent> migratedEvent)
{
eventPayload = migratedEvent.Migrate();
var payloadValue = envelope.Payload;
if (ReferenceEquals(migratedEvent, eventPayload))
if (migrate && payloadValue is IMigrated<IEvent> migratedEvent)
{
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);

20
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;

122
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<string, JsonValue> 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);

2
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<string, JsonValue>, IEquatable<JsonObject>
public class JsonObject : Dictionary<string, JsonValue>, IEquatable<JsonObject>
{
public JsonObject()
{

34
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<JsonValue>
@ -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!;

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

2
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))
{

80
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<ClrValue?>(errors, path, value.AsArray, TryParseDynamic);
var array = ParseArray<ClrValue?>(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<bool>(errors, path, value.AsArray, TryParseBoolean);
result = ParseArray<bool>(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<double>(errors, path, value.AsArray, TryParseNumber);
result = ParseArray<double>(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<Guid>(errors, path, value.AsArray, TryParseGuid);
result = ParseArray<Guid>(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<Instant>(errors, path, value.AsArray, TryParseDateTime);
result = ParseArray<Instant>(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<string>(errors, path, value.AsArray, TryParseString!);
result = ParseArray<string>(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;
}

2
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);

3
backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj

@ -62,7 +62,4 @@
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<Folder Include="Dump\" />
</ItemGroup>
</Project>

123
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<bool> tcs = new TaskCompletionSource<bool>();
private readonly SemaphoreSlim semaphore;
private List<SchedulerTask>? 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<SchedulerTask>(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<Task>();
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);
}
}
}
}
}

2
backend/src/Squidex/Areas/Api/Controllers/Contents/Models/BulkUpdateContentsJobDto.cs

@ -18,7 +18,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
/// <summary>
/// An optional query to identify the content to update.
/// </summary>
public JsonQueryDto? Query { get; set; }
public QueryJsonDto? Query { get; set; }
/// <summary>
/// An optional id of the content to update.

2
backend/src/Squidex/Areas/Api/Controllers/JsonQueryDto.cs → 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<JsonValue>
public sealed class QueryJsonDto : Query<JsonValue>
{
}
}

2
backend/src/Squidex/Config/Domain/AssetServices.cs

@ -155,7 +155,7 @@ namespace Squidex.Config.Domain
services.AddSingletonAs(c =>
{
var mongoClient = Singletons<IMongoClient>.GetOrAdd(mongoConfiguration, s => new MongoClient(s));
var mongoClient = StoreServices.GetMongoClient(mongoConfiguration);
var mongoDatabase = mongoClient.GetDatabase(mongoDatabaseName);
var gridFsbucket = new GridFSBucket<string>(mongoDatabase, new GridFSBucketOptions

2
backend/src/Squidex/Config/Domain/EventSourcingServices.cs

@ -29,7 +29,7 @@ namespace Squidex.Config.Domain
services.AddSingletonAs(c =>
{
var mongoClient = Singletons<IMongoClient>.GetOrAdd(mongoConfiguration, s => new MongoClient(s));
var mongoClient = StoreServices.GetMongoClient(mongoConfiguration);
var mongoDatabase = mongoClient.GetDatabase(mongoDatabaseName);
return new MongoEventStore(mongoDatabase, c.GetRequiredService<IEventNotifier>());

17
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<IMongoClient>();
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<IMongoClient>.GetOrAdd(configuration, s => new MongoClient(s));
return Singletons<IMongoClient>.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)

1
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<IEnumerable<ITelemetryConfigurator>>())
{

4
backend/src/Squidex/Config/Orleans/OrleansServices.cs

@ -75,10 +75,12 @@ namespace Squidex.Config.Orleans
options.HostSelf = false;
});
builder.AddIncomingGrainCallFilter<LoggingFilter>();
builder.AddOutgoingGrainCallFilter<ActivityPropagationOutgoingGrainCallFilter>();
builder.AddIncomingGrainCallFilter<ExceptionWrapperFilter>();
builder.AddIncomingGrainCallFilter<ActivityPropagationIncomingGrainCallFilter>();
builder.AddIncomingGrainCallFilter<ActivationLimiterFilter>();
builder.AddIncomingGrainCallFilter<LocalCacheFilter>();
builder.AddIncomingGrainCallFilter<LoggingFilter>();
builder.AddIncomingGrainCallFilter<StateFilter>();
var (siloPort, gatewayPort) = GetPorts(config);

3
backend/src/Squidex/Squidex.csproj

@ -41,6 +41,7 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Logging.Configuration" Version="6.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.Google" Version="6.0.5" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.MicrosoftAccount" Version="6.0.5" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="6.0.5" />
@ -50,6 +51,7 @@
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="6.0.0" />
<PackageReference Include="Microsoft.CodeAnalysis.RulesetToEditorconfigConverter" Version="3.3.3" />
<PackageReference Include="Microsoft.Data.Edm" Version="5.8.5" />
<PackageReference Include="Microsoft.Extensions.Logging.Configuration" Version="6.0.0" />
<PackageReference Include="Microsoft.IdentityModel.Logging" Version="6.14.1" />
<PackageReference Include="Microsoft.IdentityModel.Protocols" Version="6.14.1" />
<PackageReference Include="Microsoft.OData.Core" Version="7.11.0" />
@ -59,6 +61,7 @@
<PackageReference Include="Microsoft.Orleans.OrleansRuntime" Version="3.6.2" />
<PackageReference Include="Namotion.Reflection" Version="2.0.10" />
<PackageReference Include="MongoDB.Driver" Version="2.15.1" />
<PackageReference Include="MongoDB.Driver.Core.Extensions.OpenTelemetry" Version="1.0.0" />
<PackageReference Include="Namotion.Reflection" Version="2.0.10" ExcludeAssets="all" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="NJsonSchema" Version="10.7.2" />

2
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<string?>) 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());
}

2
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<IValidator> CreateValueValidators(ValidatorContext context, IField field, ValidatorFactory createFieldValidator)
public IEnumerable<IValidator> CreateValueValidators(ValidationContext context, IField field, ValidatorFactory createFieldValidator)
{
if (field is IField<AssetsFieldProperties> assets)
{

4
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<IValidator>();
A.CallTo(() => validator.ValidateAsync(A<object?>._, A<ValidationContext>._, A<AddError>._))
A.CallTo(() => validator.Validate(A<object?>._, A<ValidationContext>._))
.Throws(new InvalidOperationException());
var validatorFactory = A.Fake<IValidatorsFactory>();
@ -61,7 +61,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{
var validator = A.Fake<IValidator>();
A.CallTo(() => validator.ValidateAsync(A<object?>._, A<ValidationContext>._, A<AddError>._))
A.CallTo(() => validator.Validate(A<object?>._, A<ValidationContext>._))
.Throws(new InvalidOperationException());
var validatorFactory = A.Fake<IValidatorsFactory>();

2
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<IValidator> CreateValueValidators(ValidatorContext context, IField field, ValidatorFactory createFieldValidator)
public IEnumerable<IValidator> CreateValueValidators(ValidationContext context, IField field, ValidatorFactory createFieldValidator)
{
if (field is IField<ReferencesFieldProperties> references)
{

80
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<DomainId> AppId = NamedId.Of(DomainId.NewGuid(), "my-app");
private static readonly NamedId<DomainId> SchemaId = NamedId.Of(DomainId.NewGuid(), "my-schema");
public static ValueTask ValidateAsync(this IValidator validator, object? value, IList<string> errors,
public static async ValueTask ValidateAsync(this IValidator validator, object? value, IList<string> 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<string> errors,
public static async ValueTask ValidateAsync(this IField field, object? value, IList<string> 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<ValidationError> 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<ValidationError> 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<string> errors)
private static void AddErrors(RootContext context, IList<string> 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<ILogger<ContentValidator>>();
return new ContentValidator(partitionResolver, validationContext, CreateFactories(), log);
return new ContentValidator(partitionResolver, validationContext, CreateFactories());
}
private IValidator CreateValueValidator(IField field)
{
var log = A.Fake<ILogger<ContentValidator>>();
return new FieldValidator(new AggregateValidator(CreateValueValidators(field), log), field);
return new FieldValidator(new AggregateValidator(CreateValueValidators(field)), field);
}
public IValidator ValueValidator(IField field)

2
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<ValidationContext>._, A<AddError>._))
A.CallTo(() => validator.Validate(componentData, A<ValidationContext>._))
.MustHaveHappened();
}

17
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]

12
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<int, int>();
sut.AddUnsafe(1, 10);
Assert.Single(sut);
Assert.Equal(10, sut[1]);
}
[Fact]
public void Should_add_item_as_pair()
{

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

Loading…
Cancel
Save